Minor fix

This commit is contained in:
yangzhi 2019-04-14 13:34:51 +08:00
parent c0ef6a5b53
commit 0e859cfbcb
12 changed files with 671 additions and 54 deletions

View File

@ -28,46 +28,34 @@ ldnull
callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D) callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D)
Optional Section
Fix back button£º Optional
GamePad Input
Modify class StardewValley.Game1, modify method updateAndroidMenus(), modify Instructions at beginning: Modify class StardewValley.Game1, modify method updateAndroidMenus(), modify Instructions at beginning:
ldsfld StardewValley.InputState StardewValley.Game1::input ldsfld StardewValley.InputState StardewValley.Game1::input
callvirt Microsoft.Xna.Framework.Input.GamePadState StardewValley.InputState::GetGamePadState() callvirt Microsoft.Xna.Framework.Input.GamePadState StardewValley.InputState::GetGamePadState()
Json Asset£º
checkForAction Prefix
Json Asset Support£º 0 ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
5 ldarg.0
6 callvirt System.Boolean StardewValley.ModHooks::OnObject_checkForAction(StardewValley.Object)
11 brtrue.s -> (6) ldarg.2
13 ldc.i4.0
14 ret
Modify class StardewValley.Object, modify method checkForAction,insert instructions at beginning: isIndexOkForBasicShippedCategory Postfix
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks 0 ldarg.0
ldarg.0 1 ldc.i4 434
callvirt System.Boolean StardewValley.ModHooks::OnObject_checkForAction(StardewValley.Object) 6 bne.un.s -> (5) ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
brtrue.s -> (6) ldarg.2 8 ldc.i4.0
ldc.i4.0 9 ret
ret 10 ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
15 ldarg.0
modify method isIndexOkForBasicShippedCategory,replace instructions: 16 ldloca.s -> (0) (System.Boolean)
ldarg.0 18 callvirt System.Void StardewValley.ModHooks::OnObject_isIndexOkForBasicShippedCategory(System.Int32,System.Boolean&)
ldc.i4 434 23 ldloc.0
bne.un.s -> (5) ldsfld StardewValley.ModHooks StardewValley.Game1::hooks 24 ret
ldc.i4.0
ret
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
ldarg.0
ldloca.s -> (0) (System.Boolean)
callvirt System.Void StardewValley.ModHooks::OnObject_isIndexOkForBasicShippedCategory(System.Int32,System.Boolean&)
ldloc.0
ret
modify method canBePlacedHere insert instructions at beginning:
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
ldarg.0
ldarg.1
ldarg.2
ldloca.s -> (1) (System.Boolean)
callvirt System.Boolean StardewValley.ModHooks::OnObject_canBePlacedHere(StardewValley.Object,StardewValley.GameLocation,Microsoft.Xna.Framework.Vector2,System.Boolean&)
brtrue.s -> (9) ldarg.1
ldloc.1
ret

View File

