diff options
Diffstat (limited to 'src/SMAPI')
| -rw-r--r-- | src/SMAPI/Events/IModEvents.cs | 3 | ||||
| -rw-r--r-- | src/SMAPI/Events/IMultiplayerEvents.cs | 11 | ||||
| -rw-r--r-- | src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 46 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/EventManager.cs | 8 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/ManagedEvent.cs | 24 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/ManagedEventBase.cs | 12 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/ModEvents.cs | 4 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 29 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs | 31 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Networking/ModMessageModel.cs | 72 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 14 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SCore.cs | 5 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SGame.cs | 25 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SMultiplayer.cs | 339 | ||||
| -rw-r--r-- | src/SMAPI/IMultiplayerHelper.cs | 13 | ||||
| -rw-r--r-- | src/SMAPI/IMultiplayerPeer.cs | 2 | ||||
| -rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 22 |
17 files changed, 544 insertions, 116 deletions
diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 76da7751..bd7ab880 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Events /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> IInputEvents Input { get; } + /// <summary>Events raised for multiplayer messages and connections.</summary> + IMultiplayerEvents Multiplayer { get; } + /// <summary>Events raised when the player data changes.</summary> IPlayerEvents Player { get; } diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs new file mode 100644 index 00000000..a6ac6fd3 --- /dev/null +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -0,0 +1,11 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// <summary>Events raised for multiplayer messages and connections.</summary> + public interface IMultiplayerEvents + { + /// <summary>Raised after a mod message is received over the network.</summary> + event EventHandler<ModMessageReceivedEventArgs> ModMessageReceived; + } +} diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs new file mode 100644 index 00000000..b1960a22 --- /dev/null +++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs @@ -0,0 +1,46 @@ +using System; +using StardewModdingAPI.Framework.Networking; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments when a mod receives a message over the network.</summary> + public class ModMessageReceivedEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// <summary>The underlying message model.</summary> + private readonly ModMessageModel Message; + + + /********* + ** Accessors + *********/ + /// <summary>The unique ID of the player from whose computer the message was sent.</summary> + public long FromPlayerID => this.Message.FromPlayerID; + + /// <summary>The unique ID of the mod which sent the message.</summary> + public string FromModID => this.Message.FromModID; + + /// <summary>A message type which can be used to decide whether it's the one you want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, so mods should check the <see cref="FromModID"/>.</summary> + public string Type => this.Message.Type; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="message">The received message.</param> + internal ModMessageReceivedEventArgs(ModMessageModel message) + { + this.Message = message; + } + + /// <summary>Read the message data into the given model type.</summary> + /// <typeparam name="TModel">The message model type.</typeparam> + public TModel ReadAs<TModel>() + { + return this.Message.Data.ToObject<TModel>(); + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 31b0346a..519cf48a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -99,6 +99,12 @@ namespace StardewModdingAPI.Framework.Events public readonly ManagedEvent<MouseWheelScrolledEventArgs> MouseWheelScrolled; /**** + ** Multiplayer + ****/ + /// <summary>Raised after a mod message is received over the network.</summary> + public readonly ManagedEvent<ModMessageReceivedEventArgs> ModMessageReceived; + + /**** ** Player ****/ /// <summary>Raised after items are added or removed to a player's inventory.</summary> @@ -374,6 +380,8 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ModMessageReceived = ManageEventOf<ModMessageReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.InventoryChanged = ManageEventOf<InventoryChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf<LevelChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); this.Warped = ManageEventOf<WarpedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index c1ebf6c7..65f6e38e 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -67,6 +67,30 @@ namespace StardewModdingAPI.Framework.Events } } } + + /// <summary>Raise the event and notify all handlers.</summary> + /// <param name="args">The event arguments to pass.</param> + /// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param> + public void RaiseForMods(TEventArgs args, Func<IModMetadata, bool> match) + { + if (this.Event == null) + return; + + foreach (EventHandler<TEventArgs> handler in this.CachedInvocationList) + { + if (match(this.GetSourceMod(handler))) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } } /// <summary>An event wrapper which intercepts and logs errors in handler code.</summary> diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs index f3a278dc..defd903a 100644 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -69,12 +69,22 @@ namespace StardewModdingAPI.Framework.Events this.SourceMods.Remove(handler); } + /// <summary>Get the mod which registered the given event handler, if available.</summary> + /// <param name="handler">The event handler.</param> + protected IModMetadata GetSourceMod(TEventHandler handler) + { + return this.SourceMods.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; + } + /// <summary>Log an exception from an event handler.</summary> /// <param name="handler">The event handler instance.</param> /// <param name="ex">The exception that was raised.</param> protected void LogError(TEventHandler handler, Exception ex) { - if (this.SourceMods.TryGetValue(handler, out IModMetadata mod)) + IModMetadata mod = this.GetSourceMod(handler); + if (mod != null) mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); else this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 7a318e8b..8ad3936c 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Events /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> public IInputEvents Input { get; } + /// <summary>Events raised for multiplayer messages and connections.</summary> + public IMultiplayerEvents Multiplayer { get; } + /// <summary>Events raised when the player data changes.</summary> public IPlayerEvents Player { get; } @@ -38,6 +41,7 @@ namespace StardewModdingAPI.Framework.Events this.Display = new ModDisplayEvents(mod, eventManager); this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); + this.Multiplayer = new ModMultiplayerEvents(mod, eventManager); this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); this.Specialised = new ModSpecialisedEvents(mod, eventManager); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs new file mode 100644 index 00000000..a830a54a --- /dev/null +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>Events raised for multiplayer messages and connections.</summary> + internal class ModMultiplayerEvents : ModEventsBase, IMultiplayerEvents + { + /********* + ** Accessors + *********/ + /// <summary>Raised after a mod message is received over the network.</summary> + public event EventHandler<ModMessageReceivedEventArgs> ModMessageReceived + { + add => this.EventManager.ModMessageReceived.Add(value); + remove => this.EventManager.ModMessageReceived.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The mod which uses this instance.</param> + /// <param name="eventManager">The underlying event manager.</param> + internal ModMultiplayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index 86f8e012..eedad0bc 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewModdingAPI.Framework.Networking; using StardewValley; @@ -26,18 +27,18 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Multiplayer = multiplayer; } - /// <summary>Get the locations which are being actively synced from the host.</summary> - public IEnumerable<GameLocation> GetActiveLocations() - { - return this.Multiplayer.activeLocations(); - } - /// <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> @@ -53,5 +54,23 @@ namespace StardewModdingAPI.Framework.ModHelpers { 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 + ); + } } } diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs new file mode 100644 index 00000000..7ee39863 --- /dev/null +++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Framework.Networking +{ + /// <summary>The metadata for a mod message.</summary> + internal class ModMessageModel + { + /********* + ** Accessors + *********/ + /**** + ** Origin + ****/ + /// <summary>The unique ID of the player who broadcast the message.</summary> + public long FromPlayerID { get; set; } + + /// <summary>The unique ID of the mod which broadcast the message.</summary> + public string FromModID { get; set; } + + /**** + ** Destination + ****/ + /// <summary>The players who should receive the message, or <c>null</c> for all players.</summary> + public long[] ToPlayerIDs { get; set; } + + /// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary> + public string[] ToModIDs { get; set; } + + /// <summary>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.</summary> + public string Type { get; set; } + + /// <summary>The custom mod data being broadcast.</summary> + public JToken Data { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ModMessageModel() { } + + /// <summary>Construct an instance.</summary> + /// <param name="fromPlayerID">The unique ID of the player who broadcast the message.</param> + /// <param name="fromModID">The unique ID of the mod which broadcast the message.</param> + /// <param name="toPlayerIDs">The players who should receive the message, or <c>null</c> for all players.</param> + /// <param name="toModIDs">The mods which should receive the message, or <c>null</c> for all mods.</param> + /// <param name="type">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="data">The custom mod data being broadcast.</param> + public ModMessageModel(long fromPlayerID, string fromModID, long[] toPlayerIDs, string[] toModIDs, string type, JToken data) + { + this.FromPlayerID = fromPlayerID; + this.FromModID = fromModID; + this.ToPlayerIDs = toPlayerIDs; + this.ToModIDs = toModIDs; + this.Type = type; + this.Data = data; + } + + /// <summary>Construct an instance.</summary> + /// <param name="message">The message to clone.</param> + public ModMessageModel(ModMessageModel message) + { + this.FromPlayerID = message.FromPlayerID; + this.FromModID = message.FromModID; + this.ToPlayerIDs = message.ToPlayerIDs?.ToArray(); + this.ToModIDs = message.ToModIDs?.ToArray(); + this.Type = message.Type; + this.Data = message.Data; + } + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index e97e36bc..c7f8ffad 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.Networking public long PlayerID { get; } /// <summary>Whether this is a connection to the host player.</summary> - public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID; + public bool IsHost { get; } /// <summary>Whether the player has SMAPI installed.</summary> public bool HasSmapi => this.ApiVersion != null; @@ -57,9 +57,11 @@ namespace StardewModdingAPI.Framework.Networking /// <param name="server">The server through which to send messages.</param> /// <param name="serverConnection">The server connection through which to send messages.</param> /// <param name="client">The client through which to send messages.</param> - public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client) + /// <param name="isHost">Whether this is a connection to the host player.</param> + public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client, bool isHost) { this.PlayerID = playerID; + this.IsHost = isHost; if (model != null) { this.Platform = model.Platform; @@ -84,7 +86,8 @@ namespace StardewModdingAPI.Framework.Networking model: model, server: server, serverConnection: serverConnection, - client: null + client: null, + isHost: false ); } @@ -99,7 +102,8 @@ namespace StardewModdingAPI.Framework.Networking model: model, server: null, serverConnection: null, - client: client + client: client, + isHost: true ); } @@ -119,7 +123,7 @@ namespace StardewModdingAPI.Framework.Networking /// <param name="message">The message to send.</param> public void SendMessage(OutgoingMessage message) { - if (this.IsHostPlayer) + if (this.IsHost) this.Client.sendMessage(message); else this.Server.SendMessage(this.ServerConnection, message); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 69b33699..ca343389 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -209,7 +209,7 @@ namespace StardewModdingAPI.Framework // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose, this.Settings.VerboseLogging); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -340,9 +340,6 @@ namespace StardewModdingAPI.Framework /// <summary>Initialise SMAPI and mods after the game starts.</summary> private void InitialiseAfterGameStart() { - // load settings - this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; - // load core components this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6b19f538..c7f5962f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -51,6 +51,12 @@ namespace StardewModdingAPI.Framework /// <summary>Manages SMAPI events for mods.</summary> private readonly EventManager Events; + /// <summary>Tracks the installed mods.</summary> + private readonly ModRegistry ModRegistry; + + /// <summary>Whether SMAPI should log more information about the game context.</summary> + private readonly bool VerboseLogging; + /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary> private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -116,9 +122,6 @@ namespace StardewModdingAPI.Framework /// <summary>The game's core multiplayer utility.</summary> public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; - /// <summary>Whether SMAPI should log more information about the game context.</summary> - public bool VerboseLogging { get; set; } - /// <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> public ConcurrentQueue<string> CommandQueue { get; } = new ConcurrentQueue<string>(); @@ -136,7 +139,8 @@ namespace StardewModdingAPI.Framework /// <param name="modRegistry">Tracks the installed mods.</param> /// <param name="onGameInitialised">A callback to invoke after the game finishes initialising.</param> /// <param name="onGameExiting">A callback to invoke when the game exits.</param> - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting) + /// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param> + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting, bool verboseLogging) { SGame.ConstructorHack = null; @@ -151,11 +155,13 @@ namespace StardewModdingAPI.Framework this.Monitor = monitor; this.MonitorForGame = monitorForGame; this.Events = eventManager; + this.ModRegistry = modRegistry; this.Reflection = reflection; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; + this.VerboseLogging = verboseLogging; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging, this.OnModMessageReceived); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -191,6 +197,15 @@ namespace StardewModdingAPI.Framework this.Events.DayEnding.RaiseEmpty(); } + /// <summary>A callback invoked when a mod message is received.</summary> + /// <param name="message">The message to deliver to applicable mods.</param> + private void OnModMessageReceived(ModMessageModel message) + { + // raise events for applicable mods + HashSet<string> modIDs = new HashSet<string>(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.InvariantCultureIgnoreCase); + this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); + } + /// <summary>Constructor a content manager to read XNB files.</summary> /// <param name="serviceProvider">The service provider to use to locate services.</param> /// <param name="rootDirectory">The root directory to search for content.</param> diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index a151272e..e4f912d2 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using Lidgren.Network; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; @@ -38,6 +40,9 @@ namespace StardewModdingAPI.Framework /// <summary>Whether SMAPI should log more detailed information.</summary> private readonly bool VerboseLogging; + /// <summary>A callback to invoke when a mod message is received.</summary> + private readonly Action<ModMessageModel> OnModMessageReceived; + /********* ** Accessors @@ -45,9 +50,15 @@ namespace StardewModdingAPI.Framework /// <summary>The message ID for a SMAPI message containing context about a player.</summary> public const byte ContextSyncMessageID = 255; + /// <summary>The message ID for a mod message.</summary> + public const byte ModMessageID = 254; + /// <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 @@ -59,7 +70,8 @@ namespace StardewModdingAPI.Framework /// <param name="modRegistry">Tracks the installed mods.</param> /// <param name="reflection">Simplifies access to private code.</param> /// <param name="verboseLogging">Whether SMAPI should log more detailed information.</param> - public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging) + /// <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, bool verboseLogging, Action<ModMessageModel> onModMessageReceived) { this.Monitor = monitor; this.EventManager = eventManager; @@ -67,6 +79,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.VerboseLogging = verboseLogging; + this.OnModMessageReceived = onModMessageReceived; this.DisconnectingFarmers = reflection.GetField<List<long>>(this, "disconnectingFarmers").GetValue(); } @@ -113,7 +126,7 @@ namespace StardewModdingAPI.Framework return server; } - /// <summary>Process an incoming network message from an unknown farmhand.</summary> + /// <summary>Process an incoming network message from an unknown farmhand, usually a player whose connection hasn't been approved yet.</summary> /// <param name="server">The server instance that received the connection.</param> /// <param name="rawMessage">The raw network message that was received.</param> /// <param name="message">The message to process.</param> @@ -123,69 +136,99 @@ namespace StardewModdingAPI.Framework if (!Game1.IsMasterGame) return; - // sync SMAPI context with connected instances - if (message.MessageType == SMultiplayer.ContextSyncMessageID) + switch (message.MessageType) { - // get server - if (!(server is SLidgrenServer customServer)) - { - this.Monitor.Log($"Received context from farmhand {message.FarmerID} via unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); - return; - } - - // parse message - string data = message.Reader.ReadString(); - RemoteContextModel model = this.JsonHelper.Deserialise<RemoteContextModel>(data); - if (model.ApiVersion == null) - model = null; // no data available for unmodded players - - // log info - if (model != null) - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - else - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); - - // store peer - MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection); - - // reply with known contexts - if (this.VerboseLogging) - this.Monitor.Log(" Replying with context for current player...", LogLevel.Trace); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); - foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) - { - if (this.VerboseLogging) - this.Monitor.Log($" Replying with context for player {otherPeer.PlayerID}...", LogLevel.Trace); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); - } + // sync SMAPI context with connected instances + case SMultiplayer.ContextSyncMessageID: + { + // get server + if (!(server is SLidgrenServer customServer)) + { + this.Monitor.Log($"Received context from farmhand {message.FarmerID} via unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); + return; + } + + // parse message + string data = message.Reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise<RemoteContextModel>(data); + if (model.ApiVersion == null) + model = null; // no data available for unmodded players + + // log info + if (model != null) + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + else + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection); + + // reply with known contexts + this.VerboseLog(" Replying with context for current player..."); + newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); + newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, 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.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); + otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + } + } + } + break; - // 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)) + // handle intro from unmodded player + |
