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) --- src/SMAPI/Framework/SMultiplayer.cs | 246 +++++++++++++++++++++++++++++++++++- 1 file changed, 245 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') 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) }; + } } } -- cgit From 02a46bf13f29ce0dd8ac2f422113083c59dae42d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 3 Nov 2018 01:29:01 -0400 Subject: add APIs to send/receive messages in multiplayer (#480) --- src/SMAPI/Framework/SMultiplayer.cs | 339 +++++++++++++++++++++++++++--------- 1 file changed, 257 insertions(+), 82 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') 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); + } } } -- 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/Framework/SMultiplayer.cs | 226 +++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 96 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') 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))); } } } -- 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/Framework/SMultiplayer.cs | 50 ++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Framework/SMultiplayer.cs') 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; -