@ -544,6 +544,7 @@
<Compile Include="SMAPI\Framework\ModHelpers\InputHelper.cs" /> <Compile Include="SMAPI\Framework\ModHelpers\InputHelper.cs" />
<Compile Include="SMAPI\Framework\ModHelpers\ModHelper.cs" /> <Compile Include="SMAPI\Framework\ModHelpers\ModHelper.cs" />
<Compile Include="SMAPI\Framework\ModHelpers\ModRegistryHelper.cs" /> <Compile Include="SMAPI\Framework\ModHelpers\ModRegistryHelper.cs" />
<Compile Include="SMAPI\Framework\ModHelpers\MultiplayerHelper.cs" />
<Compile Include="SMAPI\Framework\ModHelpers\ReflectionHelper.cs" /> <Compile Include="SMAPI\Framework\ModHelpers\ReflectionHelper.cs" />
<Compile Include="SMAPI\Framework\ModHelpers\TranslationHelper.cs" /> <Compile Include="SMAPI\Framework\ModHelpers\TranslationHelper.cs" />
<Compile Include="SMAPI\Framework\ModLoading\AssemblyDefinitionResolver.cs" /> <Compile Include="SMAPI\Framework\ModLoading\AssemblyDefinitionResolver.cs" />
@ -600,6 +601,7 @@
<Compile Include="SMAPI\Framework\SGameConstructorHack.cs" /> <Compile Include="SMAPI\Framework\SGameConstructorHack.cs" />
<Compile Include="SMAPI\Framework\Singleton.cs" /> <Compile Include="SMAPI\Framework\Singleton.cs" />
<Compile Include="SMAPI\Framework\SModHooks.cs" /> <Compile Include="SMAPI\Framework\SModHooks.cs" />
<Compile Include="SMAPI\Framework\SMultiplayer.cs" />
<Compile Include="SMAPI\Framework\StateTracking\Comparers\EquatableComparer.cs" /> <Compile Include="SMAPI\Framework\StateTracking\Comparers\EquatableComparer.cs" />
<Compile Include="SMAPI\Framework\StateTracking\Comparers\GenericEqualsComparer.cs" /> <Compile Include="SMAPI\Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
<Compile Include="SMAPI\Framework\StateTracking\Comparers\ObjectReferenceComparer.cs" /> <Compile Include="SMAPI\Framework\StateTracking\Comparers\ObjectReferenceComparer.cs" />
@ -641,6 +643,7 @@
<Compile Include="SMAPI\IModLinked.cs" /> <Compile Include="SMAPI\IModLinked.cs" />
<Compile Include="SMAPI\IModRegistry.cs" /> <Compile Include="SMAPI\IModRegistry.cs" />
<Compile Include="SMAPI\IMonitor.cs" /> <Compile Include="SMAPI\IMonitor.cs" />
<Compile Include="SMAPI\IMultiplayerHelper.cs" />
<Compile Include="SMAPI\IMultiplayerPeer.cs" /> <Compile Include="SMAPI\IMultiplayerPeer.cs" />
<Compile Include="SMAPI\IMultiplayerPeerMod.cs" /> <Compile Include="SMAPI\IMultiplayerPeerMod.cs" />
<Compile Include="SMAPI\Internal\ConsoleWriting\ColorfulConsoleWriter.cs" /> <Compile Include="SMAPI\Internal\ConsoleWriting\ColorfulConsoleWriter.cs" />

View File

@ -4,6 +4,7 @@ using StardewValley;
using StardewModdingAPI.Framework; using StardewModdingAPI.Framework;
using System.Threading; using System.Threading;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System.IO;
namespace SMDroid namespace SMDroid
{ {
@ -19,7 +20,7 @@ namespace SMDroid
public ModEntry() public ModEntry()
{ {
this.core = new SCore("/sdcard/SMDroid/Mods", false); this.core = new SCore(Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid/Mods"), false);
} }
public override LocalizedContentManager OnGame1_CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) public override LocalizedContentManager OnGame1_CreateContentManager(IServiceProvider serviceProvider, string rootDirectory)
{ {
@ -30,7 +31,6 @@ namespace SMDroid
this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, SGame.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset); this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, SGame.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset);
this.NextContentManagerIsMain = true; this.NextContentManagerIsMain = true;
this.core.RunInteractively(this.ContentCore); this.core.RunInteractively(this.ContentCore);
SGame.printLog("ROOT Directory:" + rootDirectory);
return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); return this.ContentCore.CreateGameContentManager("Game1._temporaryContent");
} }

View File

