From 826dd53ab550e5b92796c510569118beee6bd044 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Aug 2018 18:28:16 -0400 Subject: move most SMAPI files into subfolder (#582) --- build/common.targets | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) (limited to 'build/common.targets') diff --git a/build/common.targets b/build/common.targets index 5b6511f8..90c477b6 100644 --- a/build/common.targets +++ b/build/common.targets @@ -99,14 +99,14 @@ - - - - - - + + + + + + @@ -114,12 +114,14 @@ - - + + + - - + + + -- cgit From b5adfd8bce12afb885d6bb6a347d25e33be602af Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Aug 2018 20:42:31 -0400 Subject: add more default game install paths --- build/common.targets | 7 +++++++ docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 8 ++++++-- src/SMAPI.ModBuildConfig/build/smapi.targets | 4 ++++ 4 files changed, 18 insertions(+), 2 deletions(-) (limited to 'build/common.targets') diff --git a/build/common.targets b/build/common.targets index 90c477b6..b5cbbe67 100644 --- a/build/common.targets +++ b/build/common.targets @@ -9,13 +9,20 @@ $(HOME)/GOG Games/Stardew Valley/game $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley $(HOME)/.steam/steam/steamapps/common/Stardew Valley + /Applications/Stardew Valley.app/Contents/MacOS $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files\GalaxyClient\Games\Stardew Valley + C:\Program Files\GOG Galaxy\Games\Stardew Valley + C:\Program Files\Steam\steamapps\common\Stardew Valley + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) diff --git a/docs/release-notes.md b/docs/release-notes.md index 12feb68a..0ff90aa3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Moved most SMAPI files into a `smapi-internal` subfolder. * Moved save backups into a `save-backups` subfolder (instead of `Mods/SaveBackup/backups`). Note that previous backups will be deleted when you update. * Update checks now work even when the mod has no update keys in most cases. + * Fixed some game install paths not detected on Windows. * Fixed installer duplicating bundled mods if you moved them after the last install. * Fixed crash when a mod manifest is corrupted. * Fixed error-handling when initialising paths. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 7f7acb0b..e6e71cf4 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -55,8 +55,12 @@ namespace StardewModdingApi.Installer case Platform.Windows: { // Windows - yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley"; - yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"; + foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) + { + yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; + yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley"; + yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; + } // Windows registry IDictionary registryKeys = new Dictionary diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 7bdd8f8f..99011629 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -42,6 +42,10 @@ + C:\Program Files\GalaxyClient\Games\Stardew Valley + C:\Program Files\GOG Galaxy\Games\Stardew Valley + C:\Program Files\Steam\steamapps\common\Stardew Valley + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley -- 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 'build/common.targets') 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 6bf3734e4ac3efd4a7c32601c81878e38cfbeac0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Nov 2018 00:34:16 -0500 Subject: fix build on Linux/Mac --- build/common.targets | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'build/common.targets') diff --git a/build/common.targets b/build/common.targets index e646e62c..f631633d 100644 --- a/build/common.targets +++ b/build/common.targets @@ -54,7 +54,7 @@ False - + $(GamePath)\GalaxyCSharp.dll @@ -87,8 +87,12 @@ False False - + + + $(GamePath)\Lidgren.Network.dll + False + $(GamePath)\StardewValley.exe False -- cgit From fb0d101ab822145fb70dbfbdc261f6596b160a4f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Nov 2018 18:46:40 -0500 Subject: fix build error on Linux/Mac (#480) --- build/common.targets | 113 ++++++++++++++++++++++----------------------------- 1 file changed, 49 insertions(+), 64 deletions(-) (limited to 'build/common.targets') diff --git a/build/common.targets b/build/common.targets index f631633d..320b3e00 100644 --- a/build/common.targets +++ b/build/common.targets @@ -38,72 +38,57 @@ - - - - - - False - - - False - - - False - - - False - + + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\Netcode.dll + False + + + False + + + False + + + False + + + False + + - - - $(GamePath)\GalaxyCSharp.dll - False - - - $(GamePath)\Lidgren.Network.dll - False - - - $(GamePath)\Netcode.dll - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - - - $(GamePath)\MonoGame.Framework.dll - False - False - + + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\MonoGame.Framework.dll + False + + - - - $(GamePath)\Lidgren.Network.dll - False - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - + + + + $(GamePath)\GalaxyCSharp.dll + False + + + $(GamePath)\Lidgren.Network.dll + False + + + $(GamePath)\xTile.dll + False + + -- cgit From e8fe550b753ced742c5084dbf851fc606ff90fb0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Nov 2018 01:00:11 -0500 Subject: remove 0Harmony.pdb from build scripts (#602) The file isn't included in the new Harmony NuGet package. --- build/common.targets | 1 - build/prepare-install-package.targets | 1 - 2 files changed, 2 deletions(-) (limited to 'build/common.targets') diff --git a/build/common.targets b/build/common.targets index 320b3e00..d9ad89f4 100644 --- a/build/common.targets +++ b/build/common.targets @@ -108,7 +108,6 @@ - diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 0b575d6f..5ba6ed37 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -38,7 +38,6 @@ - -- cgit