From e5e4ce411cc5a5e5066552978517904b21900066 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 31 Oct 2018 17:29:32 -0400 Subject: sync SMAPI context between players in multiplayer (#480) --- .../Framework/ModHelpers/MultiplayerHelper.cs | 17 ++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 128 +++++++++++ .../Framework/Networking/MultiplayerPeerMod.cs | 30 +++ .../Framework/Networking/RemoteContextModModel.cs | 15 ++ .../Framework/Networking/RemoteContextModel.cs | 24 ++ src/SMAPI/Framework/Networking/SLidgrenClient.cs | 58 +++++ src/SMAPI/Framework/Networking/SLidgrenServer.cs | 36 +++ src/SMAPI/Framework/SCore.cs | 5 +- src/SMAPI/Framework/SGame.cs | 11 +- src/SMAPI/Framework/SMultiplayer.cs | 246 ++++++++++++++++++++- src/SMAPI/IMultiplayerHelper.cs | 11 + src/SMAPI/IMultiplayerPeer.cs | 41 ++++ src/SMAPI/IMultiplayerPeerMod.cs | 15 ++ src/SMAPI/Patches/NetworkingPatch.cs | 103 +++++++++ src/SMAPI/StardewModdingAPI.csproj | 9 + 15 files changed, 741 insertions(+), 8 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/MultiplayerPeer.cs create mode 100644 src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs create mode 100644 src/SMAPI/Framework/Networking/RemoteContextModModel.cs create mode 100644 src/SMAPI/Framework/Networking/RemoteContextModel.cs create mode 100644 src/SMAPI/Framework/Networking/SLidgrenClient.cs create mode 100644 src/SMAPI/Framework/Networking/SLidgrenServer.cs create mode 100644 src/SMAPI/IMultiplayerPeer.cs create mode 100644 src/SMAPI/IMultiplayerPeerMod.cs create mode 100644 src/SMAPI/Patches/NetworkingPatch.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index c449a51b..86f8e012 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using StardewModdingAPI.Framework.Networking; using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers @@ -36,5 +37,21 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.Multiplayer.getNewID(); } + + /// Get a connected player. + /// The player's unique ID. + /// Returns the connected player, or null if no such player is connected. + public IMultiplayerPeer GetConnectedPlayer(long id) + { + return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer) + ? peer + : null; + } + + /// Get all connected players. + public IEnumerable GetConnectedPlayers() + { + return this.Multiplayer.Peers.Values; + } } } diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs new file mode 100644 index 00000000..e97e36bc --- /dev/null +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Lidgren.Network; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about a connected player. + internal class MultiplayerPeer : IMultiplayerPeer + { + /********* + ** Properties + *********/ + /// The server through which to send messages, if this is an incoming farmhand. + private readonly SLidgrenServer Server; + + /// The client through which to send messages, if this is the host player. + private readonly SLidgrenClient Client; + + /// The network connection to the player. + private readonly NetConnection ServerConnection; + + + /********* + ** Accessors + *********/ + /// The player's unique ID. + public long PlayerID { get; } + + /// Whether this is a connection to the host player. + public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID; + + /// Whether the player has SMAPI installed. + public bool HasSmapi => this.ApiVersion != null; + + /// The player's OS platform, if is true. + public GamePlatform? Platform { get; } + + /// The installed version of Stardew Valley, if is true. + public ISemanticVersion GameVersion { get; } + + /// The installed version of SMAPI, if is true. + public ISemanticVersion ApiVersion { get; } + + /// The installed mods, if is true. + public IEnumerable Mods { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's unique ID. + /// The metadata to copy. + /// The server through which to send messages. + /// The server connection through which to send messages. + /// The client through which to send messages. + public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client) + { + this.PlayerID = playerID; + if (model != null) + { + this.Platform = model.Platform; + this.GameVersion = model.GameVersion; + this.ApiVersion = model.ApiVersion; + this.Mods = model.Mods.Select(mod => new MultiplayerPeerMod(mod)).ToArray(); + } + this.Server = server; + this.ServerConnection = serverConnection; + this.Client = client; + } + + /// Construct an instance for a connection to an incoming farmhand. + /// The player's unique ID. + /// The metadata to copy, if available. + /// The server through which to send messages. + /// The server connection through which to send messages. + public static MultiplayerPeer ForConnectionToFarmhand(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection) + { + return new MultiplayerPeer( + playerID: playerID, + model: model, + server: server, + serverConnection: serverConnection, + client: null + ); + } + + /// Construct an instance for a connection to the host player. + /// The player's unique ID. + /// The metadata to copy. + /// The client through which to send messages. + public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client) + { + return new MultiplayerPeer( + playerID: playerID, + model: model, + server: null, + serverConnection: null, + client: client + ); + } + + /// Get metadata for a mod installed by the player. + /// The unique mod ID. + /// Returns the mod info, or null if the player doesn't have that mod. + public IMultiplayerPeerMod GetMod(string id) + { + if (string.IsNullOrWhiteSpace(id)) + return null; + + id = id.Trim(); + return this.Mods.FirstOrDefault(mod => mod.ID != null && mod.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Send a message to the given peer, bypassing the game's normal validation to allow messages before the connection is approved. + /// The message to send. + public void SendMessage(OutgoingMessage message) + { + if (this.IsHostPlayer) + this.Client.sendMessage(message); + else + this.Server.SendMessage(this.ServerConnection, message); + } + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs new file mode 100644 index 00000000..1b324bcd --- /dev/null +++ b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Framework.Networking +{ + internal class MultiplayerPeerMod : IMultiplayerPeerMod + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string Name { get; } + + /// The unique mod ID. + public string ID { get; } + + /// The mod version. + public ISemanticVersion Version { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + public MultiplayerPeerMod(RemoteContextModModel mod) + { + this.Name = mod.Name; + this.ID = mod.ID?.Trim(); + this.Version = mod.Version; + } + } +} diff --git a/src/SMAPI/Framework/Networking/RemoteContextModModel.cs b/src/SMAPI/Framework/Networking/RemoteContextModModel.cs new file mode 100644 index 00000000..9795d971 --- /dev/null +++ b/src/SMAPI/Framework/Networking/RemoteContextModModel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about an installed mod exchanged with connected computers. + public class RemoteContextModModel + { + /// The mod's display name. + public string Name { get; set; } + + /// The unique mod ID. + public string ID { get; set; } + + /// The mod version. + public ISemanticVersion Version { get; set; } + } +} diff --git a/src/SMAPI/Framework/Networking/RemoteContextModel.cs b/src/SMAPI/Framework/Networking/RemoteContextModel.cs new file mode 100644 index 00000000..7befb151 --- /dev/null +++ b/src/SMAPI/Framework/Networking/RemoteContextModel.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about the game, SMAPI, and installed mods exchanged with connected computers. + internal class RemoteContextModel + { + /********* + ** Accessors + *********/ + /// Whether this player is the host player. + public bool IsHost { get; set; } + + /// The game's platform version. + public GamePlatform Platform { get; set; } + + /// The installed version of Stardew Valley. + public ISemanticVersion GameVersion { get; set; } + + /// The installed version of SMAPI. + public ISemanticVersion ApiVersion { get; set; } + + /// The installed mods. + public RemoteContextModModel[] Mods { get; set; } + } +} diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs new file mode 100644 index 00000000..9dfdba15 --- /dev/null +++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs @@ -0,0 +1,58 @@ +using System; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer client used to connect to a hosted server. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SLidgrenClient : LidgrenClient + { + /********* + ** Properties + *********/ + /// Get the metadata to include in a metadata message sent to other players. + private readonly Func GetMetadataMessageFields; + + /// The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed. + private readonly Func TryProcessMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The remote address being connected. + /// Get the metadata to include in a metadata message sent to other players. + /// The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed.. + public SLidgrenClient(string address, Func getMetadataMessageFields, Func tryProcessMessage) + : base(address) + { + this.GetMetadataMessageFields = getMetadataMessageFields; + this.TryProcessMessage = tryProcessMessage; + } + + /// Send the metadata needed to connect with a remote server. + public override void sendPlayerIntroduction() + { + // send custom intro + if (this.getUserID() != "") + Game1.player.userID.Value = this.getUserID(); + this.sendMessage(SMultiplayer.ContextSyncMessageID, this.GetMetadataMessageFields()); + base.sendPlayerIntroduction(); + } + + + /********* + ** Protected methods + *********/ + /// Process an incoming network message. + /// The message to process. + protected override void processIncomingMessage(IncomingMessage message) + { + if (this.TryProcessMessage(this, message)) + return; + + base.processIncomingMessage(message); + } + } +} diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs new file mode 100644 index 00000000..971eb66d --- /dev/null +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Lidgren.Network; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SLidgrenServer : LidgrenServer + { + /********* + ** Properties + *********/ + /// A method which sends a message through a specific connection. + private readonly MethodInfo SendMessageToConnectionMethod; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + public SLidgrenServer(IGameServer gameServer) + : base(gameServer) + { + this.SendMessageToConnectionMethod = typeof(LidgrenServer).GetMethod(nameof(LidgrenServer.sendMessage), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(NetConnection), typeof(OutgoingMessage) }, null); + } + + /// Send a message to a remote server. + /// The network connection. + /// The message to send. + public void SendMessage(NetConnection connection, OutgoingMessage message) + { + this.SendMessageToConnectionMethod.Invoke(this, new object[] { connection, message }); + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a17af91e..d59051fa 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -161,7 +161,8 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new DialoguePatch(this.MonitorForGame, this.Reflection) + new DialogueErrorPatch(this.MonitorForGame, this.Reflection), + new NetworkingPatch() ); } @@ -208,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.InitialiseAfterGameStart, this.Dispose); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 57f48d11..6b19f538 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -15,9 +15,11 @@ using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; @@ -130,9 +132,11 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging on the game's behalf. /// Simplifies access to private game code. /// Manages SMAPI events for mods. + /// Encapsulates SMAPI's JSON file parsing. + /// Tracks the installed mods. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting) + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting) { SGame.ConstructorHack = null; @@ -151,7 +155,7 @@ namespace StardewModdingAPI.Framework this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -181,9 +185,6 @@ namespace StardewModdingAPI.Framework this.OnGameExiting?.Invoke(); } - /**** - ** Intercepted methods & events - ****/ /// A callback invoked before runs. protected void OnNewDayAfterFade() { diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 4923a202..a151272e 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,5 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using Lidgren.Network; +using Newtonsoft.Json; using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; +using StardewValley.Network; namespace StardewModdingAPI.Framework { @@ -12,9 +20,34 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + /// Simplifies access to private code. + private readonly Reflector Reflection; + /// Manages SMAPI events. private readonly EventManager EventManager; + /// The players who are currently disconnecting. + private readonly IList DisconnectingFarmers; + + /// Whether SMAPI should log more detailed information. + private readonly bool VerboseLogging; + + + /********* + ** Accessors + *********/ + /// The message ID for a SMAPI message containing context about a player. + public const byte ContextSyncMessageID = 255; + + /// The metadata for each connected peer. + public IDictionary Peers { get; } = new Dictionary(); + /********* ** Public methods @@ -22,10 +55,20 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging. /// Manages SMAPI events. - public SMultiplayer(IMonitor monitor, EventManager eventManager) + /// Encapsulates SMAPI's JSON file parsing. + /// Tracks the installed mods. + /// Simplifies access to private code. + /// Whether SMAPI should log more detailed information. + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging) { this.Monitor = monitor; this.EventManager = eventManager; + this.JsonHelper = jsonHelper; + this.ModRegistry = modRegistry; + this.Reflection = reflection; + this.VerboseLogging = verboseLogging; + + this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); } /// Handle sync messages from other players and perform other initial sync logic. @@ -43,5 +86,206 @@ namespace StardewModdingAPI.Framework base.UpdateLate(forceSync); this.EventManager.Legacy_AfterMainBroadcast.Raise(); } + + /// Initialise a client before the game connects to a remote server. + /// The client to initialise. + public override Client InitClient(Client client) + { + if (client is LidgrenClient) + { + string address = this.Reflection.GetField(client, "address").GetValue(); + return new SLidgrenClient(address, this.GetContextSyncMessageFields, this.TryProcessMessageFromServer); + } + + return client; + } + + /// Initialise a server before the game connects to an incoming player. + /// The server to initialise. + public override Server InitServer(Server server) + { + if (server is LidgrenServer) + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SLidgrenServer(gameServer); + } + + return server; + } + + /// Process an incoming network message from an unknown farmhand. + /// The server instance that received the connection. + /// The raw network message that was received. + /// The message to process. + public void ProcessMessageFromUnknownFarmhand(Server server, NetIncomingMessage rawMessage, IncomingMessage message) + { + // ignore invalid message (farmhands should only receive messages from the server) + if (!Game1.IsMasterGame) + return; + + // sync SMAPI context with connected instances + if (message.MessageType == 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(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))); + } + + // 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)) + { + if (this.VerboseLogging) + this.Monitor.Log($" Forwarding context to player {otherPeer.PlayerID}...", LogLevel.Trace); + otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + } + } + } + + // handle intro from unmodded player + else if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) + { + // get server + if (!(server is SLidgrenServer customServer)) + { + this.Monitor.Log($"Received connection from farmhand {message.FarmerID} with unknown client {server.GetType().FullName}. Mods will not be able to sync data to that player.", LogLevel.Warn); + return; + } + + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); + } + } + + /// Process an incoming network message from the server. + /// The client instance that received the connection. + /// The message to process. + /// Returns whether the message was handled. + public bool TryProcessMessageFromServer(SLidgrenClient client, IncomingMessage message) + { + // receive SMAPI context from a connected player + if (message.MessageType == SMultiplayer.ContextSyncMessageID) + { + // parse message + string data = message.Reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + + // log info + if (model != null) + this.Monitor.Log($"Received context for {(model.IsHost ? "host" : "farmhand")} {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + else + this.Monitor.Log($"Received context for player {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace); + + // store peer + this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); + return true; + } + + // handle intro from unmodded player + if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) + { + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); + } + + return false; + } + + /// Remove players who are disconnecting. + protected override void removeDisconnectedFarmers() + { + foreach (long playerID in this.DisconnectingFarmers) + { + this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); + this.Peers.Remove(playerID); + } + + base.removeDisconnectedFarmers(); + } + + + /********* + ** Private methods + *********/ + /// Get the fields to include in a context sync message sent to other players. + 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) }; + } + + /// Get the fields to include in a context sync message sent to other players. + /// The peer whose data to represent. + private object[] GetContextSyncMessageFields(IMultiplayerPeer peer) + { + if (!peer.HasSmapi) + return new object[] { "{}" }; + + RemoteContextModel model = new RemoteContextModel + { + IsHost = peer.IsHostPlayer, + 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) }; + } } } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs index 43a0ac95..b01a7bed 100644 --- a/src/SMAPI/IMultiplayerHelper.cs +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -11,5 +11,16 @@ namespace StardewModdingAPI /// Get the locations which are being actively synced from the host. IEnumerable GetActiveLocations(); + + /* disable until ready for release: + + /// Get a connected player. + /// The player's unique ID. + /// Returns the connected player, or null if no such player is connected. + IMultiplayerPeer GetConnectedPlayer(long id); + + /// Get all connected players. + IEnumerable GetConnectedPlayers(); + */ } } diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs new file mode 100644 index 00000000..e314eba5 --- /dev/null +++ b/src/SMAPI/IMultiplayerPeer.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Metadata about a connected player. + public interface IMultiplayerPeer + { + /********* + ** Accessors + *********/ + /// The player's unique ID. + long PlayerID { get; } + + /// Whether this is a connection to the host player. + bool IsHostPlayer { get; } + + /// Whether the player has SMAPI installed. + bool HasSmapi { get; } + + /// The player's OS platform, if is true. + GamePlatform? Platform { get; } + + /// The installed version of Stardew Valley, if is true. + ISemanticVersion GameVersion { get; } + + /// The installed version of SMAPI, if is true. + ISemanticVersion ApiVersion { get; } + + /// The installed mods, if is true. + IEnumerable Mods { get; } + + + /********* + ** Methods + *********/ + /// Get metadata for a mod installed by the player. + /// The unique mod ID. + /// Returns the mod info, or null if the player doesn't have that mod. + IMultiplayerPeerMod GetMod(string id); + } +} diff --git a/src/SMAPI/IMultiplayerPeerMod.cs b/src/SMAPI/IMultiplayerPeerMod.cs new file mode 100644 index 00000000..005408b1 --- /dev/null +++ b/src/SMAPI/IMultiplayerPeerMod.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI +{ + /// Metadata about a mod installed by a connected player. + public interface IMultiplayerPeerMod + { + /// The mod's display name. + string Name { get; } + + /// The unique mod ID. + string ID { get; } + + /// The mod version. + ISemanticVersion Version { get; } + } +} diff --git a/src/SMAPI/Patches/NetworkingPatch.cs b/src/SMAPI/Patches/NetworkingPatch.cs new file mode 100644 index 00000000..12ccf84c --- /dev/null +++ b/src/SMAPI/Patches/NetworkingPatch.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using Harmony; +using Lidgren.Network; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using StardewValley.Network; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch to enable the SMAPI multiplayer metadata handshake. + internal class NetworkingPatch : IHarmonyPatch + { + /********* + ** Properties + *********/ + /// The constructor for the internal NetBufferReadStream type. + private static readonly ConstructorInfo NetBufferReadStreamConstructor = NetworkingPatch.GetNetBufferReadStreamConstructor(); + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(NetworkingPatch)}"; + + + /********* + ** Public methods + *********/ + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(NetworkingPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the method. + /// The instance being patched. + /// The raw network message to parse. + /// The private peers field on the instance. + /// The private gameServer field on the instance. + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Prefix_LidgrenServer_ParseDataMessageFromClient(LidgrenServer __instance, NetIncomingMessage dataMsg, Bimap ___peers, IGameServer ___gameServer) + { + // get SMAPI overrides + SMultiplayer multiplayer = ((SGame)Game1.game1).Multiplayer; + SLidgrenServer server = (SLidgrenServer)__instance; + + // add hook to call multiplayer core + NetConnection peer = dataMsg.SenderConnection; + using (IncomingMessage message = new IncomingMessage()) + using (Stream readStream = (Stream)NetworkingPatch.NetBufferReadStreamConstructor.Invoke(new object[] { dataMsg })) + using (BinaryReader reader = new BinaryReader(readStream)) + { + while (dataMsg.LengthBits - dataMsg.Position >= 8) + { + message.Read(reader); + if (___peers.ContainsLeft(message.FarmerID) && ___peers[message.FarmerID] == peer) + ___gameServer.processIncomingMessage(message); + else if (message.MessageType == Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); + ___gameServer.checkFarmhandRequest("", farmer, msg => server.SendMessage(peer, msg), () => ___peers[farmer.Value.UniqueMultiplayerID] = peer); + } + else + multiplayer.ProcessMessageFromUnknownFarmhand(__instance, dataMsg, message); // added hook + } + } + + return false; + } + + /// Get the constructor for the internal NetBufferReadStream type. + private static ConstructorInfo GetNetBufferReadStreamConstructor() + { + // get type + string typeName = $"StardewValley.Network.NetBufferReadStream, {Constants.GameAssemblyName}"; + Type type = Type.GetType(typeName); + if (type == null) + throw new InvalidOperationException($"Can't find type: {typeName}"); + + // get constructor + ConstructorInfo constructor = type.GetConstructor(new[] { typeof(NetBuffer) }); + if (constructor == null) + throw new InvalidOperationException($"Can't find constructor for type: {typeName}"); + + return constructor; + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 4ce0892e..2fdf4d97 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -164,6 +164,12 @@ + + + + + + @@ -243,9 +249,11 @@ + + @@ -310,6 +318,7 @@ + -- cgit From 17551bae65c6970f71d22d74e73b60fd81967130 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 31 Oct 2018 17:29:54 -0400 Subject: disable versions of MTN which remove SMAPI's SMultiplayer subclass (#480) --- src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json index 541dcd91..6f8d9c40 100644 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -245,7 +245,12 @@ "Move Faster": { "ID": "shuaiz.MoveFasterMod", - "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) + "~1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) + }, + + "MTN": { + "ID": "SgtPickles.MTN", + "~1.2.5 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API. }, "Multiple Sprites and Portraits On Rotation (File Loading)": { -- cgit From 6f23aaf2954f7eeb93b7cc5aad2d3f9b237883f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 2 Nov 2018 23:55:35 -0400 Subject: add internal mod.HasID helper (#480) --- src/SMAPI/Framework/IModMetadata.cs | 4 ++++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 9 +++++++++ src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/ModRegistry.cs | 2 +- src/SMAPI/Framework/SCore.cs | 6 +++--- 5 files changed, 18 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index bda9429f..7ada7dea 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -88,6 +88,10 @@ namespace StardewModdingAPI.Framework /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). bool HasID(); + /// Whether the mod has the given ID. + /// The mod ID to check. + bool HasID(string id); + /// Get the defined update keys. /// Only return valid update keys. IEnumerable GetUpdateKeys(bool validOnly = true); diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 04aa679b..0cb62a75 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -153,6 +153,15 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } + /// Whether the mod has the given ID. + /// The mod ID to check. + public bool HasID(string id) + { + return + this.HasID() + && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.InvariantCultureIgnoreCase); + } + /// Get the defined update keys. /// Only return valid update keys. public IEnumerable GetUpdateKeys(bool validOnly = false) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9992cc78..3ff70d64 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -379,7 +379,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The loaded mods. private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) { - IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id)); // yield dependencies if (manifest.Dependencies != null) diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index e7d4f89a..da68fce3 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + return this.GetAll().FirstOrDefault(p => p.HasID(uniqueID)); } /// Get the mod metadata from one of its assemblies. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d59051fa..69b33699 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -750,7 +750,7 @@ namespace StardewModdingAPI.Framework // log loaded content packs if (loadedContentPacks.Any()) { - string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName; this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) @@ -907,7 +907,7 @@ namespace StardewModdingAPI.Framework if (this.ModRegistry.Get(dependency.UniqueID) == null) { string dependencyName = mods - .FirstOrDefault(otherMod => otherMod.HasID() && dependency.UniqueID.Equals(otherMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID)) ?.DisplayName ?? dependency.UniqueID; errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; return false; @@ -971,7 +971,7 @@ namespace StardewModdingAPI.Framework // get content packs IContentPack[] contentPacks = this.ModRegistry .GetAll(assemblyMods: false) - .Where(p => p.IsContentPack && mod.Manifest.UniqueID.Equals(p.Manifest.ContentPackFor.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID)) .Select(p => p.ContentPack) .ToArray(); -- cgit From 02a46bf13f29ce0dd8ac2f422113083c59dae42d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 3 Nov 2018 01:29:01 -0400 Subject: add APIs to send/receive messages in multiplayer (#480) --- src/SMAPI/Events/IModEvents.cs | 3 + src/SMAPI/Events/IMultiplayerEvents.cs | 11 + src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 46 +++ src/SMAPI/Framework/Events/EventManager.cs | 8 + src/SMAPI/Framework/Events/ManagedEvent.cs | 24 ++ src/SMAPI/Framework/Events/ManagedEventBase.cs | 12 +- src/SMAPI/Framework/Events/ModEvents.cs | 4 + src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 29 ++ .../Framework/ModHelpers/MultiplayerHelper.cs | 31 +- src/SMAPI/Framework/Networking/ModMessageModel.cs | 72 +++++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 14 +- src/SMAPI/Framework/SCore.cs | 5 +- src/SMAPI/Framework/SGame.cs | 25 +- src/SMAPI/Framework/SMultiplayer.cs | 339 ++++++++++++++++----- src/SMAPI/IMultiplayerHelper.cs | 13 +- src/SMAPI/IMultiplayerPeer.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 22 +- 17 files changed, 544 insertions(+), 116 deletions(-) create mode 100644 src/SMAPI/Events/IMultiplayerEvents.cs create mode 100644 src/SMAPI/Events/ModMessageReceivedEventArgs.cs create mode 100644 src/SMAPI/Framework/Events/ModMultiplayerEvents.cs create mode 100644 src/SMAPI/Framework/Networking/ModMessageModel.cs (limited to 'src') 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 /// Events raised when the player provides input using a controller, keyboard, or mouse. IInputEvents Input { get; } + /// Events raised for multiplayer messages and connections. + IMultiplayerEvents Multiplayer { get; } + /// Events raised when the player data changes. 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 +{ + /// Events raised for multiplayer messages and connections. + public interface IMultiplayerEvents + { + /// Raised after a mod message is received over the network. + event EventHandler 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 +{ + /// Event arguments when a mod receives a message over the network. + public class ModMessageReceivedEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The underlying message model. + private readonly ModMessageModel Message; + + + /********* + ** Accessors + *********/ + /// The unique ID of the player from whose computer the message was sent. + public long FromPlayerID => this.Message.FromPlayerID; + + /// The unique ID of the mod which sent the message. + public string FromModID => this.Message.FromModID; + + /// A message type which can be used to decide whether it's the one you want to handle, like SetPlayerLocation. This doesn't need to be globally unique, so mods should check the . + public string Type => this.Message.Type; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The received message. + internal ModMessageReceivedEventArgs(ModMessageModel message) + { + this.Message = message; + } + + /// Read the message data into the given model type. + /// The message model type. + public TModel ReadAs() + { + return this.Message.Data.ToObject(); + } + } +} 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 @@ -98,6 +98,12 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player scrolls the mouse wheel. public readonly ManagedEvent MouseWheelScrolled; + /**** + ** Multiplayer + ****/ + /// Raised after a mod message is received over the network. + public readonly ManagedEvent ModMessageReceived; + /**** ** Player ****/ @@ -374,6 +380,8 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); this.Warped = ManageEventOf(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 } } } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + /// A lambda which returns true if the event should be raised for the given mod. + public void RaiseForMods(TEventArgs args, Func match) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + if (match(this.GetSourceMod(handler))) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } } /// An event wrapper which intercepts and logs errors in handler code. 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); } + /// Get the mod which registered the given event handler, if available. + /// The event handler. + protected IModMetadata GetSourceMod(TEventHandler handler) + { + return this.SourceMods.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; + } + /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. 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 /// Events raised when the player provides input using a controller, keyboard, or mouse. public IInputEvents Input { get; } + /// Events raised for multiplayer messages and connections. + public IMultiplayerEvents Multiplayer { get; } + /// Events raised when the player data changes. 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 +{ + /// Events raised for multiplayer messages and connections. + internal class ModMultiplayerEvents : ModEventsBase, IMultiplayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after a mod message is received over the network. + public event EventHandler ModMessageReceived + { + add => this.EventManager.ModMessageReceived.Add(value); + remove => this.EventManager.ModMessageReceived.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + 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; } - /// Get the locations which are being actively synced from the host. - public IEnumerable GetActiveLocations() - { - return this.Multiplayer.activeLocations(); - } - /// Get a new multiplayer ID. public long GetNewID() { return this.Multiplayer.getNewID(); } + /// Get the locations which are being actively synced from the host. + public IEnumerable GetActiveLocations() + { + return this.Multiplayer.activeLocations(); + } + /// Get a connected player. /// The player's unique ID. /// Returns the connected player, or null if no such player is connected. @@ -53,5 +54,23 @@ namespace StardewModdingAPI.Framework.ModHelpers