@ -32,16 +32,16 @@ namespace StardewModdingAPI
public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform; public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
/// <summary>The path to the game folder.</summary> /// <summary>The path to the game folder.</summary>
public static string ExecutionPath { get; } = "/sdcard/SMDroid"; public static string ExecutionPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path , "SMDroid");
/// <summary>The directory path containing Stardew Valley's app data.</summary> /// <summary>The directory path containing Stardew Valley's app data.</summary>
public static string DataPath { get; } = Path.Combine("/sdcard", "SMDroid"); public static string DataPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid");
/// <summary>The directory path in which error logs should be stored.</summary> /// <summary>The directory path in which error logs should be stored.</summary>
public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs"); public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs");
/// <summary>The directory path where all saves are stored.</summary> /// <summary>The directory path where all saves are stored.</summary>
public static string SavesPath { get; } = Path.Combine("/sdcard", "StardewValley"); public static string SavesPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "StardewValley");
/// <summary>The name of the current save folder (if save info is available, regardless of whether the save file exists yet).</summary> /// <summary>The name of the current save folder (if save info is available, regardless of whether the save file exists yet).</summary>
public static string SaveFolderName => Constants.GetSaveFolderName(); public static string SaveFolderName => Constants.GetSaveFolderName();
@ -56,7 +56,7 @@ namespace StardewModdingAPI
internal const string HomePageUrl = "https://smapi.io"; internal const string HomePageUrl = "https://smapi.io";
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary> /// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
internal static readonly string InternalFilesPath = "/sdcard/SMDroid/smapi-internal"; internal static readonly string InternalFilesPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid/smapi-internal");
/// <summary>The file path for the SMAPI configuration file.</summary> /// <summary>The file path for the SMAPI configuration file.</summary>
internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json"); internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json");

View File

@ -81,7 +81,7 @@ namespace StardewModdingAPI.Framework.Input
try try
{ {
// get new states // get new states
GamePadState realController = GamePad.GetState(PlayerIndex.One); GamePadState realController = GamePad.GetState(Game1.playerOneIndex);
KeyboardState realKeyboard = Keyboard.GetState(); KeyboardState realKeyboard = Keyboard.GetState();
MouseState realMouse = Mouse.GetState(); MouseState realMouse = Mouse.GetState();
var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);

View File

@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public ICommandHelper ConsoleCommands { get; } public ICommandHelper ConsoleCommands { get; }
/// <summary>Provides multiplayer utilities.</summary> /// <summary>Provides multiplayer utilities.</summary>
//public IMultiplayerHelper Multiplayer { get; } public IMultiplayerHelper Multiplayer { get; }
/// <summary>An API for reading translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> &lt; <c>pt.json</c> &lt; <c>default.json</c>).</summary> /// <summary>An API for reading translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> &lt; <c>pt.json</c> &lt; <c>default.json</c>).</summary>
public ITranslationHelper Translation { get; } public ITranslationHelper Translation { get; }
@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param> /// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param>
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper)
: base(modID) : base(modID)
{ {
// validate directory // validate directory
@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry));
this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper));
this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper));
//this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer));
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
this.Events = events; this.Events = events;
} }

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using StardewModdingAPI.Framework.Networking;
using StardewValley;
namespace StardewModdingAPI.Framework.ModHelpers
{
/// <summary>Provides multiplayer utilities.</summary>
internal class MultiplayerHelper : BaseHelper, IMultiplayerHelper
{
/*********
** Fields
*********/
/// <summary>SMAPI's core multiplayer utility.</summary>
private readonly SMultiplayer Multiplayer;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="multiplayer">SMAPI's core multiplayer utility.</param>
public MultiplayerHelper(string modID, SMultiplayer multiplayer)
: base(modID)
{
this.Multiplayer = multiplayer;
}
/// <summary>Get a new multiplayer ID.</summary>
public long GetNewID()
{
return this.Multiplayer.getNewID();
}
/// <summary>Get the locations which are being actively synced from the host.</summary>
public IEnumerable<GameLocation> GetActiveLocations()
{
return this.Multiplayer.activeLocations();
}
/// <summary>Get a connected player.</summary>
/// <param name="id">The player's unique ID.</param>
/// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
public IMultiplayerPeer GetConnectedPlayer(long id)
{
return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer)
? peer
: null;
}
/// <summary>Get all connected players.</summary>
public IEnumerable<IMultiplayerPeer> GetConnectedPlayers()
{
return this.Multiplayer.Peers.Values;
}
/// <summary>Send a message to mods installed by connected players.</summary>
/// <typeparam name="TMessage">The data type. This can be a class with a default constructor, or a value type.</typeparam>
/// <param name="message">The data to send over the network.</param>
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
/// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
/// <param name="playerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
/// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
public void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null)
{
this.Multiplayer.BroadcastModMessage(
message: message,
messageType: messageType,
fromModID: this.ModID,
toModIDs: modIDs,
toPlayerIDs: playerIDs
);
}
}
}

