sync SMAPI context between players in multiplayer (#480)
This commit is contained in:
parent
688ee69ee6
commit
e5e4ce411c
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue