From 63fb4dbe8ae4d611c4854f863b9b29265e02fdee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 5 Oct 2018 21:59:57 -0400 Subject: tweak new event naming convention (#310) --- src/SMAPI/Framework/SMultiplayer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 687b1922..4923a202 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -31,17 +31,17 @@ namespace StardewModdingAPI.Framework /// Handle sync messages from other players and perform other initial sync logic. public override void UpdateEarly() { - this.EventManager.Multiplayer_BeforeMainSync.Raise(); + this.EventManager.Legacy_BeforeMainSync.Raise(); base.UpdateEarly(); - this.EventManager.Multiplayer_AfterMainSync.Raise(); + this.EventManager.Legacy_AfterMainSync.Raise(); } /// Broadcast sync messages to other players and perform other final sync logic. public override void UpdateLate(bool forceSync = false) { - this.EventManager.Multiplayer_BeforeMainBroadcast.Raise(); + this.EventManager.Legacy_BeforeMainBroadcast.Raise(); base.UpdateLate(forceSync); - this.EventManager.Multiplayer_AfterMainBroadcast.Raise(); + this.EventManager.Legacy_AfterMainBroadcast.Raise(); } } } -- cgit 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) --- build/common.targets | 8 + .../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 + 16 files changed, 749 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/SMAPI/Framework/SMultiplayer.cs') diff --git a/build/common.targets b/build/common.targets index b5cbbe67..e646e62c 100644 --- a/build/common.targets +++ b/build/common.targets @@ -56,6 +56,14 @@ + + $(GamePath)\GalaxyCSharp.dll + False + + + $(GamePath)\Lidgren.Network.dll + False + $(GamePath)\Netcode.dll False 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 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) --- docs/release-notes.md | 1 + 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 +- 18 files changed, 545 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/SMAPI/Framework/SMultiplayer.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index cb83e7ec..2111e35e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -31,6 +31,7 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). + * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. * Added more events to the prototype `helper.Events` for SMAPI 3.0. 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 { return this.Multiplayer.Peers.Values; } + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + public void SendMessage(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 +{ + /// The metadata for a mod message. + internal class ModMessageModel + { + /********* + ** Accessors + *********/ + /**** + ** Origin + ****/ + /// The unique ID of the player who broadcast the message. + public long FromPlayerID { get; set; } + + /// The unique ID of the mod which broadcast the message. + public string FromModID { get; set; } + + /**** + ** Destination + ****/ + /// The players who should receive the message, or null for all players. + public long[] ToPlayerIDs { get; set; } + + /// The mods which should receive the message, or null for all mods. + public string[] ToModIDs { get; set; } + + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + public string Type { get; set; } + + /// The custom mod data being broadcast. + public JToken Data { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModMessageModel() { } + + /// Construct an instance. + /// The unique ID of the player who broadcast the message. + /// The unique ID of the mod which broadcast the message. + /// The players who should receive the message, or null for all players. + /// The mods which should receive the message, or null for all mods. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The custom mod data being broadcast. + 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; + } + + /// Construct an instance. + /// The message to clone. + 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; } /// Whether this is a connection to the host player. - public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID; + public bool IsHost { get; } /// Whether the player has SMAPI installed. public bool HasSmapi => this.ApiVersion != null; @@ -57,9 +57,11 @@ namespace StardewModdingAPI.Framework.Networking /// 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) + /// Whether this is a connection to the host player. + 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 /// The message to send. 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 /// Initialise SMAPI and mods after the game starts. 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 /// Manages SMAPI events for mods. private readonly EventManager Events; + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Whether SMAPI should log more information about the game context. + private readonly bool VerboseLogging; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -116,9 +122,6 @@ namespace StardewModdingAPI.Framework /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; - /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } - /// A list of queued commands to execute. /// This property must be threadsafe, since it's accessed from a separate console input thread. public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); @@ -136,7 +139,8 @@ namespace StardewModdingAPI.Framework /// 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, JsonHelper jsonHelper, ModRegistry modRegistry, Action onGameInitialised, Action onGameExiting) + /// Whether SMAPI should log more information about the game context. + 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(); } + /// A callback invoked when a mod message is received. + /// The message to deliver to applicable mods. + private void OnModMessageReceived(ModMessageModel message) + { + // raise events for applicable mods + HashSet modIDs = new HashSet(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)); + } + /// Constructor a content manager to read XNB files. /// The service provider to use to locate services. /// The root directory to search for content. 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 /// Whether SMAPI should log more detailed information. private readonly bool VerboseLogging; + /// A callback to invoke when a mod message is received. + private readonly Action OnModMessageReceived; + /********* ** Accessors @@ -45,9 +50,15 @@ namespace StardewModdingAPI.Framework /// The message ID for a SMAPI message containing context about a player. public const byte ContextSyncMessageID = 255; + /// The message ID for a mod message. + public const byte ModMessageID = 254; + /// The metadata for each connected peer. public IDictionary Peers { get; } = new Dictionary(); + /// The metadata for the host player, if the current player is a farmhand. + public MultiplayerPeer HostPeer; + /********* ** Public methods @@ -59,7 +70,8 @@ namespace StardewModdingAPI.Framework /// 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) + /// A callback to invoke when a mod message is received. + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging, Action 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>(this, "disconnectingFarmers").GetValue(); } @@ -113,7 +126,7 @@ namespace StardewModdingAPI.Framework return server; } - /// Process an incoming network message from an unknown farmhand. + /// Process an incoming network message from an unknown farmhand, usually a player whose connection hasn't been approved yet. /// The server instance that received the connection. /// The raw network message that was received. /// The message to process. @@ -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(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(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 + case Multiplayer.playerIntroduction: + if (!this.Peers.ContainsKey(message.FarmerID)) { - if (this.VerboseLogging) - this.Monitor.Log($" Forwarding context to player {otherPeer.PlayerID}...", LogLevel.Trace); - otherPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, newPeer.PlayerID, fields)); + // 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); + var peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; } - } + break; + + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + break; } + } - // handle intro from unmodded player - else if (message.MessageType == Multiplayer.playerIntroduction && !this.Peers.ContainsKey(message.FarmerID)) + /// Process an incoming message from an approved connection. + /// The message to process. + public override void processIncomingMessage(IncomingMessage message) + { + switch (message.MessageType) { - // 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); + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + break; + + // let game process message + default: + base.processIncomingMessage(message); + break; } + } /// Process an incoming network message from the server. @@ -194,33 +237,50 @@ namespace StardewModdingAPI.Framework /// 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) + switch (message.MessageType) { - // 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; - } + // receive SMAPI context from a connected player + case 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 + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } + 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); - } + // handle intro from unmodded player + case Multiplayer.playerIntroduction: + if (!this.Peers.ContainsKey(message.FarmerID)) + { + // store peer + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + var peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } + return false; + + // handle mod message + case SMultiplayer.ModMessageID: + this.ReceiveModMessage(message); + return true; - return false; + default: + return false; + } } /// Remove players who are disconnecting. @@ -235,10 +295,117 @@ namespace StardewModdingAPI.Framework base.removeDisconnectedFarmers(); } + /// Broadcast a mod message to matching players. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The unique ID of the mod sending the message. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + public void BroadcastModMessage(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.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); + return; + } + + // filter player IDs + HashSet playerIDs = null; + if (toPlayerIDs != null && toPlayerIDs.Any()) + { + playerIDs = new HashSet(toPlayerIDs); + playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); + if (!playerIDs.Any()) + { + this.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.VerboseLogging) + 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(SMultiplayer.ModMessageID, peer.PlayerID, data)); + } + } + } + else if (this.HostPeer != null && this.HostPeer.HasSmapi) + this.HostPeer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, this.HostPeer.PlayerID, data)); + else + this.VerboseLog(" Can't send message because no valid connections were found."); + + } + /********* ** Private methods *********/ + /// Receive a mod message sent from another player's mods. + /// The raw message to parse. + private void ReceiveModMessage(IncomingMessage message) + { + // parse message + string json = message.Reader.ReadString(); + ModMessageModel model = this.JsonHelper.Deserialise(json); + HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); + if (this.VerboseLogging) + this.Monitor.Log($"Received message: {json}."); + + // 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.VerboseLog($" Forwarding message to player {peer.PlayerID}."); + peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); + } + } + } + } + + /// Get all connected player IDs, including the current player. + private IEnumerable GetKnownPlayerIDs() + { + yield return Game1.player.UniqueMultiplayerID; + foreach (long peerID in this.Peers.Keys) + yield return peerID; + } + /// Get the fields to include in a context sync message sent to other players. private object[] GetContextSyncMessageFields() { @@ -271,7 +438,7 @@ namespace StardewModdingAPI.Framework RemoteContextModel model = new RemoteContextModel { - IsHost = peer.IsHostPlayer, + IsHost = peer.IsHost, Platform = peer.Platform.Value, ApiVersion = peer.ApiVersion, GameVersion = peer.GameVersion, @@ -287,5 +454,13 @@ namespace StardewModdingAPI.Framework return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; } + + /// Log a trace message if is enabled. + /// The message to log. + private void VerboseLog(string message) + { + if (this.VerboseLogging) + this.Monitor.Log(message, LogLevel.Trace); + } } } diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs index b01a7bed..4067a676 100644 --- a/src/SMAPI/IMultiplayerHelper.cs +++ b/src/SMAPI/IMultiplayerHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewValley; @@ -12,8 +13,6 @@ 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. @@ -21,6 +20,14 @@ namespace StardewModdingAPI /// Get all connected players. IEnumerable GetConnectedPlayers(); - */ + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null); } } diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs index e314eba5..0d4d3261 100644 --- a/src/SMAPI/IMultiplayerPeer.cs +++ b/src/SMAPI/IMultiplayerPeer.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI long PlayerID { get; } /// Whether this is a connection to the host player. - bool IsHostPlayer { get; } + bool IsHost { get; } /// Whether the player has SMAPI installed. bool HasSmapi { get; } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 2fdf4d97..11fa5d35 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -116,6 +116,7 @@ + @@ -130,6 +131,7 @@ + @@ -160,10 +162,20 @@ - + + + + + + + + + + + @@ -183,15 +195,8 @@ - - - - - - - @@ -224,7 +229,6 @@ - -- cgit From bfb40202793f2f7f2c9c73272f01a477b23edfa2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 21:34:48 -0500 Subject: rewrite multiplayer sync to use generic callbacks from client/server for better extensibility (#480) --- src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 2 +- src/SMAPI/Framework/Networking/MessageType.cs | 26 +++ src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 6 +- src/SMAPI/Framework/Networking/SLidgrenClient.cs | 37 ++-- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 122 +++++++++++- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SMultiplayer.cs | 226 +++++++++++++--------- src/SMAPI/Patches/LidgrenServerPatch.cs | 89 +++++++++ src/SMAPI/Patches/NetworkingPatch.cs | 103 ---------- src/SMAPI/StardewModdingAPI.csproj | 3 +- 10 files changed, 383 insertions(+), 233 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/MessageType.cs create mode 100644 src/SMAPI/Patches/LidgrenServerPatch.cs delete mode 100644 src/SMAPI/Patches/NetworkingPatch.cs (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs index b1960a22..49366ec6 100644 --- a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs +++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Framework.Networking; namespace StardewModdingAPI.Events { - /// Event arguments when a mod receives a message over the network. + /// Event arguments for an event. public class ModMessageReceivedEventArgs : EventArgs { /********* diff --git a/src/SMAPI/Framework/Networking/MessageType.cs b/src/SMAPI/Framework/Networking/MessageType.cs new file mode 100644 index 00000000..bd9acfa9 --- /dev/null +++ b/src/SMAPI/Framework/Networking/MessageType.cs @@ -0,0 +1,26 @@ +using StardewValley; + +namespace StardewModdingAPI.Framework.Networking +{ + /// Network message types recognised by SMAPI and Stardew Valley. + internal enum MessageType : byte + { + /********* + ** SMAPI + *********/ + /// A data message intended for mods to consume. + ModMessage = 254, + + /// Metadata context about a player synced by SMAPI. + ModContext = 255, + + /********* + ** Vanilla + *********/ + /// Metadata about the host server sent to a farmhand. + ServerIntroduction = Multiplayer.serverIntroduction, + + /// Metadata about a player sent to a farmhand or server. + PlayerIntroduction = Multiplayer.playerIntroduction + } +} diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index c7f8ffad..e703dbb1 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Lidgren.Network; -using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -95,7 +94,8 @@ namespace StardewModdingAPI.Framework.Networking /// 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) + /// Whether this connection is for the host player. + public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client, bool isHost) { return new MultiplayerPeer( playerID: playerID, @@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework.Networking server: null, serverConnection: null, client: client, - isHost: true + isHost: isHost ); } diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs index 9dfdba15..c05e6b76 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenClient.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs @@ -1,44 +1,38 @@ 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. + /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. 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; + /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. + private readonly Action OnProcessingMessage; + /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. + private readonly Action OnSendingMessage; /********* ** 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) + /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. + public SLidgrenClient(string address, Action onProcessingMessage, Action onSendingMessage) : base(address) { - this.GetMetadataMessageFields = getMetadataMessageFields; - this.TryProcessMessage = tryProcessMessage; + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; } - /// Send the metadata needed to connect with a remote server. - public override void sendPlayerIntroduction() + /// Send a message to the connected peer. + public override void sendMessage(OutgoingMessage message) { - // send custom intro - if (this.getUserID() != "") - Game1.player.userID.Value = this.getUserID(); - this.sendMessage(SMultiplayer.ContextSyncMessageID, this.GetMetadataMessageFields()); - base.sendPlayerIntroduction(); + this.OnSendingMessage(this, message, () => base.sendMessage(message)); } @@ -49,10 +43,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to process. protected override void processIncomingMessage(IncomingMessage message) { - if (this.TryProcessMessage(this, message)) - return; - - base.processIncomingMessage(message); + this.OnProcessingMessage(this, message, () => base.processIncomingMessage(message)); } } } diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 971eb66d..060b433b 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -1,5 +1,11 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Reflection; using Lidgren.Network; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Patches; +using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -10,8 +16,27 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - /// A method which sends a message through a specific connection. - private readonly MethodInfo SendMessageToConnectionMethod; + + /// The constructor for the internal NetBufferReadStream type. + private readonly ConstructorInfo NetBufferReadStreamConstructor = SLidgrenServer.GetNetBufferReadStreamConstructor(); + + /// The constructor for the internal NetBufferWriteStream type. + private readonly ConstructorInfo NetBufferWriteStreamConstructor = SLidgrenServer.GetNetBufferWriteStreamConstructor(); + + /// A method which reads farmer data from the given binary reader. + private readonly Func ReadFarmer; + + /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. + private readonly Action OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, target player ID, and a callback to run the default logic. + private readonly Action OnSendingMessage; + + /// The peer connections. + private readonly Bimap Peers; + + /// The underlying net server. + private readonly IReflectedField Server; /********* @@ -19,18 +44,105 @@ namespace StardewModdingAPI.Framework.Networking *********/ /// Construct an instance. /// The underlying game server. - public SLidgrenServer(IGameServer gameServer) + /// Simplifies access to private code. + /// A method which reads farmer data from the given binary reader. + /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, and a callback to run the default logic. + public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action onProcessingMessage, Action onSendingMessage) : base(gameServer) { - this.SendMessageToConnectionMethod = typeof(LidgrenServer).GetMethod(nameof(LidgrenServer.sendMessage), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(NetConnection), typeof(OutgoingMessage) }, null); + this.ReadFarmer = readFarmer; + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; + this.Peers = reflection.GetField>(this, "peers").GetValue(); + this.Server = reflection.GetField(this, "server"); } /// Send a message to a remote server. /// The network connection. /// The message to send. + /// This is an implementation of which calls . This method is invoked via . public void SendMessage(NetConnection connection, OutgoingMessage message) { - this.SendMessageToConnectionMethod.Invoke(this, new object[] { connection, message }); + this.OnSendingMessage(this, connection, message, () => + { + NetServer server = this.Server.GetValue(); + NetOutgoingMessage netMessage = server.CreateMessage(); + using (Stream bufferWriteStream = (Stream)this.NetBufferWriteStreamConstructor.Invoke(new object[] { netMessage })) + using (BinaryWriter writer = new BinaryWriter(bufferWriteStream)) + message.Write(writer); + + server.SendMessage(netMessage, connection, NetDeliveryMethod.ReliableOrdered); + }); + } + + /// Parse a data message from a client. + /// The raw network message to parse. + /// This is an implementation of which calls . This method is invoked via . + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] + public bool ParseDataMessageFromClient(NetIncomingMessage rawMessage) + { + // add hook to call multiplayer core + NetConnection peer = rawMessage.SenderConnection; + using (IncomingMessage message = new IncomingMessage()) + using (Stream readStream = (Stream)this.NetBufferReadStreamConstructor.Invoke(new object[] { rawMessage })) + using (BinaryReader reader = new BinaryReader(readStream)) + { + while (rawMessage.LengthBits - rawMessage.Position >= 8) + { + message.Read(reader); + this.OnProcessingMessage(this, rawMessage, message, () => + { + if (this.Peers.ContainsLeft(message.FarmerID) && this.Peers[message.FarmerID] == peer) + this.gameServer.processIncomingMessage(message); + else if (message.MessageType == Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = this.ReadFarmer(message.Reader); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.SendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); + } + }); + } + } + + return false; + } + + + /********* + ** Private methods + *********/ + /// 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; + } + + /// Get the constructor for the internal NetBufferWriteStream type. + private static ConstructorInfo GetNetBufferWriteStreamConstructor() + { + // get type + string typeName = $"StardewValley.Network.NetBufferWriteStream, {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/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ca343389..f078acba 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -162,7 +162,7 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new NetworkingPatch() + new LidgrenServerPatch() ); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index e4f912d2..70f1a89a 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -14,6 +14,17 @@ using StardewValley.Network; namespace StardewModdingAPI.Framework { /// SMAPI's implementation of the game's core multiplayer logic. + /// + /// 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. + /// internal class SMultiplayer : Multiplayer { /********* @@ -47,12 +58,6 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// The message ID for a SMAPI message containing context about a player. - public const byte ContextSyncMessageID = 255; - - /// The message ID for a mod message. - public const byte ModMessageID = 254; - /// The metadata for each connected peer. public IDictionary Peers { get; } = new Dictionary(); @@ -107,7 +112,7 @@ namespace StardewModdingAPI.Framework if (client is LidgrenClient) { string address = this.Reflection.GetField(client, "address").GetValue(); - return new SLidgrenClient(address, this.GetContextSyncMessageFields, this.TryProcessMessageFromServer); + return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); } return client; @@ -120,56 +125,78 @@ namespace StardewModdingAPI.Framework if (server is LidgrenServer) { IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SLidgrenServer(gameServer); + return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage, this.OnServerSendingMessage); } return server; } - /// Process an incoming network message from an unknown farmhand, usually a player whose connection hasn't been approved yet. + /// A callback raised when sending a network message as the host player. + /// The server sending the message. + /// The connection to which a message is being sent. + /// The message being sent. + /// Send the underlying message. + protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) + { + resume(); + } + + /// A callback raised when sending a message as a farmhand. + /// The client sending the message. + /// The message being sent. + /// Send the underlying message. + protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) + { + switch (message.MessageType) + { + // sync mod context (step 1) + case (byte)MessageType.PlayerIntroduction: + client.sendMessage((byte)MessageType.ModContext, this.GetContextSyncMessageFields()); + resume(); + break; + + // run default logic + default: + resume(); + break; + } + } + + /// Process an incoming network message as the host player. /// 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) + /// Process the message using the game's default logic. + public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) { - // ignore invalid message (farmhands should only receive messages from the server) - if (!Game1.IsMasterGame) - return; switch (message.MessageType) { - // sync SMAPI context with connected instances - case SMultiplayer.ContextSyncMessageID: + // sync mod context (step 2) + case (byte)MessageType.ModContext: { - // get server - if (!(server is SLidgrenServer customServer)) + // 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 = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, server, rawMessage.SenderConnection); + if (this.Peers.ContainsKey(message.FarmerID)) { - 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); + this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); return; } + this.Peers[message.FarmerID] = newPeer; - // 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 own context + this.VerboseLog(" Replying with host context..."); + newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); - // reply with known contexts - this.VerboseLog(" Replying with context for current player..."); - newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); + // reply with other players' context 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))); + newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); } // forward to other peers @@ -179,107 +206,103 @@ namespace StardewModdingAPI.Framework 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)); + otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } } break; - // handle intro from unmodded player - case Multiplayer.playerIntroduction: - if (!this.Peers.ContainsKey(message.FarmerID)) + // handle player intro + case (byte)MessageType.PlayerIntroduction: { - // get server - if (!(server is SLidgrenServer customServer)) + // get peer + if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - 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; + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); } - // store peer - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - var peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, customServer, rawMessage.SenderConnection); - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; } break; // handle mod message - case SMultiplayer.ModMessageID: - this.ReceiveModMessage(message); - break; - } - } - - /// Process an incoming message from an approved connection. - /// The message to process. - public override void processIncomingMessage(IncomingMessage message) - { - switch (message.MessageType) - { - // handle mod message - case SMultiplayer.ModMessageID: + case (byte)MessageType.ModMessage: this.ReceiveModMessage(message); break; - // let game process message default: - base.processIncomingMessage(message); + resume(); break; } - } - /// Process an incoming network message from the server. + /// Process an incoming network message as a farmhand. /// The client instance that received the connection. /// The message to process. + /// Process the message using the game's default logic. /// Returns whether the message was handled. - public bool TryProcessMessageFromServer(SLidgrenClient client, IncomingMessage message) + public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { + if (message.MessageType != Multiplayer.farmerDelta && message.MessageType != Multiplayer.locationDelta && message.MessageType != Multiplayer.teamDelta && message.MessageType != Multiplayer.worldDelta) + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Alert); + switch (message.MessageType) { - // receive SMAPI context from a connected player - case SMultiplayer.ContextSyncMessageID: + // mod context sync (step 4) + case (byte)MessageType.ModContext: { // 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); + 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 = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client); + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client, 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.Peers[message.FarmerID] = peer; if (peer.IsHost) this.HostPeer = peer; } - return true; + break; - // handle intro from unmodded player - case Multiplayer.playerIntroduction: - if (!this.Peers.ContainsKey(message.FarmerID)) + // handle server intro + case (byte)MessageType.ServerIntroduction: { // store peer - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - var peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client); - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) + { + this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true); + } + resume(); + break; + } + + // handle player intro + case (byte)MessageType.PlayerIntroduction: + { + // store peer + if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) + { + peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); + this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); + this.Peers[message.FarmerID] = peer; + if (peer.IsHost) + this.HostPeer = peer; + } } - return false; // handle mod message - case SMultiplayer.ModMessageID: + case (byte)MessageType.ModMessage: this.ReceiveModMessage(message); - return true; + break; default: - return false; + resume(); + break; } } @@ -352,12 +375,12 @@ namespace StardewModdingAPI.Framework if (playerIDs == null || playerIDs.Contains(peer.PlayerID)) { model.ToPlayerIDs = new[] { peer.PlayerID }; - peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, data)); + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data)); } } } else if (this.HostPeer != null && this.HostPeer.HasSmapi) - this.HostPeer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, this.HostPeer.PlayerID, data)); + this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data)); else this.VerboseLog(" Can't send message because no valid connections were found."); @@ -367,6 +390,17 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// Read the metadata context for a player. + /// The stream reader. + private RemoteContextModel ReadContext(BinaryReader reader) + { + string data = reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + return model.ApiVersion != null + ? model + : null; // no data available for unmodded players + } + /// Receive a mod message sent from another player's mods. /// The raw message to parse. private void ReceiveModMessage(IncomingMessage message) @@ -392,7 +426,7 @@ namespace StardewModdingAPI.Framework { newModel.ToPlayerIDs = new[] { peer.PlayerID }; this.VerboseLog($" Forwarding message to player {peer.PlayerID}."); - peer.SendMessage(new OutgoingMessage(SMultiplayer.ModMessageID, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); } } } diff --git a/src/SMAPI/Patches/LidgrenServerPatch.cs b/src/SMAPI/Patches/LidgrenServerPatch.cs new file mode 100644 index 00000000..6f937665 --- /dev/null +++ b/src/SMAPI/Patches/LidgrenServerPatch.cs @@ -0,0 +1,89 @@ +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 let SMAPI override methods. + internal class LidgrenServerPatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(LidgrenServerPatch)}"; + + + /********* + ** Public methods + *********/ + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + // override parseDataMessageFromClient + { + MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + + // override sendMessage + { + MethodInfo method = typeof(LidgrenServer).GetMethod("sendMessage", BindingFlags.NonPublic | BindingFlags.Instance, null, new [] { typeof(NetConnection), typeof(OutgoingMessage) }, null); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_SendMessage)); + 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) + { + if (__instance is SLidgrenServer smapiServer) + { + smapiServer.ParseDataMessageFromClient(dataMsg); + return false; + } + + return true; + } + + /// The method to call instead of the method. + /// The instance being patched. + /// The connection to which to send the message. + /// 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_SendMessage(LidgrenServer __instance, NetConnection connection, OutgoingMessage message, Bimap ___peers, IGameServer ___gameServer) + { + if (__instance is SLidgrenServer smapiServer) + { + smapiServer.SendMessage(connection, message); + return false; + } + + return true; + } + } +} diff --git a/src/SMAPI/Patches/NetworkingPatch.cs b/src/SMAPI/Patches/NetworkingPatch.cs deleted file mode 100644 index 12ccf84c..00000000 --- a/src/SMAPI/Patches/NetworkingPatch.cs +++ /dev/null @@ -1,103 +0,0 @@ -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 11fa5d35..dfe55eb9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -174,6 +174,7 @@ + @@ -322,7 +323,7 @@ - + -- cgit From 222265816d803e8e145c0a500568412d03dd49da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 22:41:31 -0500 Subject: add ContextReceived event (#480) --- src/SMAPI/Events/ContextReceivedEventArgs.cs | 25 +++++++++++ src/SMAPI/Events/IMultiplayerEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 4 ++ src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 7 +++ src/SMAPI/Framework/SMultiplayer.cs | 50 +++++++++++++++------- src/SMAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Events/ContextReceivedEventArgs.cs (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Events/ContextReceivedEventArgs.cs b/src/SMAPI/Events/ContextReceivedEventArgs.cs new file mode 100644 index 00000000..c715cf1c --- /dev/null +++ b/src/SMAPI/Events/ContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to whom a connection is being established. + internal ContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index a6ac6fd3..91e0789c 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,6 +5,9 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + event EventHandler ContextReceived; + /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 519cf48a..63ac17ee 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -101,6 +101,9 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + public readonly ManagedEvent ContextReceived; + /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; @@ -380,6 +383,7 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + this.ContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ContextReceived)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index a830a54a..432a92d3 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -16,6 +16,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ModMessageReceived.Remove(value); } + /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. + public event EventHandler ContextReceived + { + add => this.EventManager.ContextReceived.Add(value); + remove => this.EventManager.ContextReceived.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 70f1a89a..9f649639 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Lidgren.Network; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; @@ -186,7 +188,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); return; } - this.Peers[message.FarmerID] = newPeer; + this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); // reply with own context this.VerboseLog(" Replying with host context..."); @@ -209,20 +211,23 @@ namespace StardewModdingAPI.Framework otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } + + // raise event + this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(newPeer)); } break; // handle player intro case (byte)MessageType.PlayerIntroduction: + // store peer if new + if (!this.Peers.ContainsKey(message.FarmerID)) { - // get peer - if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) - { - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); - } - + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + MultiplayerPeer peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); + this.AddPeer(peer, canBeHost: false); } + + resume(); break; // handle mod message @@ -262,9 +267,7 @@ namespace StardewModdingAPI.Framework 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.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + this.AddPeer(peer, canBeHost: true); } break; @@ -275,7 +278,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true); + this.AddPeer(MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true), canBeHost: false); } resume(); break; @@ -289,10 +292,11 @@ namespace StardewModdingAPI.Framework { peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); - this.Peers[message.FarmerID] = peer; - if (peer.IsHost) - this.HostPeer = peer; + this.AddPeer(peer, canBeHost: true); } + + resume(); + break; } // handle mod message @@ -390,6 +394,22 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// Save a received peer. + /// The peer to add. + /// Whether to track the peer as the host if applicable. + /// Whether to raise the event. + 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.ContextReceived.Raise(new ContextReceivedEventArgs(peer)); + } + /// Read the metadata context for a player. /// The stream reader. private RemoteContextModel ReadContext(BinaryReader reader) diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index dfe55eb9..09ecddcd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ + -- cgit From b4a5b3829f0f738e5b7e05048068eaec9d2d01d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Nov 2018 23:07:10 -0500 Subject: add PeerDisconnected event (#480) --- src/SMAPI/Events/ContextReceivedEventArgs.cs | 25 --------------------- src/SMAPI/Events/IMultiplayerEvents.cs | 7 ++++-- src/SMAPI/Events/PeerContextReceivedEventArgs.cs | 25 +++++++++++++++++++++ src/SMAPI/Events/PeerDisconnectedEventArgs.cs | 25 +++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 10 ++++++--- src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 15 +++++++++---- src/SMAPI/Framework/SMultiplayer.cs | 26 ++++++++++++++++------ src/SMAPI/StardewModdingAPI.csproj | 3 ++- 8 files changed, 94 insertions(+), 42 deletions(-) delete mode 100644 src/SMAPI/Events/ContextReceivedEventArgs.cs create mode 100644 src/SMAPI/Events/PeerContextReceivedEventArgs.cs create mode 100644 src/SMAPI/Events/PeerDisconnectedEventArgs.cs (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Events/ContextReceivedEventArgs.cs b/src/SMAPI/Events/ContextReceivedEventArgs.cs deleted file mode 100644 index c715cf1c..00000000 --- a/src/SMAPI/Events/ContextReceivedEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an event. - public class ContextReceivedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The player whose metadata was received. - public IMultiplayerPeer Peer { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player to whom a connection is being established. - internal ContextReceivedEventArgs(IMultiplayerPeer peer) - { - this.Peer = peer; - } - } -} diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index 91e0789c..4a31f48e 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,10 +5,13 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - event EventHandler ContextReceived; + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + event EventHandler PeerContextReceived; /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; + + /// Raised after the connection with a peer is severed. + event EventHandler PeerDisconnected; } } diff --git a/src/SMAPI/Events/PeerContextReceivedEventArgs.cs b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs new file mode 100644 index 00000000..151a295c --- /dev/null +++ b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer whose metadata was received. + internal PeerContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Events/PeerDisconnectedEventArgs.cs b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs new file mode 100644 index 00000000..8517988a --- /dev/null +++ b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerDisconnectedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer who disconnected. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer who disconnected. + internal PeerDisconnectedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 63ac17ee..b9d1c453 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -101,12 +101,15 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - public readonly ManagedEvent ContextReceived; + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public readonly ManagedEvent PeerContextReceived; /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; + /// Raised after the connection with a peer is severed. + public readonly ManagedEvent PeerDisconnected; + /**** ** Player ****/ @@ -383,8 +386,9 @@ namespace StardewModdingAPI.Framework.Events this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); - this.ContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ContextReceived)); + this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.PeerDisconnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 432a92d3..152c4e0c 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public event EventHandler PeerContextReceived + { + add => this.EventManager.PeerContextReceived.Add(value); + remove => this.EventManager.PeerContextReceived.Remove(value); + } + /// Raised after a mod message is received over the network. public event EventHandler ModMessageReceived { @@ -16,11 +23,11 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ModMessageReceived.Remove(value); } - /// Raised after the mod context for a player is received. This happens before the game approves the connection, so the player does not yet exist in the game. This is the earliest point where messages can be sent to the player via SMAPI. - public event EventHandler ContextReceived + /// Raised after the connection with a peer is severed. + public event EventHandler PeerDisconnected { - add => this.EventManager.ContextReceived.Add(value); - remove => this.EventManager.ContextReceived.Remove(value); + add => this.EventManager.PeerDisconnected.Add(value); + remove => this.EventManager.PeerDisconnected.Remove(value); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 9f649639..2d0f8b9b 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -140,6 +140,9 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + resume(); } @@ -149,6 +152,9 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + switch (message.MessageType) { // sync mod context (step 1) @@ -171,6 +177,8 @@ namespace StardewModdingAPI.Framework /// Process the message using the game's default logic. public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) { + if (this.VerboseLogging) + this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) { @@ -213,7 +221,7 @@ namespace StardewModdingAPI.Framework } // raise event - this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(newPeer)); + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer)); } break; @@ -248,8 +256,8 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { - if (message.MessageType != Multiplayer.farmerDelta && message.MessageType != Multiplayer.locationDelta && message.MessageType != Multiplayer.teamDelta && message.MessageType != Multiplayer.worldDelta) - this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Alert); + if (this.VerboseLogging) + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) { @@ -315,8 +323,12 @@ namespace StardewModdingAPI.Framework { foreach (long playerID in this.DisconnectingFarmers) { - this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); - this.Peers.Remove(playerID); + 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(); @@ -397,7 +409,7 @@ namespace StardewModdingAPI.Framework /// Save a received peer. /// The peer to add. /// Whether to track the peer as the host if applicable. - /// Whether to raise the event. + /// Whether to raise the event. private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true) { // store @@ -407,7 +419,7 @@ namespace StardewModdingAPI.Framework // raise event if (raiseEvent) - this.EventManager.ContextReceived.Raise(new ContextReceivedEventArgs(peer)); + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer)); } /// Read the metadata context for a player. diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 09ecddcd..8c12c1f9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,7 +85,6 @@ - @@ -137,6 +136,8 @@ + + -- cgit From 6f569c579422641d6a56b491795504abd11893e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 7 Nov 2018 13:33:56 -0500 Subject: add verbose logging as a monitor feature --- docs/release-notes.md | 1 + src/SMAPI/Framework/Monitor.cs | 15 +++++++++++++- src/SMAPI/Framework/SCore.cs | 16 ++++----------- src/SMAPI/Framework/SGame.cs | 31 ++++++++++++---------------- src/SMAPI/Framework/SMultiplayer.cs | 41 +++++++++++++------------------------ src/SMAPI/IMonitor.cs | 9 +++++++- 6 files changed, 54 insertions(+), 59 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2783ebc2..b9a73f99 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -34,6 +34,7 @@ * For modders: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data). * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. + * Added [verbose logging](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Logging#Verbose_logging) feature. * Added `IContentPack.WriteJsonFile` method. * Added IntelliSense documentation for the non-developers version of SMAPI. * Added more events to the prototype `helper.Events` for SMAPI 3.0. diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 2812a9cc..a4d92e4b 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + public bool IsVerbose { get; } + /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } @@ -56,7 +59,8 @@ namespace StardewModdingAPI.Framework /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. /// The console color scheme to use. - public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme, bool isVerbose) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -68,6 +72,7 @@ namespace StardewModdingAPI.Framework this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme); this.ConsoleInterceptor = consoleInterceptor; this.ExitTokenSource = exitTokenSource; + this.IsVerbose = isVerbose; } /// Log a message for the player or developer. @@ -78,6 +83,14 @@ namespace StardewModdingAPI.Framework this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } + /// Log a message that only appears when is enabled. + /// The message to log. + public void VerboseLog(string message) + { + if (this.IsVerbose) + this.Log(message, LogLevel.Trace); + } + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// The reason for the shutdown. public void ExitGameImmediately(string reason) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ee175a1b..7e5b6e02 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) { WriteToConsole = writeToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, @@ -213,7 +213,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.DeprecationManager, this.InitialiseAfterGameStart, this.Dispose, this.Settings.VerboseLogging); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.DeprecationManager, this.InitialiseAfterGameStart, this.Dispose); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -355,7 +355,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - this.VerboseLog("Verbose logging enabled."); + this.Monitor.VerboseLog("Verbose logging enabled."); // validate XNB integrity if (!this.ValidateContentIntegrity()) @@ -1298,7 +1298,7 @@ namespace StardewModdingAPI.Framework /// The name of the module which will log messages with this instance. private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme) + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, @@ -1306,14 +1306,6 @@ namespace StardewModdingAPI.Framework }; } - /// Log a message if verbose mode is enabled. - /// The message to log. - private void VerboseLog(string message) - { - if (this.Settings.VerboseLogging) - this.Monitor.Log(message, LogLevel.Trace); - } - /// Get the absolute path to the next available log file. private string GetLogPath() { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 04db9632..3de97aea 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -57,9 +57,6 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; - /// Whether SMAPI should log more information about the game context. - private readonly bool VerboseLogging; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -143,8 +140,7 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - /// Whether SMAPI should log more information about the game context. - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting, bool verboseLogging) + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting) { SGame.ConstructorHack = null; @@ -164,9 +160,8 @@ namespace StardewModdingAPI.Framework this.DeprecationManager = deprecationManager; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; - this.VerboseLogging = verboseLogging; Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.VerboseLogging, this.OnModMessageReceived); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived); Game1.hooks = new SModHooks(this.OnNewDayAfterFade); // init observables @@ -468,7 +463,7 @@ namespace StardewModdingAPI.Framework // since the game adds & removes its own handler on the fly. if (this.Watchers.WindowSizeWatcher.IsChanged) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; @@ -507,7 +502,7 @@ namespace StardewModdingAPI.Framework int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; this.Watchers.MouseWheelScrollWatcher.Reset(); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); this.Events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); } @@ -520,7 +515,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); this.Events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); @@ -542,7 +537,7 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); this.Events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); @@ -581,7 +576,7 @@ namespace StardewModdingAPI.Framework IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events @@ -609,7 +604,7 @@ namespace StardewModdingAPI.Framework GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); this.Watchers.LocationsWatcher.ResetLocationList(); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) { string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; @@ -705,7 +700,7 @@ namespace StardewModdingAPI.Framework int now = this.Watchers.TimeWatcher.CurrentValue; this.Watchers.TimeWatcher.Reset(); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); this.Events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); @@ -722,7 +717,7 @@ namespace StardewModdingAPI.Framework // raise current location changed if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; @@ -733,7 +728,7 @@ namespace StardewModdingAPI.Framework // raise player leveled up a skill foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); this.Events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); @@ -744,7 +739,7 @@ namespace StardewModdingAPI.Framework ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); if (changedItems.Any()) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); this.Events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems)); @@ -753,7 +748,7 @@ namespace StardewModdingAPI.Framework // raise mine level changed if (playerTracker.TryGetNewMineLevel(out int mineLevel)) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel)); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 2d0f8b9b..dc9b8b68 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -50,9 +50,6 @@ namespace StardewModdingAPI.Framework /// The players who are currently disconnecting. private readonly IList DisconnectingFarmers; - /// Whether SMAPI should log more detailed information. - private readonly bool VerboseLogging; - /// A callback to invoke when a mod message is received. private readonly Action OnModMessageReceived; @@ -76,16 +73,14 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Simplifies access to private code. - /// Whether SMAPI should log more detailed information. /// A callback to invoke when a mod message is received. - public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging, Action onModMessageReceived) + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action onModMessageReceived) { this.Monitor = monitor; this.EventManager = eventManager; this.JsonHelper = jsonHelper; this.ModRegistry = modRegistry; this.Reflection = reflection; - this.VerboseLogging = verboseLogging; this.OnModMessageReceived = onModMessageReceived; this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); @@ -140,7 +135,7 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); resume(); @@ -152,7 +147,7 @@ namespace StardewModdingAPI.Framework /// Send the underlying message. protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) @@ -177,7 +172,7 @@ namespace StardewModdingAPI.Framework /// Process the message using the game's default logic. public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) @@ -199,13 +194,13 @@ namespace StardewModdingAPI.Framework this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); // reply with own context - this.VerboseLog(" Replying with host 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.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); + this.Monitor.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); } @@ -215,7 +210,7 @@ namespace StardewModdingAPI.Framework 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}..."); + this.Monitor.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); } } @@ -256,7 +251,7 @@ namespace StardewModdingAPI.Framework /// Returns whether the message was handled. public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) { - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); switch (message.MessageType) @@ -351,7 +346,7 @@ namespace StardewModdingAPI.Framework throw new ArgumentNullException(nameof(fromModID)); if (!this.Peers.Any()) { - this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); + this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); return; } @@ -363,7 +358,7 @@ namespace StardewModdingAPI.Framework playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); if (!playerIDs.Any()) { - this.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); + this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); return; } } @@ -380,7 +375,7 @@ namespace StardewModdingAPI.Framework string data = JsonConvert.SerializeObject(model, Formatting.None); // log message - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace); // send message @@ -398,7 +393,7 @@ namespace StardewModdingAPI.Framework else if (this.HostPeer != null && this.HostPeer.HasSmapi) this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data)); else - this.VerboseLog(" Can't send message because no valid connections were found."); + this.Monitor.VerboseLog(" Can't send message because no valid connections were found."); } @@ -441,7 +436,7 @@ namespace StardewModdingAPI.Framework string json = message.Reader.ReadString(); ModMessageModel model = this.JsonHelper.Deserialise(json); HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); - if (this.VerboseLogging) + if (this.Monitor.IsVerbose) this.Monitor.Log($"Received message: {json}."); // notify local mods @@ -457,7 +452,7 @@ namespace StardewModdingAPI.Framework if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) { newModel.ToPlayerIDs = new[] { peer.PlayerID }; - this.VerboseLog($" Forwarding message to player {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))); } } @@ -520,13 +515,5 @@ namespace StardewModdingAPI.Framework return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; } - - /// Log a trace message if is enabled. - /// The message to log. - private void VerboseLog(string message) - { - if (this.VerboseLogging) - this.Monitor.Log(message, LogLevel.Trace); - } } } diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs index 62c479bc..0f153e10 100644 --- a/src/SMAPI/IMonitor.cs +++ b/src/SMAPI/IMonitor.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI +namespace StardewModdingAPI { /// Encapsulates monitoring and logging for a given module. public interface IMonitor @@ -9,6 +9,9 @@ /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. bool IsExiting { get; } + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + bool IsVerbose { get; } + /********* ** Methods @@ -18,6 +21,10 @@ /// The log severity level. void Log(string message, LogLevel level = LogLevel.Debug); + /// Log a message that only appears when is enabled. + /// The message to log. + void VerboseLog(string message); + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// The reason for the shutdown. void ExitGameImmediately(string reason); -- cgit From 90ecd377c88ba5a4c08a3c7e67618435358e5685 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 00:11:09 -0500 Subject: rework multiplayer code to allow for upcoming Galaxy server overrides (#480) --- src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 60 +++-------------------- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 59 +++------------------- src/SMAPI/Framework/SMultiplayer.cs | 42 ++++++---------- src/SMAPI/Patches/LidgrenServerPatch.cs | 30 ------------ 4 files changed, 28 insertions(+), 163 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index 7f0fa4f7..44a71978 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Lidgren.Network; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -12,14 +11,8 @@ namespace StardewModdingAPI.Framework.Networking /********* ** 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; + /// A method which sends a message to the peer. + private readonly Action SendMessageImpl; /********* @@ -53,11 +46,9 @@ namespace StardewModdingAPI.Framework.Networking /// 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. + /// A method which sends a message to the peer. /// Whether this is a connection to the host player. - public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client, bool isHost) + public MultiplayerPeer(long playerID, RemoteContextModel model, Action sendMessage, bool isHost) { this.PlayerID = playerID; this.IsHost = isHost; @@ -68,43 +59,7 @@ namespace StardewModdingAPI.Framework.Networking 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, - isHost: false - ); - } - - /// 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. - /// Whether this connection is for the host player. - public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client, bool isHost) - { - return new MultiplayerPeer( - playerID: playerID, - model: model, - server: null, - serverConnection: null, - client: client, - isHost: isHost - ); + this.SendMessageImpl = sendMessage; } /// Get metadata for a mod installed by the player. @@ -123,10 +78,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to send. public void SendMessage(OutgoingMessage message) { - if (this.IsHost) - this.Client.sendMessage(message); - else - this.Server.SendMessage(this.ServerConnection, message); + this.SendMessageImpl(message); } } } diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 060b433b..36f96bc3 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -20,24 +20,15 @@ namespace StardewModdingAPI.Framework.Networking /// The constructor for the internal NetBufferReadStream type. private readonly ConstructorInfo NetBufferReadStreamConstructor = SLidgrenServer.GetNetBufferReadStreamConstructor(); - /// The constructor for the internal NetBufferWriteStream type. - private readonly ConstructorInfo NetBufferWriteStreamConstructor = SLidgrenServer.GetNetBufferWriteStreamConstructor(); - /// A method which reads farmer data from the given binary reader. private readonly Func ReadFarmer; - /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. - private readonly Action OnProcessingMessage; - - /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, target player ID, and a callback to run the default logic. - private readonly Action OnSendingMessage; + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; /// The peer connections. private readonly Bimap Peers; - /// The underlying net server. - private readonly IReflectedField Server; - /********* ** Public methods @@ -46,34 +37,13 @@ namespace StardewModdingAPI.Framework.Networking /// The underlying game server. /// Simplifies access to private code. /// A method which reads farmer data from the given binary reader. - /// A callback to raise when receiving a message. This receives the server instance, raw/parsed incoming message, and a callback to run the default logic. - /// A callback to raise when sending a message. This receives the server instance, outgoing connection, outgoing message, and a callback to run the default logic. - public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action onProcessingMessage, Action onSendingMessage) + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action, Action> onProcessingMessage) : base(gameServer) { this.ReadFarmer = readFarmer; this.OnProcessingMessage = onProcessingMessage; - this.OnSendingMessage = onSendingMessage; this.Peers = reflection.GetField>(this, "peers").GetValue(); - this.Server = reflection.GetField(this, "server"); - } - - /// Send a message to a remote server. - /// The network connection. - /// The message to send. - /// This is an implementation of which calls . This method is invoked via . - public void SendMessage(NetConnection connection, OutgoingMessage message) - { - this.OnSendingMessage(this, connection, message, () => - { - NetServer server = this.Server.GetValue(); - NetOutgoingMessage netMessage = server.CreateMessage(); - using (Stream bufferWriteStream = (Stream)this.NetBufferWriteStreamConstructor.Invoke(new object[] { netMessage })) - using (BinaryWriter writer = new BinaryWriter(bufferWriteStream)) - message.Write(writer); - - server.SendMessage(netMessage, connection, NetDeliveryMethod.ReliableOrdered); - }); } /// Parse a data message from a client. @@ -91,14 +61,14 @@ namespace StardewModdingAPI.Framework.Networking while (rawMessage.LengthBits - rawMessage.Position >= 8) { message.Read(reader); - this.OnProcessingMessage(this, rawMessage, message, () => + this.OnProcessingMessage(message, outgoing => this.sendMessage(rawMessage.SenderConnection, outgoing), () => { if (this.Peers.ContainsLeft(message.FarmerID) && this.Peers[message.FarmerID] == peer) this.gameServer.processIncomingMessage(message); else if (message.MessageType == Multiplayer.playerIntroduction) { NetFarmerRoot farmer = this.ReadFarmer(message.Reader); - this.gameServer.checkFarmhandRequest("", farmer, msg => this.SendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); } }); } @@ -127,22 +97,5 @@ namespace StardewModdingAPI.Framework.Networking return constructor; } - - /// Get the constructor for the internal NetBufferWriteStream type. - private static ConstructorInfo GetNetBufferWriteStreamConstructor() - { - // get type - string typeName = $"StardewValley.Network.NetBufferWriteStream, {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/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index dc9b8b68..843cd1fb 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Lidgren.Network; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StardewModdingAPI.Events; @@ -119,26 +118,18 @@ namespace StardewModdingAPI.Framework /// The server to initialise. public override Server InitServer(Server server) { - if (server is LidgrenServer) + switch (server) { - IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage, this.OnServerSendingMessage); - } + case LidgrenServer _: + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); + } - return server; - } - /// A callback raised when sending a network message as the host player. - /// The server sending the message. - /// The connection to which a message is being sent. - /// The message being sent. - /// Send the underlying message. - protected void OnServerSendingMessage(SLidgrenServer server, NetConnection connection, OutgoingMessage message, Action resume) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"SERVER SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); - - resume(); + default: + return server; + } } /// A callback raised when sending a message as a farmhand. @@ -166,11 +157,10 @@ namespace StardewModdingAPI.Framework } /// Process an incoming network message as the host player. - /// The server instance that received the connection. - /// The raw network message that was received. /// The message to process. + /// A method which sends the given message to the client. /// Process the message using the game's default logic. - public void OnServerProcessingMessage(SLidgrenServer server, NetIncomingMessage rawMessage, IncomingMessage message, Action resume) + public void OnServerProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) { if (this.Monitor.IsVerbose) this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); @@ -185,7 +175,7 @@ namespace StardewModdingAPI.Framework 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 = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, server, rawMessage.SenderConnection); + MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false); if (this.Peers.ContainsKey(message.FarmerID)) { this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); @@ -226,7 +216,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID)) { this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - MultiplayerPeer peer = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, null, server, rawMessage.SenderConnection); + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false); this.AddPeer(peer, canBeHost: false); } @@ -264,7 +254,7 @@ namespace StardewModdingAPI.Framework 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 = MultiplayerPeer.ForConnectionToHost(message.FarmerID, model, client, model?.IsHost ?? this.HostPeer == null); + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, client.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); @@ -281,7 +271,7 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.AddPeer(MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: true), canBeHost: false); + this.AddPeer(new MultiplayerPeer(message.FarmerID, null, client.sendMessage, isHost: true), canBeHost: false); } resume(); break; @@ -293,7 +283,7 @@ namespace StardewModdingAPI.Framework // store peer if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - peer = MultiplayerPeer.ForConnectionToHost(message.FarmerID, null, client, isHost: this.HostPeer == null); + peer = new MultiplayerPeer(message.FarmerID, null, client.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); } diff --git a/src/SMAPI/Patches/LidgrenServerPatch.cs b/src/SMAPI/Patches/LidgrenServerPatch.cs index 6f937665..47acd4c4 100644 --- a/src/SMAPI/Patches/LidgrenServerPatch.cs +++ b/src/SMAPI/Patches/LidgrenServerPatch.cs @@ -1,13 +1,9 @@ -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 @@ -35,13 +31,6 @@ namespace StardewModdingAPI.Patches MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_ParseDataMessageFromClient)); harmony.Patch(method, new HarmonyMethod(prefix), null); } - - // override sendMessage - { - MethodInfo method = typeof(LidgrenServer).GetMethod("sendMessage", BindingFlags.NonPublic | BindingFlags.Instance, null, new [] { typeof(NetConnection), typeof(OutgoingMessage) }, null); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.Prefix_LidgrenServer_SendMessage)); - harmony.Patch(method, new HarmonyMethod(prefix), null); - } } @@ -66,24 +55,5 @@ namespace StardewModdingAPI.Patches return true; } - - /// The method to call instead of the method. - /// The instance being patched. - /// The connection to which to send the message. - /// 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_SendMessage(LidgrenServer __instance, NetConnection connection, OutgoingMessage message, Bimap ___peers, IGameServer ___gameServer) - { - if (__instance is SLidgrenServer smapiServer) - { - smapiServer.SendMessage(connection, message); - return false; - } - - return true; - } } } -- cgit From 77a7a0fe58186e49e24c41f47a2a33661afbc81c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 00:35:36 -0500 Subject: rework multiplayer code to allow for upcoming Galaxy client overrides (#480) --- src/SMAPI/Framework/Networking/SLidgrenClient.cs | 19 ++++++------- src/SMAPI/Framework/SMultiplayer.cs | 34 +++++++++++++----------- 2 files changed, 29 insertions(+), 24 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs index c05e6b76..02d9d68f 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenClient.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs @@ -9,20 +9,21 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. - private readonly Action OnProcessingMessage; + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + private readonly Action, Action> OnSendingMessage; - /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. - private readonly Action OnSendingMessage; /********* ** Public methods *********/ /// Construct an instance. /// The remote address being connected. - /// A callback to raise when receiving a message. This receives the client instance, incoming message, and a callback to run the default logic. - /// A callback to raise when sending a message. This receives the client instance, outgoing message, and a callback to run the default logic. - public SLidgrenClient(string address, Action onProcessingMessage, Action onSendingMessage) + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + public SLidgrenClient(string address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) : base(address) { this.OnProcessingMessage = onProcessingMessage; @@ -32,7 +33,7 @@ namespace StardewModdingAPI.Framework.Networking /// Send a message to the connected peer. public override void sendMessage(OutgoingMessage message) { - this.OnSendingMessage(this, message, () => base.sendMessage(message)); + this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); } @@ -43,7 +44,7 @@ namespace StardewModdingAPI.Framework.Networking /// The message to process. protected override void processIncomingMessage(IncomingMessage message) { - this.OnProcessingMessage(this, message, () => base.processIncomingMessage(message)); + this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); } } } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 843cd1fb..6ce7596d 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -105,13 +105,17 @@ namespace StardewModdingAPI.Framework /// The client to initialise. public override Client InitClient(Client client) { - if (client is LidgrenClient) + switch (client) { - string address = this.Reflection.GetField(client, "address").GetValue(); - return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); - } + case LidgrenClient _: + { + string address = this.Reflection.GetField(client, "address").GetValue(); + return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); + } - return client; + default: + return client; + } } /// Initialise a server before the game connects to an incoming player. @@ -133,10 +137,10 @@ namespace StardewModdingAPI.Framework } /// A callback raised when sending a message as a farmhand. - /// The client sending the message. /// The message being sent. - /// Send the underlying message. - protected void OnClientSendingMessage(SLidgrenClient client, OutgoingMessage message, Action resume) + /// Send an arbitrary message through the client. + /// Resume sending the underlying message. + protected void OnClientSendingMessage(OutgoingMessage message, Action sendMessage, Action resume) { if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); @@ -145,7 +149,7 @@ namespace StardewModdingAPI.Framework { // sync mod context (step 1) case (byte)MessageType.PlayerIntroduction: - client.sendMessage((byte)MessageType.ModContext, this.GetContextSyncMessageFields()); + sendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); resume(); break; @@ -235,11 +239,11 @@ namespace StardewModdingAPI.Framework } /// Process an incoming network message as a farmhand. - /// The client instance that received the connection. /// The message to process. - /// Process the message using the game's default logic. + /// Send an arbitrary message through the client. + /// Resume processing the message using the game's default logic. /// Returns whether the message was handled. - public void OnClientProcessingMessage(SLidgrenClient client, IncomingMessage message, Action resume) + public void OnClientProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) { if (this.Monitor.IsVerbose) this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); @@ -254,7 +258,7 @@ namespace StardewModdingAPI.Framework 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, client.sendMessage, isHost: model?.IsHost ?? this.HostPeer == null); + 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); @@ -271,7 +275,7 @@ namespace StardewModdingAPI.Framework 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, client.sendMessage, isHost: true), canBeHost: false); + this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false); } resume(); break; @@ -283,7 +287,7 @@ namespace StardewModdingAPI.Framework // store peer if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - peer = new MultiplayerPeer(message.FarmerID, null, client.sendMessage, isHost: this.HostPeer == null); + 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); } -- cgit From 8e1d45b3100453a11bcdf663d3567935c592098c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 00:57:52 -0500 Subject: override Galaxy client/server to support context sync (#480) This commit assumes the changes I requested in the game code to make GalaxyNetClient and GalaxyNetServer public are implemented in 1.3.32. --- src/SMAPI/Framework/Networking/SGalaxyNetClient.cs | 52 ++++++++++++ src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 96 ++++++++++++++++++++++ src/SMAPI/Framework/SMultiplayer.cs | 13 +++ src/SMAPI/StardewModdingAPI.csproj | 6 +- 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetClient.cs create mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs new file mode 100644 index 00000000..fddd423d --- /dev/null +++ b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs @@ -0,0 +1,52 @@ +using System; +using Galaxy.Api; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. + internal class SGalaxyNetClient : GalaxyNetClient + { + /********* + ** Properties + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + private readonly Action, Action> OnSendingMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The remote address being connected. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + public SGalaxyNetClient(GalaxyID address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) + : base(address) + { + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; + } + + /// Send a message to the connected peer. + public override void sendMessage(OutgoingMessage message) + { + this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); + } + + + /********* + ** Protected methods + *********/ + /// Process an incoming network message. + /// The message to process. + protected override void processIncomingMessage(IncomingMessage message) + { + this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); + } + } +} diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs new file mode 100644 index 00000000..99eae8ad --- /dev/null +++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using Galaxy.Api; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; +using StardewValley.Network; +using StardewValley.SDKs; + +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 SGalaxyNetServer : GalaxyNetServer + { + /********* + ** Properties + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// The peer connections. + private readonly Bimap Peers; + + /// The underlying net server. + private readonly IReflectedField Server; + + /// The underlying method which handles incoming connections. + private readonly Action BaseReceiveConnection; + + /// The underlying method which handles incoming disconnections. + private readonly Action BaseReceiveDisconnect; + + /// The underlying method which handles incoming errors. + private readonly Action BaseReceiveError; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + /// Simplifies access to private code. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SGalaxyNetServer(IGameServer gameServer, Reflector reflection, Action, Action> onProcessingMessage) + : base(gameServer) + { + this.OnProcessingMessage = onProcessingMessage; + this.Peers = reflection.GetField>(this, "peers").GetValue(); + this.Server = reflection.GetField(this, "server"); + + this.BaseReceiveConnection = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveConnection").MethodInfo); + this.BaseReceiveDisconnect = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveDisconnect").MethodInfo); + this.BaseReceiveError = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveError").MethodInfo); + } + + /// Receive and process messages from the client. + public override void receiveMessages() + { + GalaxySocket server = this.Server.GetValue(); + if (server == null) + return; + + server.Receive(this.BaseReceiveConnection, this.OnReceiveMessage, this.BaseReceiveDisconnect, this.BaseReceiveError); + server.Heartbeat(server.LobbyMembers()); + foreach (GalaxyID connection in server.Connections) + { + if (server.GetPingWith(connection) > 30000L) + server.Kick(connection); + } + } + + /// Read and process a message from the client. + /// The Galaxy peer ID. + /// The data to process. + private void OnReceiveMessage(GalaxyID peerID, Stream data) + { + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(data)) + { + message.Read(reader); + this.OnProcessingMessage(message, outgoing => this.sendMessage(peerID, outgoing), () => + { + if (this.Peers.ContainsLeft(message.FarmerID) && (long)this.Peers[message.FarmerID] == (long)peerID.ToUint64()) + { + this.gameServer.processIncomingMessage(message); + } + else if (message.MessageType == Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = Game1.multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peerID.ToUint64()); + this.gameServer.checkFarmhandRequest(Convert.ToString(peerID.ToUint64()), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + } + } +} diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 6ce7596d..5a8aa3e5 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -2,6 +2,7 @@ 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; @@ -11,6 +12,7 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.Network; +using StardewValley.SDKs; namespace StardewModdingAPI.Framework { @@ -113,6 +115,12 @@ namespace StardewModdingAPI.Framework return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); } + case GalaxyNetClient _: + { + GalaxyID address = this.Reflection.GetField(client, "lobbyId").GetValue(); + return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); + } + default: return client; } @@ -130,6 +138,11 @@ namespace StardewModdingAPI.Framework return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); } + case GalaxyNetServer _: + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); + } default: return server; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 471e98fc..29c9f7fa 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -55,7 +55,7 @@ - + @@ -174,13 +174,15 @@ - + + + -- cgit From 15acbc8f230dd2c4ba394960cdcc12a22a831bbf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 01:36:43 -0500 Subject: patch GalaxyNetServer to support context sync pending game code changes to make it public (#480) --- src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 96 ------------------- src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Framework/SMultiplayer.cs | 10 +- src/SMAPI/Patches/GalaxyNetServerPatch.cs | 106 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 2 +- 5 files changed, 114 insertions(+), 103 deletions(-) delete mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs create mode 100644 src/SMAPI/Patches/GalaxyNetServerPatch.cs (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs deleted file mode 100644 index 99eae8ad..00000000 --- a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.IO; -using Galaxy.Api; -using StardewModdingAPI.Framework.Reflection; -using StardewValley; -using StardewValley.Network; -using StardewValley.SDKs; - -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 SGalaxyNetServer : GalaxyNetServer - { - /********* - ** Properties - *********/ - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - private readonly Action, Action> OnProcessingMessage; - - /// The peer connections. - private readonly Bimap Peers; - - /// The underlying net server. - private readonly IReflectedField Server; - - /// The underlying method which handles incoming connections. - private readonly Action BaseReceiveConnection; - - /// The underlying method which handles incoming disconnections. - private readonly Action BaseReceiveDisconnect; - - /// The underlying method which handles incoming errors. - private readonly Action BaseReceiveError; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying game server. - /// Simplifies access to private code. - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - public SGalaxyNetServer(IGameServer gameServer, Reflector reflection, Action, Action> onProcessingMessage) - : base(gameServer) - { - this.OnProcessingMessage = onProcessingMessage; - this.Peers = reflection.GetField>(this, "peers").GetValue(); - this.Server = reflection.GetField(this, "server"); - - this.BaseReceiveConnection = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveConnection").MethodInfo); - this.BaseReceiveDisconnect = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveDisconnect").MethodInfo); - this.BaseReceiveError = (Action)Delegate.CreateDelegate(typeof(Action), this, reflection.GetMethod(this, "onReceiveError").MethodInfo); - } - - /// Receive and process messages from the client. - public override void receiveMessages() - { - GalaxySocket server = this.Server.GetValue(); - if (server == null) - return; - - server.Receive(this.BaseReceiveConnection, this.OnReceiveMessage, this.BaseReceiveDisconnect, this.BaseReceiveError); - server.Heartbeat(server.LobbyMembers()); - foreach (GalaxyID connection in server.Connections) - { - if (server.GetPingWith(connection) > 30000L) - server.Kick(connection); - } - } - - /// Read and process a message from the client. - /// The Galaxy peer ID. - /// The data to process. - private void OnReceiveMessage(GalaxyID peerID, Stream data) - { - using (IncomingMessage message = new IncomingMessage()) - using (BinaryReader reader = new BinaryReader(data)) - { - message.Read(reader); - this.OnProcessingMessage(message, outgoing => this.sendMessage(peerID, outgoing), () => - { - if (this.Peers.ContainsLeft(message.FarmerID) && (long)this.Peers[message.FarmerID] == (long)peerID.ToUint64()) - { - this.gameServer.processIncomingMessage(message); - } - else if (message.MessageType == Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = Game1.multiplayer.readFarmer(message.Reader); - GalaxyID capturedPeer = new GalaxyID(peerID.ToUint64()); - this.gameServer.checkFarmhandRequest(Convert.ToString(peerID.ToUint64()), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); - } - }); - } - } - } -} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 890058b0..6ad118ce 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -170,7 +170,8 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new LidgrenServerPatch() + new LidgrenServerPatch(), + new GalaxyNetServerPatch(() => this.GameInstance.Multiplayer) ); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 5a8aa3e5..1777a261 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -138,11 +138,11 @@ namespace StardewModdingAPI.Framework return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); } - case GalaxyNetServer _: - { - IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); - } + //case GalaxyNetServer _: + // { + // IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + // return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); + // } default: return server; diff --git a/src/SMAPI/Patches/GalaxyNetServerPatch.cs b/src/SMAPI/Patches/GalaxyNetServerPatch.cs new file mode 100644 index 00000000..e01ac329 --- /dev/null +++ b/src/SMAPI/Patches/GalaxyNetServerPatch.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using Galaxy.Api; +using Harmony; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Patching; +using StardewValley.Network; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch to let SMAPI override methods. + internal class GalaxyNetServerPatch : IHarmonyPatch + { + /********* + ** Properties + *********/ + /// SMAPI's implementation of the game's core multiplayer logic. + private static Lazy Multiplayer; + + /// The name of the internal GalaxyNetServer class. + private static readonly string ServerTypeName = $"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"; + + /// The method which sends an arbitrary message. + private static MethodInfo SendMessageMethod; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(GalaxyNetServerPatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's implementation of the game's core multiplayer logic. + public GalaxyNetServerPatch(Func multiplayer) + { + // init + GalaxyNetServerPatch.Multiplayer = new Lazy(multiplayer); + + // get server.sendMessage method + Type type = Type.GetType(GalaxyNetServerPatch.ServerTypeName); + if (type == null) + throw new InvalidOperationException($"Can't find type '{GalaxyNetServerPatch.ServerTypeName}'."); + GalaxyNetServerPatch.SendMessageMethod = type.GetMethod("sendMessage", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(GalaxyID), typeof(OutgoingMessage) }, null); + if (GalaxyNetServerPatch.SendMessageMethod == null) + throw new InvalidOperationException($"Can't find method 'sendMessage' on '{GalaxyNetServerPatch.ServerTypeName}'."); + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + // override parseDataMessageFromClient + { + MethodInfo method = AccessTools.Method(Type.GetType($"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"), "onReceiveMessage"); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GalaxyNetServerPatch.Prefix_GalaxyNetServer_OnReceiveMessage)); + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the method. + /// The instance being patched. + /// The Galaxy peer ID. + /// The data to process. + /// 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_GalaxyNetServer_OnReceiveMessage(Server __instance, GalaxyID peer, Stream messageStream, Bimap ___peers, IGameServer ___gameServer) + { + SMultiplayer multiplayer = GalaxyNetServerPatch.Multiplayer.Value; + + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(messageStream)) + { + message.Read(reader); + multiplayer.OnServerProcessingMessage(message, outgoing => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { peer, outgoing }), () => + { + if (___peers.ContainsLeft(message.FarmerID) && (long)___peers[message.FarmerID] == (long)peer.ToUint64()) + { + ___gameServer.processIncomingMessage(message); + } + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peer.ToUint64()); + ___gameServer.checkFarmhandRequest(Convert.ToString(peer.ToUint64()), farmer, msg => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { capturedPeer, msg }), () => ___peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + + return false; + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 29c9f7fa..f16087bc 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -182,7 +182,6 @@ - @@ -325,6 +324,7 @@ + -- cgit From 0a50cdb162d7ba1c9219d1982dccbb5828b070c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 18:18:32 -0500 Subject: update multiplayer code for Stardew Valley 1.3.22 (#480) --- src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 63 ++++++++++++ src/SMAPI/Framework/Networking/SLidgrenServer.cs | 59 +++--------- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/Framework/SMultiplayer.cs | 19 ++-- src/SMAPI/Patches/GalaxyNetServerPatch.cs | 106 --------------------- src/SMAPI/Patches/LidgrenServerPatch.cs | 59 ------------ src/SMAPI/StardewModdingAPI.csproj | 3 +- 7 files changed, 83 insertions(+), 230 deletions(-) create mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs delete mode 100644 src/SMAPI/Patches/GalaxyNetServerPatch.cs delete mode 100644 src/SMAPI/Patches/LidgrenServerPatch.cs (limited to 'src/SMAPI/Framework/SMultiplayer.cs') diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs new file mode 100644 index 00000000..2fc92737 --- /dev/null +++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Galaxy.Api; +using StardewValley.Network; +using StardewValley.SDKs; + +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 SGalaxyNetServer : GalaxyNetServer + { + /********* + ** Properties + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// SMAPI's implementation of the game's core multiplayer logic. + private readonly SMultiplayer Multiplayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + /// SMAPI's implementation of the game's core multiplayer logic. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SGalaxyNetServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) + : base(gameServer) + { + this.Multiplayer = multiplayer; + this.OnProcessingMessage = onProcessingMessage; + } + + /// Read and process a message from the client. + /// The Galaxy peer ID. + /// The data to process. + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] + protected override void onReceiveMessage(GalaxyID peer, Stream messageStream) + { + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(messageStream)) + { + message.Read(reader); + this.OnProcessingMessage(message, outgoing => this.sendMessage(peer, outgoing), () => + { + if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peer.ToUint64()) + { + this.gameServer.processIncomingMessage(message); + } + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peer.ToUint64()); + this.gameServer.checkFarmhandRequest(Convert.ToString(peer.ToUint64()), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + } + } +} diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index 36f96bc3..37e546a1 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -1,11 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reflection; using Lidgren.Network; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Patches; -using StardewValley; using StardewValley.Network; namespace StardewModdingAPI.Framework.Networking @@ -16,46 +12,36 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Properties *********/ - - /// The constructor for the internal NetBufferReadStream type. - private readonly ConstructorInfo NetBufferReadStreamConstructor = SLidgrenServer.GetNetBufferReadStreamConstructor(); - - /// A method which reads farmer data from the given binary reader. - private readonly Func ReadFarmer; + /// SMAPI's implementation of the game's core multiplayer logic. + private readonly SMultiplayer Multiplayer; /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. private readonly Action, Action> OnProcessingMessage; - /// The peer connections. - private readonly Bimap Peers; - /********* ** Public methods *********/ /// Construct an instance. + /// SMAPI's implementation of the game's core multiplayer logic. /// The underlying game server. - /// Simplifies access to private code. - /// A method which reads farmer data from the given binary reader. /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - public SLidgrenServer(IGameServer gameServer, Reflector reflection, Func readFarmer, Action, Action> onProcessingMessage) + public SLidgrenServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) : base(gameServer) { - this.ReadFarmer = readFarmer; + this.Multiplayer = multiplayer; this.OnProcessingMessage = onProcessingMessage; - this.Peers = reflection.GetField>(this, "peers").GetValue(); } /// Parse a data message from a client. /// The raw network message to parse. - /// This is an implementation of which calls . This method is invoked via . [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] - public bool ParseDataMessageFromClient(NetIncomingMessage rawMessage) + protected override void parseDataMessageFromClient(NetIncomingMessage rawMessage) { // add hook to call multiplayer core NetConnection peer = rawMessage.SenderConnection; using (IncomingMessage message = new IncomingMessage()) - using (Stream readStream = (Stream)this.NetBufferReadStreamConstructor.Invoke(new object[] { rawMessage })) + using (Stream readStream = new NetBufferReadStream(rawMessage)) using (BinaryReader reader = new BinaryReader(readStream)) { while (rawMessage.LengthBits - rawMessage.Position >= 8) @@ -63,39 +49,16 @@ namespace StardewModdingAPI.Framework.Networking message.Read(reader); this.OnProcessingMessage(message, outgoing => this.sendMessage(rawMessage.SenderConnection, outgoing), () => { - if (this.Peers.ContainsLeft(message.FarmerID) && this.Peers[message.FarmerID] == peer) + if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) this.gameServer.processIncomingMessage(message); - else if (message.MessageType == Multiplayer.playerIntroduction) + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) { - NetFarmerRoot farmer = this.ReadFarmer(message.Reader); - this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.Peers[farmer.Value.UniqueMultiplayerID] = peer); + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer); } }); } } - - return false; - } - - - /********* - ** Private methods - *********/ - /// 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/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6ad118ce..128659c7 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -169,9 +169,7 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new DialogueErrorPatch(this.MonitorForGame, this.Reflection), - new LidgrenServerPatch(), - new GalaxyNetServerPatch(() => this.GameInstance.Multiplayer) + new DialogueErrorPatch(this.MonitorForGame, this.Reflection) ); } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 1777a261..629fce1d 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -48,9 +48,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events. private readonly EventManager EventManager; - /// The players who are currently disconnecting. - private readonly IList DisconnectingFarmers; - /// A callback to invoke when a mod message is received. private readonly Action OnModMessageReceived; @@ -83,8 +80,6 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.OnModMessageReceived = onModMessageReceived; - - this.DisconnectingFarmers = reflection.GetField>(this, "disconnectingFarmers").GetValue(); } /// Handle sync messages from other players and perform other initial sync logic. @@ -135,14 +130,14 @@ namespace StardewModdingAPI.Framework case LidgrenServer _: { IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - return new SLidgrenServer(gameServer, this.Reflection, this.readFarmer, this.OnServerProcessingMessage); + return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage); } - //case GalaxyNetServer _: - // { - // IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); - // return new SGalaxyNetServer(gameServer, this.Reflection, this.OnServerProcessingMessage); - // } + case GalaxyNetServer _: + { + IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage); + } default: return server; @@ -323,7 +318,7 @@ namespace StardewModdingAPI.Framework /// Remove players who are disconnecting. protected override void removeDisconnectedFarmers() { - foreach (long playerID in this.DisconnectingFarmers) + foreach (long playerID in this.disconnectingFarmers) { if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) { diff --git a/src/SMAPI/Patches/GalaxyNetServerPatch.cs b/src/SMAPI/Patches/GalaxyNetServerPatch.cs deleted file mode 100644 index e01ac329..00000000 --- a/src/SMAPI/Patches/GalaxyNetServerPatch.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using Galaxy.Api; -using Harmony; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Patching; -using StardewValley.Network; - -namespace StardewModdingAPI.Patches -{ - /// A Harmony patch to let SMAPI override methods. - internal class GalaxyNetServerPatch : IHarmonyPatch - { - /********* - ** Properties - *********/ - /// SMAPI's implementation of the game's core multiplayer logic. - private static Lazy Multiplayer; - - /// The name of the internal GalaxyNetServer class. - private static readonly string ServerTypeName = $"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"; - - /// The method which sends an arbitrary message. - private static MethodInfo SendMessageMethod; - - - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(GalaxyNetServerPatch)}"; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's implementation of the game's core multiplayer logic. - public GalaxyNetServerPatch(Func multiplayer) - { - // init - GalaxyNetServerPatch.Multiplayer = new Lazy(multiplayer); - - // get server.sendMessage method - Type type = Type.GetType(GalaxyNetServerPatch.ServerTypeName); - if (type == null) - throw new InvalidOperationException($"Can't find type '{GalaxyNetServerPatch.ServerTypeName}'."); - GalaxyNetServerPatch.SendMessageMethod = type.GetMethod("sendMessage", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(GalaxyID), typeof(OutgoingMessage) }, null); - if (GalaxyNetServerPatch.SendMessageMethod == null) - throw new InvalidOperationException($"Can't find method 'sendMessage' on '{GalaxyNetServerPatch.ServerTypeName}'."); - } - - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - // override parseDataMessageFromClient - { - MethodInfo method = AccessTools.Method(Type.GetType($"StardewValley.SDKs.GalaxyNetServer, {Constants.GameAssemblyName}"), "onReceiveMessage"); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(GalaxyNetServerPatch.Prefix_GalaxyNetServer_OnReceiveMessage)); - harmony.Patch(method, new HarmonyMethod(prefix), null); - } - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of the method. - /// The instance being patched. - /// The Galaxy peer ID. - /// The data to process. - /// 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_GalaxyNetServer_OnReceiveMessage(Server __instance, GalaxyID peer, Stream messageStream, Bimap ___peers, IGameServer ___gameServer) - { - SMultiplayer multiplayer = GalaxyNetServerPatch.Multiplayer.Value; - - using (IncomingMessage message = new IncomingMessage()) - using (BinaryReader reader = new BinaryReader(messageStream)) - { - message.Read(reader); - multiplayer.OnServerProcessingMessage(message, outgoing => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { peer, outgoing }), () => - { - if (___peers.ContainsLeft(message.FarmerID) && (long)___peers[message.FarmerID] == (long)peer.ToUint64()) - { - ___gameServer.processIncomingMessage(message); - } - else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = multiplayer.readFarmer(message.Reader); - GalaxyID capturedPeer = new GalaxyID(peer.ToUint64()); - ___gameServer.checkFarmhandRequest(Convert.ToString(peer.ToUint64()), farmer, msg => GalaxyNetServerPatch.SendMessageMethod.Invoke(__instance, new object[] { capturedPeer, msg }), () => ___peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); - } - }); - } - - return false; - } - } -} diff --git a/src/SMAPI/Patches/LidgrenServerPatch.cs b/src/SMAPI/Patches/LidgrenServerPatch.cs deleted file mode 100644 index 47acd4c4..00000000 --- a/src/SMAPI/Patches/LidgrenServerPatch.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Harmony; -using Lidgren.Network; -using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.Patching; -using StardewValley.Network; - -namespace StardewModdingAPI.Patches -{ - /// A Harmony patch to let SMAPI override methods. - internal class LidgrenServerPatch : IHarmonyPatch - { - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(LidgrenServerPatch)}"; - - - /********* - ** Public methods - *********/ - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - // override parseDataMessageFromClient - { - MethodInfo method = AccessTools.Method(typeof(LidgrenServer), "parseDataMessageFromClient"); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LidgrenServerPatch.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) - { - if (__instance is SLidgrenServer smapiServer) - { - smapiServer.ParseDataMessageFromClient(dataMsg); - return false; - } - - return true; - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index f16087bc..70fe2bed 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -182,6 +182,7 @@ + @@ -324,8 +325,6 @@ - - -- cgit