View File

@ -981,7 +981,7 @@ namespace StardewModdingAPI.Framework
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper); IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
//IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer);
IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
{ {
@ -991,7 +991,7 @@ namespace StardewModdingAPI.Framework
return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper);
} }
modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, translationHelper); modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
} }
// init mod // init mod

View File

@ -72,6 +72,7 @@ namespace StardewModdingAPI.Framework
internal bool OnObjectCanBePlacedHere(SObject instance, GameLocation location, Vector2 tile, ref bool result) internal bool OnObjectCanBePlacedHere(SObject instance, GameLocation location, Vector2 tile, ref bool result)
{ {
ObjectCanBePlacedHereEventArgs args = new ObjectCanBePlacedHereEventArgs(instance, location, tile, result); ObjectCanBePlacedHereEventArgs args = new ObjectCanBePlacedHereEventArgs(instance, location, tile, result);
args.__result = result;
bool run =this.Events.ObjectCanBePlacedHere.RaiseForChainRun(args); bool run =this.Events.ObjectCanBePlacedHere.RaiseForChainRun(args);
result = args.__result; result = args.__result;
return run; return run;
@ -80,6 +81,7 @@ namespace StardewModdingAPI.Framework
internal void OnObjectIsIndexOkForBasicShippedCategory(int index, ref bool result) internal void OnObjectIsIndexOkForBasicShippedCategory(int index, ref bool result)
{ {
ObjectIsIndexOkForBasicShippedCategoryEventArgs args = new ObjectIsIndexOkForBasicShippedCategoryEventArgs(index, result); ObjectIsIndexOkForBasicShippedCategoryEventArgs args = new ObjectIsIndexOkForBasicShippedCategoryEventArgs(index, result);
args.__result = result;
this.Events.ObjectIsIndexOkForBasicShippedCategory.RaiseForChainRun(args); this.Events.ObjectIsIndexOkForBasicShippedCategory.RaiseForChainRun(args);
result = args.__result; result = args.__result;
} }
@ -147,8 +149,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Manages input visible to the game.</summary> /// <summary>Manages input visible to the game.</summary>
public SInputState Input => (SInputState)this.Reflection.GetField<InputState>(typeof(Game1), "input").GetValue(); public SInputState Input => (SInputState)this.Reflection.GetField<InputState>(typeof(Game1), "input").GetValue();
///// <summary>The game's core multiplayer utility.</summary> /// <summary>The game's core multiplayer utility.</summary>
//public SMultiplayer Multiplayer => (SMultiplayer)this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").GetValue(); public SMultiplayer Multiplayer => (SMultiplayer)this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").GetValue();
/// <summary>A list of queued commands to execute.</summary> /// <summary>A list of queued commands to execute.</summary>
/// <remarks>This property must be threadsafe, since it's accessed from a separate console input thread.</remarks> /// <remarks>This property must be threadsafe, since it's accessed from a separate console input thread.</remarks>
@ -198,7 +200,7 @@ namespace StardewModdingAPI.Framework
this.OnGameInitialised = onGameInitialised; this.OnGameInitialised = onGameInitialised;
this.OnGameExiting = onGameExiting; this.OnGameExiting = onGameExiting;
this.Reflection.GetField<InputState>(typeof(Game1), "input").SetValue(new SInputState()); this.Reflection.GetField<InputState>(typeof(Game1), "input").SetValue(new SInputState());
//this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").SetValue(new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived)); this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").SetValue(new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived));
//Game1.hooks = new SModHooks(this.OnNewDayAfterFade); //Game1.hooks = new SModHooks(this.OnNewDayAfterFade);
// init observables // init observables
@ -500,7 +502,7 @@ namespace StardewModdingAPI.Framework
/********* /*********
** Locale changed events ** Locale changed events
*********/ *********/
if (this.Watchers.LocaleWatcher.IsChanged) if (this.Watchers.LocaleWatcher.IsChanged || SGame.TicksElapsed == 0)
{ {
this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace); this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace);
this.OnLocaleChanged(); this.OnLocaleChanged();
@ -1605,6 +1607,7 @@ namespace StardewModdingAPI.Framework
string s = Game1.content.LoadString(@"Strings\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); string s = Game1.content.LoadString(@"Strings\StringsFromCSFiles:DayTimeMoneyBox.cs.10378");
SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 0x60, 0x20, "", 1f, -1, 0.088f); SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 0x60, 0x20, "", 1f, -1, 0.088f);
} }
events.Rendered.RaiseEmpty();
_spriteBatchEnd.Invoke(); _spriteBatchEnd.Invoke();
drawOverlays.Invoke(Game1.spriteBatch); drawOverlays.Invoke(Game1.spriteBatch);
renderScreenBuffer.Invoke(BlendState.Opaque); renderScreenBuffer.Invoke(BlendState.Opaque);
@ -1615,12 +1618,12 @@ namespace StardewModdingAPI.Framework
SpriteBatchBegin.Invoke(1f); SpriteBatchBegin.Invoke(1f);
events.RenderingHud.RaiseEmpty(); events.RenderingHud.RaiseEmpty();
DrawHUD.Invoke(); DrawHUD.Invoke();
events.RenderedHud.RaiseEmpty();
if (((Game1.currentLocation != null) && !(Game1.activeClickableMenu is GameMenu)) && !(Game1.activeClickableMenu is QuestLog)) if (((Game1.currentLocation != null) && !(Game1.activeClickableMenu is GameMenu)) && !(Game1.activeClickableMenu is QuestLog))
{ {
Game1.currentLocation.drawAboveAlwaysFrontLayerText(Game1.spriteBatch); Game1.currentLocation.drawAboveAlwaysFrontLayerText(Game1.spriteBatch);
} }
DrawAfterMap.Invoke(); DrawAfterMap.Invoke();
events.RenderedHud.RaiseEmpty();
_spriteBatchEnd.Invoke(); _spriteBatchEnd.Invoke();
if (Game1.tutorialManager != null) if (Game1.tutorialManager != null)
{ {
@ -1658,7 +1661,6 @@ namespace StardewModdingAPI.Framework
_spriteBatchEnd.Invoke(); _spriteBatchEnd.Invoke();
} }
DrawTutorialUI.Invoke(); DrawTutorialUI.Invoke();
events.Rendered.RaiseEmpty();
} }
} }
} }

