summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets8
-rw-r--r--src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs17
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeer.cs128
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs30
-rw-r--r--src/SMAPI/Framework/Networking/RemoteContextModModel.cs15
-rw-r--r--src/SMAPI/Framework/Networking/RemoteContextModel.cs24
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenClient.cs58
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenServer.cs36
-rw-r--r--src/SMAPI/Framework/SCore.cs5
-rw-r--r--src/SMAPI/Framework/SGame.cs11
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs246
-rw-r--r--src/SMAPI/IMultiplayerHelper.cs11
-rw-r--r--src/SMAPI/IMultiplayerPeer.cs41
-rw-r--r--src/SMAPI/IMultiplayerPeerMod.cs15
-rw-r--r--src/SMAPI/Patches/NetworkingPatch.cs103
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj9
16 files changed, 749 insertions, 8 deletions
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 @@
</Reference>
<!-- game DLLs -->
+ <Reference Include="GalaxyCSharp">
+ <HintPath>$(GamePath)\GalaxyCSharp.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="Lidgren.Network">
+ <HintPath>$(GamePath)\Lidgren.Network.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
<Reference Include="Netcode">
<HintPath>$(GamePath)\Netcode.dll</HintPath>
<Private Condition="'$(MSBuildProjectName)' != 'StardewModdingAPI.Tests'">False</Private>
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();
}
+
+ /// <summary>Get a connected player.</summary>
+ /// <param name="id">The player's unique ID.</param>
+ /// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
+ public IMultiplayerPeer GetConnectedPlayer(long id)
+ {
+ return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer)
+ ? peer
+ : null;
+ }
+
+ /// <summary>Get all connected players.</summary>
+ public IEnumerable<IMultiplayerPeer> 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
+{
+ /// <summary>Metadata about a connected player.</summary>
+ internal class MultiplayerPeer : IMultiplayerPeer
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The server through which to send messages, if this is an incoming farmhand.</summary>
+ private readonly SLidgrenServer Server;
+
+ /// <summary>The client through which to send messages, if this is the host player.</summary>
+ private readonly SLidgrenClient Client;
+
+ /// <summary>The network connection to the player.</summary>
+ private readonly NetConnection ServerConnection;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The player's unique ID.</summary>
+ public long PlayerID { get; }
+
+ /// <summary>Whether this is a connection to the host player.</summary>
+ public bool IsHostPlayer => this.PlayerID == Game1.MasterPlayer.UniqueMultiplayerID;
+
+ /// <summary>Whether the player has SMAPI installed.</summary>
+ public bool HasSmapi => this.ApiVersion != null;
+
+ /// <summary>The player's OS platform, if <see cref="HasSmapi"/> is true.</summary>
+ public GamePlatform? Platform { get; }
+
+ /// <summary>The installed version of Stardew Valley, if <see cref="HasSmapi"/> is true.</summary>
+ public ISemanticVersion GameVersion { get; }
+
+ /// <summary>The installed version of SMAPI, if <see cref="HasSmapi"/> is true.</summary>
+ public ISemanticVersion ApiVersion { get; }
+
+ /// <summary>The installed mods, if <see cref="HasSmapi"/> is true.</summary>
+ public IEnumerable<IMultiplayerPeerMod> Mods { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="playerID">The player's unique ID.</param>
+ /// <param name="model">The metadata to copy.</param>
+ /// <param name="server">The server through which to send messages.</param>
+ /// <param name="serverConnection">The server connection through which to send messages.</param>
+ /// <param name="client">The client through which to send messages.</param>
+ public MultiplayerPeer(long playerID, RemoteContextModel model, SLidgrenServer server, NetConnection serverConnection, SLidgrenClient client)
+ {
+ 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;
+ }
+
+ /// <summary>Construct an instance for a connection to an incoming farmhand.</summary>
+ /// <param name="playerID">The player's unique ID.</param>
+ /// <param name="model">The metadata to copy, if available.</param>
+ /// <param name="server">The server through which to send messages.</param>
+ /// <param name="serverConnection">The server connection through which to send messages.</param>
+ 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
+ );
+ }
+
+ /// <summary>Construct an instance for a connection to the host player.</summary>
+ /// <param name="playerID">The player's unique ID.</param>
+ /// <param name="model">The metadata to copy.</param>
+ /// <param name="client">The client through which to send messages.</param>
+ public static MultiplayerPeer ForConnectionToHost(long playerID, RemoteContextModel model, SLidgrenClient client)
+ {
+ return new MultiplayerPeer(
+ playerID: playerID,
+ model: model,
+ server: null,
+ serverConnection: null,
+ client: client
+ );
+ }
+
+ /// <summary>Get metadata for a mod installed by the player.</summary>
+ /// <param name="id">The unique mod ID.</param>
+ /// <returns>Returns the mod info, or <c>null</c> if the player doesn't have that mod.</returns>
+ 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));
+ }
+
+ /// <summary>Send a message to the given peer, bypassing the game's normal validation to allow messages before the connection is approved.</summary>
+ /// <param name="message">The message to send.</param>
+ 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
+ *********/
+ /// <summary>The mod's display name.</summary>
+ public string Name { get; }
+
+ /// <summary>The unique mod ID.</summary>
+ public string ID { get; }
+
+ /// <summary>The mod version.</summary>
+ public ISemanticVersion Version { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod metadata.</param>
+ 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
+{
+ /// <summary>Metadata about an installed mod exchanged with connected computers.</summary>
+ public class RemoteContextModModel
+ {
+ /// <summary>The mod's display name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>The unique mod ID.</summary>
+ public string ID { get; set; }
+
+ /// <summary>The mod version.</summary>
+ 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
+{
+ /// <summary>Metadata about the game, SMAPI, and installed mods exchanged with connected computers.</summary>
+ internal class RemoteContextModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether this player is the host player.</summary>
+ public bool IsHost { get; set; }
+
+ /// <summary>The game's platform version.</summary>
+ public GamePlatform Platform { get; set; }
+
+ /// <summary>The installed version of Stardew Valley.</summary>
+ public ISemanticVersion GameVersion { get; set; }
+
+ /// <summary>The installed version of SMAPI.</summary>
+ public ISemanticVersion ApiVersion { get; set; }
+
+ /// <summary>The installed mods.</summary>
+ 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
+{
+ /// <summary>A multiplayer client used to connect to a hosted server. This is an implementation of <see cref="LidgrenClient"/> that adds support for SMAPI's metadata context exchange.</summary>
+ internal class SLidgrenClient : LidgrenClient
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Get the metadata to include in a metadata message sent to other players.</summary>
+ private readonly Func<object[]> GetMetadataMessageFields;
+
+ /// <summary>The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed.</summary>
+ private readonly Func<SLidgrenClient, IncomingMessage, bool> TryProcessMessage;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="address">The remote address being connected.</param>
+ /// <param name="getMetadataMessageFields">Get the metadata to include in a metadata message sent to other players.</param>
+ /// <param name="tryProcessMessage">The method to call when receiving a custom SMAPI message from the server, which returns whether the message was processed..</param>
+ public SLidgrenClient(string address, Func<object[]> getMetadataMessageFields, Func<SLidgrenClient, IncomingMessage, bool> tryProcessMessage)
+ : base(address)
+ {
+ this.GetMetadataMessageFields = getMetadataMessageFields;
+ this.TryProcessMessage = tryProcessMessage;
+ }
+
+ /// <summary>Send the metadata needed to connect with a remote server.</summary>
+ 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
+ *********/
+ /// <summary>Process an incoming network message.</summary>
+ /// <param name="message">The message to process.</param>
+ 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
+{
+ /// <summary>A multiplayer server used to connect to an incoming player. This is an implementation of <see cref="LidgrenServer"/> that adds support for SMAPI's metadata context exchange.</summary>
+ internal class SLidgrenServer : LidgrenServer
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>A method which sends a message through a specific connection.</summary>
+ private readonly MethodInfo SendMessageToConnectionMethod;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="gameServer">The underlying game server.</param>
+ 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);
+ }
+
+ /// <summary>Send a message to a remote server.</summary>
+ /// <param name="connection">The network connection.</param>
+ /// <param name="message">The message to send.</param>
+ 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
/// <param name="monitorForGame">Encapsulates monitoring and logging on the game's behalf.</param>
/// <param name="reflection">Simplifies access to private game code.</param>
/// <param name="eventManager">Manages SMAPI events for mods.</param>
+ /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
+ /// <param name="modRegistry">Tracks the installed mods.</param>
/// <param name="onGameInitialised">A callback to invoke after the game finishes initialising.</param>
/// <param name="onGameExiting">A callback to invoke when the game exits.</param>
- internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, 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
- ****/
/// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary>
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
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
+ /// <summary>Tracks the installed mods.</summary>
+ private readonly ModRegistry ModRegistry;
+
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ private readonly JsonHelper JsonHelper;
+
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
/// <summary>Manages SMAPI events.</summary>
private readonly EventManager EventManager;
+ /// <summary>The players who are currently disconnecting.</summary>
+ private readonly IList<long> DisconnectingFarmers;
+
+ /// <summary>Whether SMAPI should log more detailed information.</summary>
+ private readonly bool VerboseLogging;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The message ID for a SMAPI message containing context about a player.</summary>
+ public const byte ContextSyncMessageID = 255;
+
+ /// <summary>The metadata for each connected peer.</summary>
+ public IDictionary<long, MultiplayerPeer> Peers { get; } = new Dictionary<long, MultiplayerPeer>();
+
/*********
** Public methods
@@ -22,10 +55,20 @@ namespace StardewModdingAPI.Framework
/// <summary>Construct an instance.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="eventManager">Manages SMAPI events.</param>
- public SMultiplayer(IMonitor monitor, EventManager eventManager)
+ /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
+ /// <param name="modRegistry">Tracks the installed mods.</param>
+ /// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="verboseLogging">Whether SMAPI should log more detailed information.</param>
+ public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, bool verboseLogging)
{
this.Monitor = monitor;
this.EventManager = eventManager;
+ this.JsonHelper = jsonHelper;
+ this.ModRegistry = modRegistry;
+ this.Reflection = reflection;
+ this.VerboseLogging = verboseLogging;
+
+ this.DisconnectingFarmers = reflection.GetField<List<long>>(this, "disconnectingFarmers").GetValue();
}
/// <summary>Handle sync messages from other players and perform other initial sync logic.</summary>
@@ -43,5 +86,206 @@ namespace StardewModdingAPI.Framework
base.UpdateLate(forceSync);
this.EventManager.Legacy_AfterMainBroadcast.Raise();
}
+
+ /// <summary>Initialise a client before the game connects to a remote server.</summary>
+ /// <param name="client">The client to initialise.</param>
+ public override Client InitClient(Client client)
+ {
+ if (client is LidgrenClient)
+ {
+ string address = this.Reflection.GetField<string>(client, "address").GetValue();
+ return new SLidgrenClient(address, this.GetContextSyncMessageFields, this.TryProcessMessageFromServer);
+ }
+
+ return client;
+ }
+
+ /// <summary>Initialise a server before the game connects to an incoming player.</summary>
+ /// <param name="server">The server to initialise.</param>
+ public override Server InitServer(Server server)
+ {
+ if (server is LidgrenServer)
+ {
+ IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
+ return new SLidgrenServer(gameServer);
+ }
+
+ return server;
+ }
+
+ /// <summary>Process an incoming network message from an unknown farmhand.</summary>
+ /// <param name="server">The server instance that received the connection.</param>
+ /// <param name="rawMessage">The raw network message that was received.</param>
+ /// <param name="message">The message to process.</param>
+ 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<RemoteContextModel>(data);
+ if (model.ApiVersion == null)
+ model = null; // no data available for unmodded players
+
+ // log info
+ if (model != null)
+ this.Monitor.Log($"Received context for farmhand {message.FarmerID} running SMAPI {model.ApiVersion} with {model.Mods.Length} mods{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace);
+ else
+ this.Monitor.Log($"Received context for farmhand {message.FarmerID} running vanilla{(this.VerboseLogging ? $": {data}" : "")}.", LogLevel.Trace);
+
+ // store peer
+ MultiplayerPeer newPeer = this.Peers[message.FarmerID] = MultiplayerPeer.ForConnectionToFarmhand(message.FarmerID, model, customServer, rawMessage.SenderConnection);
+
+ // reply with known contexts
+ if (this.VerboseLogging)
+ this.Monitor.Log(" Replying with context for current player...", LogLevel.Trace);
+ newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));
+ foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
+ {
+ if (this.VerboseLogging)
+ this.Monitor.Log($" Replying with context for player {otherPeer.PlayerID}...", LogLevel.Trace);
+ newPeer.SendMessage(new OutgoingMessage(SMultiplayer.ContextSyncMessageID, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer)));
+ }
+
+ // 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);
+ }
+ }
+
+ /// <summary>Process an incoming network message from the server.</summary>
+ /// <param name="client">The client instance that received the connection.</param>
+ /// <param name="message">The message to process.</param>
+ /// <returns>Returns whether the message was handled.</returns>
+ 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<RemoteContextModel>(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;
+ }
+
+ /// <summary>Remove players who are disconnecting.</summary>
+ 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
+ *********/
+ /// <summary>Get the fields to include in a context sync message sent to other players.</summary>
+ 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) };
+ }
+
+ /// <summary>Get the fields to include in a context sync message sent to other players.</summary>
+ /// <param name="peer">The peer whose data to represent.</param>
+ 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
/// <summary>Get the locations which are being actively synced from the host.</summary>
IEnumerable<GameLocation> GetActiveLocations();
+
+ /* disable until ready for release:
+
+ /// <summary>Get a connected player.</summary>
+ /// <param name="id">The player's unique ID.</param>
+ /// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
+ IMultiplayerPeer GetConnectedPlayer(long id);
+
+ /// <summary>Get all connected players.</summary>
+ IEnumerable<IMultiplayerPeer> 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
+{
+ /// <summary>Metadata about a connected player.</summary>
+ public interface IMultiplayerPeer
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The player's unique ID.</summary>
+ long PlayerID { get; }
+
+ /// <summary>Whether this is a connection to the host player.</summary>
+ bool IsHostPlayer { get; }
+
+ /// <summary>Whether the player has SMAPI installed.</summary>
+ bool HasSmapi { get; }
+
+ /// <summary>The player's OS platform, if <see cref="HasSmapi"/> is true.</summary>
+ GamePlatform? Platform { get; }
+
+ /// <summary>The installed version of Stardew Valley, if <see cref="HasSmapi"/> is true.</summary>
+ ISemanticVersion GameVersion { get; }
+
+ /// <summary>The installed version of SMAPI, if <see cref="HasSmapi"/> is true.</summary>
+ ISemanticVersion ApiVersion { get; }
+
+ /// <summary>The installed mods, if <see cref="HasSmapi"/> is true.</summary>
+ IEnumerable<IMultiplayerPeerMod> Mods { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get metadata for a mod installed by the player.</summary>
+ /// <param name="id">The unique mod ID.</param>
+ /// <returns>Returns the mod info, or <c>null</c> if the player doesn't have that mod.</returns>
+ 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
+{
+ /// <summary>Metadata about a mod installed by a connected player.</summary>
+ public interface IMultiplayerPeerMod
+ {
+ /// <summary>The mod's display name.</summary>
+ string Name { get; }
+
+ /// <summary>The unique mod ID.</summary>
+ string ID { get; }
+
+ /// <summary>The mod version.</summary>
+ 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
+{
+ /// <summary>A Harmony patch to enable the SMAPI multiplayer metadata handshake.</summary>
+ internal class NetworkingPatch : IHarmonyPatch
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The constructor for the internal <c>NetBufferReadStream</c> type.</summary>
+ private static readonly ConstructorInfo NetBufferReadStreamConstructor = NetworkingPatch.GetNetBufferReadStreamConstructor();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A unique name for this patch.</summary>
+ public string Name => $"{nameof(NetworkingPatch)}";
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Apply the Harmony patch.</summary>
+ /// <param name="harmony">The Harmony instance.</param>
+ 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
+ *********/
+ /// <summary>The method to call instead of the <see cref="LidgrenServer.parseDataMessageFromClient"/> method.</summary>
+ /// <param name="__instance">The instance being patched.</param>
+ /// <param name="dataMsg">The raw network message to parse.</param>
+ /// <param name="___peers">The private <c>peers</c> field on the <paramref name="__instance"/> instance.</param>
+ /// <param name="___gameServer">The private <c>gameServer</c> field on the <paramref name="__instance"/> instance.</param>
+ /// <returns>Returns whether to execute the original method.</returns>
+ /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")]
+ private static bool Prefix_LidgrenServer_ParseDataMessageFromClient(LidgrenServer __instance, NetIncomingMessage dataMsg, Bimap<long, NetConnection> ___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;
+ }
+
+ /// <summary>Get the constructor for the internal <c>NetBufferReadStream</c> type.</summary>
+ 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 @@
<Compile Include="Framework\Events\ModDisplayEvents.cs" />
<Compile Include="Framework\Events\ModSpecialisedEvents.cs" />
<Compile Include="Framework\ModHelpers\DataHelper.cs" />
+ <Compile Include="Framework\Networking\MultiplayerPeer.cs" />
+ <Compile Include="Framework\Networking\MultiplayerPeerMod.cs" />
+ <Compile Include="Framework\Networking\RemoteContextModel.cs" />
+ <Compile Include="Framework\Networking\RemoteContextModModel.cs" />
+ <Compile Include="Framework\Networking\SLidgrenClient.cs" />
+ <Compile Include="Framework\Networking\SLidgrenServer.cs" />
<Compile Include="Framework\SCore.cs" />
<Compile Include="Framework\SGameConstructorHack.cs" />
<Compile Include="Framework\ContentManagers\BaseContentManager.cs" />
@@ -243,9 +249,11 @@
<Compile Include="IContentPack.cs" />
<Compile Include="IModInfo.cs" />
<Compile Include="IMultiplayerHelper.cs" />
+ <Compile Include="IMultiplayerPeer.cs" />
<Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" />
<Compile Include="IReflectedProperty.cs" />
+ <Compile Include="IMultiplayerPeerMod.cs" />
<Compile Include="Metadata\CoreAssetPropagator.cs" />
<Compile Include="ContentSource.cs" />
<Compile Include="Framework\Content\AssetInfo.cs" />
@@ -310,6 +318,7 @@
<Compile Include="Metadata\InstructionMetadata.cs" />
<Compile Include="Mod.cs" />
<Compile Include="Patches\DialogueErrorPatch.cs" />
+ <Compile Include="Patches\NetworkingPatch.cs" />
<Compile Include="PatchMode.cs" />
<Compile Include="GamePlatform.cs" />
<Compile Include="Program.cs" />