View File

@ -0,0 +1,515 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
//using Galaxy.Api;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewValley;
using StardewValley.Network;
using StardewValley.SDKs;
namespace StardewModdingAPI.Framework
{
/// <summary>SMAPI's implementation of the game's core multiplayer logic.</summary>
/// <remarks>
/// SMAPI syncs mod context to all players through the host as such:
/// 1. Farmhand sends ModContext + PlayerIntro.
/// 2. If host receives ModContext: it stores the context, replies with known contexts, and forwards it to other farmhands.
/// 3. If host receives PlayerIntro before ModContext: it stores a 'vanilla player' context, and forwards it to other farmhands.
/// 4. If farmhand receives ModContext: it stores it.
/// 5. If farmhand receives ServerIntro without a preceding ModContext: it stores a 'vanilla host' context.
/// 6. If farmhand receives PlayerIntro without a preceding ModContext AND it's not the host peer: it stores a 'vanilla player' context.
///
/// Once a farmhand/server stored a context, messages can be sent to that player through the SMAPI APIs.
/// </remarks>
internal class SMultiplayer : Multiplayer
{
/*********
** Fields
*********/
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
/// <summary>Tracks the installed mods.</summary>
private readonly ModRegistry ModRegistry;
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
/// <summary>Simplifies access to private code.</summary>
private readonly Reflector Reflection;
/// <summary>Manages SMAPI events.</summary>
private readonly EventManager EventManager;
/// <summary>A callback to invoke when a mod message is received.</summary>
private readonly Action<ModMessageModel> OnModMessageReceived;
/*********
** Accessors
*********/
/// <summary>The metadata for each connected peer.</summary>
public IDictionary<long, MultiplayerPeer> Peers { get; } = new Dictionary<long, MultiplayerPeer>();
/// <summary>The metadata for the host player, if the current player is a farmhand.</summary>
public MultiplayerPeer HostPeer;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="eventManager">Manages SMAPI events.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="modRegistry">Tracks the installed mods.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onModMessageReceived">A callback to invoke when a mod message is received.</param>
public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action<ModMessageModel> onModMessageReceived)
{
this.Monitor = monitor;
this.EventManager = eventManager;
this.JsonHelper = jsonHelper;
this.ModRegistry = modRegistry;
this.Reflection = reflection;
this.OnModMessageReceived = onModMessageReceived;
}
/// <summary>Perform cleanup needed when a multiplayer session ends.</summary>
public void CleanupOnMultiplayerExit()
{
this.Peers.Clear();
this.HostPeer = null;
}
/// <summary>Initialise a client before the game connects to a remote server.</summary>
/// <param name="client">The client to initialise.</param>
public override Client InitClient(Client client)
{
switch (client)
{
//case LidgrenClient _:
// {
// string address = this.Reflection.GetField<string>(client, "address").GetValue();
// return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
// }
//case GalaxyNetClient _:
// {
// GalaxyID address = this.Reflection.GetField<GalaxyID>(client, "lobbyId").GetValue();
// return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
// }
default:
this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}", LogLevel.Trace);
return client;
}
}
/// <summary>Initialise a server before the game connects to an incoming player.</summary>
/// <param name="server">The server to initialise.</param>
public override Server InitServer(Server server)
{
switch (server)
{
//case LidgrenServer _:
// {
// IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
// return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage);
// }
//case GalaxyNetServer _:
// {
// IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
// return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage);
// }
default:
this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}", LogLevel.Trace);
return server;
}
}
/// <summary>A callback raised when sending a message as a farmhand.</summary>
/// <param name="message">The message being sent.</param>
/// <param name="sendMessage">Send an arbitrary message through the client.</param>
/// <param name="resume">Resume sending the underlying message.</param>
protected void OnClientSendingMessage(OutgoingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);
switch (message.MessageType)
{
// sync mod context (step 1)
case (byte)MessageType.PlayerIntroduction:
sendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));
resume();
break;
// run default logic
default:
resume();
break;
}
}
/// <summary>Process an incoming network message as the host player.</summary>
/// <param name="message">The message to process.</param>
/// <param name="sendMessage">A method which sends the given message to the client.</param>
/// <param name="resume">Process the message using the game's default logic.</param>
public void OnServerProcessingMessage(IncomingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);
switch (message.MessageType)
{
// sync mod context (step 2)
case (byte)MessageType.ModContext:
{
// parse message
RemoteContextModel model = this.ReadContext(message.Reader);
this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
// store peer
MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false);
if (this.Peers.ContainsKey(message.FarmerID))
{
this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info);
this.Peers.Remove(message.FarmerID);
return;
}
this.AddPeer(newPeer, canBeHost: false, raiseEvent: false);
// reply with own context
this.Monitor.VerboseLog(" Replying with host context...");
newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));
// reply with other players' context
foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
{
this.Monitor.VerboseLog($" Replying with context for player {otherPeer.PlayerID}...");
newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer)));
}
// forward to other peers
if (this.Peers.Count > 1)
{
object[] fields = this.GetContextSyncMessageFields(newPeer);
foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
{
this.Monitor.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}...");
otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields));
}
}
// raise event
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer));
}
break;
// handle player intro
case (byte)MessageType.PlayerIntroduction:
// store peer if new
if (!this.Peers.ContainsKey(message.FarmerID))
{
this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace);
MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false);
this.AddPeer(peer, canBeHost: false);
}
resume();
break;
// handle mod message
case (byte)MessageType.ModMessage:
this.ReceiveModMessage(message);
break;
default:
resume();
break;
}
}
/// <summary>Process an incoming network message as a farmhand.</summary>
/// <param name="message">The message to process.</param>
/// <param name="sendMessage">Send an arbitrary message through the client.</param>
/// <param name="resume">Resume processing the message using the game's default logic.</param>
/// <returns>Returns whether the message was handled.</returns>
public void OnClientProcessingMessage(IncomingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);
switch (message.MessageType)
{
// mod context sync (step 4)
case (byte)MessageType.ModContext:
{
// parse message
RemoteContextModel model = this.ReadContext(message.Reader);
this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
// store peer
MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null);
if (peer.IsHost && this.HostPeer != null)
{
this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error);
return;
}
this.AddPeer(peer, canBeHost: true);
}
break;
// handle server intro
case (byte)MessageType.ServerIntroduction:
{
// store peer
if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null)
{
this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace);
this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false);
}
resume();
break;
}
// handle player intro
case (byte)MessageType.PlayerIntroduction:
{
// store peer
if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer))
{
peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null);
this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace);
this.AddPeer(peer, canBeHost: true);
}
resume();
break;
}
// handle mod message
case (byte)MessageType.ModMessage:
this.ReceiveModMessage(message);
break;
default:
resume();
break;
}
}
/// <summary>Remove players who are disconnecting.</summary>
protected override void removeDisconnectedFarmers()
{
//foreach (long playerID in this.disconnectingFarmers)
//{
// if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
// {
// this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace);
// this.Peers.Remove(playerID);
// this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer));
// }
//}
base.removeDisconnectedFarmers();
}
/// <summary>Broadcast a mod message to matching players.</summary>
/// <param name="message">The data to send over the network.</param>
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
/// <param name="fromModID">The unique ID of the mod sending the message.</param>
/// <param name="toModIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
/// <param name="toPlayerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
{
// validate
if (message == null)
throw new ArgumentNullException(nameof(message));
if (string.IsNullOrWhiteSpace(messageType))
throw new ArgumentNullException(nameof(messageType));
if (string.IsNullOrWhiteSpace(fromModID))
throw new ArgumentNullException(nameof(fromModID));
if (!this.Peers.Any())
{
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players.");
return;
}
// filter player IDs
HashSet<long> playerIDs = null;
if (toPlayerIDs != null && toPlayerIDs.Any())
{
playerIDs = new HashSet<long>(toPlayerIDs);
playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id));
if (!playerIDs.Any())
{
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected.");
return;
}
}
// get data to send
ModMessageModel model = new ModMessageModel(
fromPlayerID: Game1.player.UniqueMultiplayerID,
fromModID: fromModID,
toModIDs: toModIDs,
toPlayerIDs: playerIDs?.ToArray(),
type: messageType,
data: JToken.FromObject(message)
);
string data = JsonConvert.SerializeObject(model, Formatting.None);
// log message
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace);
// send message
if (Context.IsMainPlayer)
{
foreach (MultiplayerPeer peer in this.Peers.Values)
{
if (playerIDs == null || playerIDs.Contains(peer.PlayerID))
{
model.ToPlayerIDs = new[] { peer.PlayerID };
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data));
}
}
}
else if (this.HostPeer != null && this.HostPeer.HasSmapi)
this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
else
this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
}
/*********
** Private methods
*********/
/// <summary>Save a received peer.</summary>
/// <param name="peer">The peer to add.</param>
/// <param name="canBeHost">Whether to track the peer as the host if applicable.</param>
/// <param name="raiseEvent">Whether to raise the <see cref="Events.EventManager.PeerContextReceived"/> event.</param>
private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true)
{
// store
this.Peers[peer.PlayerID] = peer;
if (canBeHost && peer.IsHost)
this.HostPeer = peer;
// raise event
if (raiseEvent)
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer));
}
/// <summary>Read the metadata context for a player.</summary>
/// <param name="reader">The stream reader.</param>
private RemoteContextModel ReadContext(BinaryReader reader)
{
string data = reader.ReadString();
RemoteContextModel model = this.JsonHelper.Deserialise<RemoteContextModel>(data);
return model.ApiVersion != null
? model
: null; // no data available for unmodded players
}
/// <summary>Receive a mod message sent from another player's mods.</summary>
/// <param name="message">The raw message to parse.</param>
private void ReceiveModMessage(IncomingMessage message)
{
// parse message
string json = message.Reader.ReadString();
ModMessageModel model = this.JsonHelper.Deserialise<ModMessageModel>(json);
HashSet<long> playerIDs = new HashSet<long>(model.ToPlayerIDs ?? this.GetKnownPlayerIDs());
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Received message: {json}.", LogLevel.Trace);
// notify local mods
if (playerIDs.Contains(Game1.player.UniqueMultiplayerID))
this.OnModMessageReceived(model);
// forward to other players
if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID))
{
ModMessageModel newModel = new ModMessageModel(model);
foreach (long playerID in playerIDs)
{
if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
{
newModel.ToPlayerIDs = new[] { peer.PlayerID };
this.Monitor.VerboseLog($" Forwarding message to player {peer.PlayerID}.");
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None)));
}
}
}
}
/// <summary>Get all connected player IDs, including the current player.</summary>
private IEnumerable<long> GetKnownPlayerIDs()
{
yield return Game1.player.UniqueMultiplayerID;
foreach (long peerID in this.Peers.Keys)
yield return peerID;
}
/// <summary>Get the fields to include in a context sync message sent to other players.</summary>
private object[] GetContextSyncMessageFields()
{
RemoteContextModel model = new RemoteContextModel
{
IsHost = Context.IsWorldReady && Context.IsMainPlayer,
Platform = Constants.TargetPlatform,
ApiVersion = Constants.ApiVersion,
GameVersion = Constants.GameVersion,
Mods = this.ModRegistry
.GetAll()
.Select(mod => new RemoteContextModModel
{
ID = mod.Manifest.UniqueID,
Name = mod.Manifest.Name,
Version = mod.Manifest.Version
})
.ToArray()
};
return new object[] { this.JsonHelper.Serialise(model, Formatting.None) };
}
/// <summary>Get the fields to include in a context sync message sent to other players.</summary>
/// <param name="peer">The peer whose data to represent.</param>
private object[] GetContextSyncMessageFields(IMultiplayerPeer peer)
{
if (!peer.HasSmapi)
return new object[] { "{}" };
RemoteContextModel model = new RemoteContextModel
{
IsHost = peer.IsHost,
Platform = peer.Platform.Value,
ApiVersion = peer.ApiVersion,
GameVersion = peer.GameVersion,
Mods = peer.Mods
.Select(mod => new RemoteContextModModel
{
ID = mod.ID,
Name = mod.Name,
Version = mod.Version
})
.ToArray()
};
return new object[] { this.JsonHelper.Serialise(model, Formatting.None) };
}
}
}

View File

@ -35,8 +35,8 @@ namespace StardewModdingAPI
/// <summary>Metadata about loaded mods.</summary> /// <summary>Metadata about loaded mods.</summary>
IModRegistry ModRegistry { get; } IModRegistry ModRegistry { get; }
///// <summary>Provides multiplayer utilities.</summary> /// <summary>Provides multiplayer utilities.</summary>
//IMultiplayerHelper Multiplayer { get; } IMultiplayerHelper Multiplayer { get; }
/// <summary>Provides translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> &lt; <c>pt.json</c> &lt; <c>default.json</c>).</summary> /// <summary>Provides translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> &lt; <c>pt.json</c> &lt; <c>default.json</c>).</summary>
ITranslationHelper Translation { get; } ITranslationHelper Translation { get; }

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using StardewValley;
namespace StardewModdingAPI
{
/// <summary>Provides multiplayer utilities.</summary>
public interface IMultiplayerHelper : IModLinked
{
/// <summary>Get a new multiplayer ID.</summary>
long GetNewID();
/// <summary>Get the locations which are being actively synced from the host.</summary>
IEnumerable<GameLocation> GetActiveLocations();
/// <summary>Get a connected player.</summary>
/// <param name="id">The player's unique ID.</param>
/// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
IMultiplayerPeer GetConnectedPlayer(long id);
/// <summary>Get all connected players.</summary>
IEnumerable<IMultiplayerPeer> GetConnectedPlayers();
/// <summary>Send a message to mods installed by connected players.</summary>
/// <typeparam name="TMessage">The data type. This can be a class with a default constructor, or a value type.</typeparam>
/// <param name="message">The data to send over the network.</param>
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
/// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
/// <param name="playerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
/// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null);
}
}