summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-11-04 23:18:55 -0500
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-11-04 23:18:55 -0500
commite8276166c3e8d1a0b3a976ef29a00f8e1569cc72 (patch)
treea78c44e929ff1de70d20b012385c418aea7e78a6 /src
parent688ee69ee64e03aee7a693e6c15092daf229ac5e (diff)
parentb4a5b3829f0f738e5b7e05048068eaec9d2d01d1 (diff)
downloadSMAPI-e8276166c3e8d1a0b3a976ef29a00f8e1569cc72.tar.gz
SMAPI-e8276166c3e8d1a0b3a976ef29a00f8e1569cc72.tar.bz2
SMAPI-e8276166c3e8d1a0b3a976ef29a00f8e1569cc72.zip
Merge branch 'add-multiplayer-sync' into develop
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json7
-rw-r--r--src/SMAPI/Events/IModEvents.cs3
-rw-r--r--src/SMAPI/Events/IMultiplayerEvents.cs17
-rw-r--r--src/SMAPI/Events/ModMessageReceivedEventArgs.cs46
-rw-r--r--src/SMAPI/Events/PeerContextReceivedEventArgs.cs25
-rw-r--r--src/SMAPI/Events/PeerDisconnectedEventArgs.cs25
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs16
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs24
-rw-r--r--src/SMAPI/Framework/Events/ManagedEventBase.cs12
-rw-r--r--src/SMAPI/Framework/Events/ModEvents.cs4
-rw-r--r--src/SMAPI/Framework/Events/ModMultiplayerEvents.cs43
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs4
-rw-r--r--src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs42
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs9
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs2
-rw-r--r--src/SMAPI/Framework/ModRegistry.cs2
-rw-r--r--src/SMAPI/Framework/Networking/MessageType.cs26
-rw-r--r--src/SMAPI/Framework/Networking/ModMessageModel.cs72
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeer.cs132
-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.cs49
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenServer.cs148
-rw-r--r--src/SMAPI/Framework/SCore.cs14
-rw-r--r--src/SMAPI/Framework/SGame.cs32
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs487
-rw-r--r--src/SMAPI/IMultiplayerHelper.cs18
-rw-r--r--src/SMAPI/IMultiplayerPeer.cs41
-rw-r--r--src/SMAPI/IMultiplayerPeerMod.cs15
-rw-r--r--src/SMAPI/Patches/LidgrenServerPatch.cs89
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj34
32 files changed, 1474 insertions, 33 deletions
diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
index 541dcd91..6f8d9c40 100644
--- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
@@ -245,7 +245,12 @@
"Move Faster": {
"ID": "shuaiz.MoveFasterMod",
- "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?)
+ "~1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?)
+ },
+
+ "MTN": {
+ "ID": "SgtPickles.MTN",
+ "~1.2.5 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API.
},
"Multiple Sprites and Portraits On Rotation (File Loading)": {
diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs
index 76da7751..bd7ab880 100644
--- a/src/SMAPI/Events/IModEvents.cs
+++ b/src/SMAPI/Events/IModEvents.cs
@@ -12,6 +12,9 @@ namespace StardewModdingAPI.Events
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
IInputEvents Input { get; }
+ /// <summary>Events raised for multiplayer messages and connections.</summary>
+ IMultiplayerEvents Multiplayer { get; }
+
/// <summary>Events raised when the player data changes.</summary>
IPlayerEvents Player { get; }
diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs
new file mode 100644
index 00000000..4a31f48e
--- /dev/null
+++ b/src/SMAPI/Events/IMultiplayerEvents.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Events raised for multiplayer messages and connections.</summary>
+ public interface IMultiplayerEvents
+ {
+ /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
+ event EventHandler<PeerContextReceivedEventArgs> PeerContextReceived;
+
+ /// <summary>Raised after a mod message is received over the network.</summary>
+ event EventHandler<ModMessageReceivedEventArgs> ModMessageReceived;
+
+ /// <summary>Raised after the connection with a peer is severed.</summary>
+ event EventHandler<PeerDisconnectedEventArgs> PeerDisconnected;
+ }
+}
diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs
new file mode 100644
index 00000000..49366ec6
--- /dev/null
+++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs
@@ -0,0 +1,46 @@
+using System;
+using StardewModdingAPI.Framework.Networking;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for an <see cref="IMultiplayerEvents.ModMessageReceived"/> event.</summary>
+ public class ModMessageReceivedEventArgs : EventArgs
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying message model.</summary>
+ private readonly ModMessageModel Message;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The unique ID of the player from whose computer the message was sent.</summary>
+ public long FromPlayerID => this.Message.FromPlayerID;
+
+ /// <summary>The unique ID of the mod which sent the message.</summary>
+ public string FromModID => this.Message.FromModID;
+
+ /// <summary>A message type which can be used to decide whether it's the one you want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, so mods should check the <see cref="FromModID"/>.</summary>
+ public string Type => this.Message.Type;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="message">The received message.</param>
+ internal ModMessageReceivedEventArgs(ModMessageModel message)
+ {
+ this.Message = message;
+ }
+
+ /// <summary>Read the message data into the given model type.</summary>
+ /// <typeparam name="TModel">The message model type.</typeparam>
+ public TModel ReadAs<TModel>()
+ {
+ return this.Message.Data.ToObject<TModel>();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/PeerContextReceivedEventArgs.cs b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs
new file mode 100644
index 00000000..151a295c
--- /dev/null
+++ b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for an <see cref="IMultiplayerEvents.PeerContextReceived"/> event.</summary>
+ public class PeerContextReceivedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The peer whose metadata was received.</summary>
+ public IMultiplayerPeer Peer { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="peer">The peer whose metadata was received.</param>
+ internal PeerContextReceivedEventArgs(IMultiplayerPeer peer)
+ {
+ this.Peer = peer;
+ }
+ }
+}
diff --git a/src/SMAPI/Events/PeerDisconnectedEventArgs.cs b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs
new file mode 100644
index 00000000..8517988a
--- /dev/null
+++ b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for an <see cref="IMultiplayerEvents.PeerDisconnected"/> event.</summary>
+ public class PeerDisconnectedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The peer who disconnected.</summary>
+ public IMultiplayerPeer Peer { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="peer">The peer who disconnected.</param>
+ internal PeerDisconnectedEventArgs(IMultiplayerPeer peer)
+ {
+ this.Peer = peer;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index 31b0346a..b9d1c453 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -99,6 +99,18 @@ namespace StardewModdingAPI.Framework.Events
public readonly ManagedEvent<MouseWheelScrolledEventArgs> MouseWheelScrolled;
/****
+ ** Multiplayer
+ ****/
+ /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
+ public readonly ManagedEvent<PeerContextReceivedEventArgs> PeerContextReceived;
+
+ /// <summary>Raised after a mod message is received over the network.</summary>
+ public readonly ManagedEvent<ModMessageReceivedEventArgs> ModMessageReceived;
+
+ /// <summary>Raised after the connection with a peer is severed.</summary>
+ public readonly ManagedEvent<PeerDisconnectedEventArgs> PeerDisconnected;
+
+ /****
** Player
****/
/// <summary>Raised after items are added or removed to a player's inventory.</summary>
@@ -374,6 +386,10 @@ namespace StardewModdingAPI.Framework.Events
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved));
this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled));
+ this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived));
+ this.ModMessageReceived = ManageEventOf<ModMessageReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived));
+ this.PeerDisconnected = ManageEventOf<PeerDisconnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected));
+
this.InventoryChanged = ManageEventOf<InventoryChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged));
this.LevelChanged = ManageEventOf<LevelChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged));
this.Warped = ManageEventOf<WarpedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped));
diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs
index c1ebf6c7..65f6e38e 100644
--- a/src/SMAPI/Framework/Events/ManagedEvent.cs
+++ b/src/SMAPI/Framework/Events/ManagedEvent.cs
@@ -67,6 +67,30 @@ namespace StardewModdingAPI.Framework.Events
}
}
}
+
+ /// <summary>Raise the event and notify all handlers.</summary>
+ /// <param name="args">The event arguments to pass.</param>
+ /// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param>
+ public void RaiseForMods(TEventArgs args, Func<IModMetadata, bool> match)
+ {
+ if (this.Event == null)
+ return;
+
+ foreach (EventHandler<TEventArgs> handler in this.CachedInvocationList)
+ {
+ if (match(this.GetSourceMod(handler)))
+ {
+ try
+ {
+ handler.Invoke(null, args);
+ }
+ catch (Exception ex)
+ {
+ this.LogError(handler, ex);
+ }
+ }
+ }
+ }
}
/// <summary>An event wrapper which intercepts and logs errors in handler code.</summary>
diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs
index f3a278dc..defd903a 100644
--- a/src/SMAPI/Framework/Events/ManagedEventBase.cs
+++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs
@@ -69,12 +69,22 @@ namespace StardewModdingAPI.Framework.Events
this.SourceMods.Remove(handler);
}
+ /// <summary>Get the mod which registered the given event handler, if available.</summary>
+ /// <param name="handler">The event handler.</param>
+ protected IModMetadata GetSourceMod(TEventHandler handler)
+ {
+ return this.SourceMods.TryGetValue(handler, out IModMetadata mod)
+ ? mod
+ : null;
+ }
+
/// <summary>Log an exception from an event handler.</summary>
/// <param name="handler">The event handler instance.</param>
/// <param name="ex">The exception that was raised.</param>
protected void LogError(TEventHandler handler, Exception ex)
{
- if (this.SourceMods.TryGetValue(handler, out IModMetadata mod))
+ IModMetadata mod = this.GetSourceMod(handler);
+ if (mod != null)
mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error);
else
this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error);
diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs
index 7a318e8b..8ad3936c 100644
--- a/src/SMAPI/Framework/Events/ModEvents.cs
+++ b/src/SMAPI/Framework/Events/ModEvents.cs
@@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
public IInputEvents Input { get; }
+ /// <summary>Events raised for multiplayer messages and connections.</summary>
+ public IMultiplayerEvents Multiplayer { get; }
+
/// <summary>Events raised when the player data changes.</summary>
public IPlayerEvents Player { get; }
@@ -38,6 +41,7 @@ namespace StardewModdingAPI.Framework.Events
this.Display = new ModDisplayEvents(mod, eventManager);
this.GameLoop = new ModGameLoopEvents(mod, eventManager);
this.Input = new ModInputEvents(mod, eventManager);
+ this.Multiplayer = new ModMultiplayerEvents(mod, eventManager);
this.Player = new ModPlayerEvents(mod, eventManager);
this.World = new ModWorldEvents(mod, eventManager);
this.Specialised = new ModSpecialisedEvents(mod, eventManager);
diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
new file mode 100644
index 00000000..152c4e0c
--- /dev/null
+++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
@@ -0,0 +1,43 @@
+using System;
+using StardewModdingAPI.Events;
+
+namespace StardewModdingAPI.Framework.Events
+{
+ /// <summary>Events raised for multiplayer messages and connections.</summary>
+ internal class ModMultiplayerEvents : ModEventsBase, IMultiplayerEvents
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
+ public event EventHandler<PeerContextReceivedEventArgs> PeerContextReceived
+ {
+ add => this.EventManager.PeerContextReceived.Add(value);
+ remove => this.EventManager.PeerContextReceived.Remove(value);
+ }
+
+ /// <summary>Raised after a mod message is received over the network.</summary>
+ public event EventHandler<ModMessageReceivedEventArgs> ModMessageReceived
+ {
+ add => this.EventManager.ModMessageReceived.Add(value);
+ remove => this.EventManager.ModMessageReceived.Remove(value);
+ }
+
+ /// <summary>Raised after the connection with a peer is severed.</summary>
+ public event EventHandler<PeerDisconnectedEventArgs> PeerDisconnected
+ {
+ add => this.EventManager.PeerDisconnected.Add(value);
+ remove => this.EventManager.PeerDisconnected.Remove(value);
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod which uses this instance.</param>
+ /// <param name="eventManager">The underlying event manager.</param>
+ internal ModMultiplayerEvents(IModMetadata mod, EventManager eventManager)
+ : base(mod, eventManager) { }
+ }
+}
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index bda9429f..7ada7dea 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -88,6 +88,10 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded).</summary>
bool HasID();
+ /// <summary>Whether the mod has the given ID.</summary>
+ /// <param name="id">The mod ID to check.</param>
+ bool HasID(string id);
+
/// <summary>Get the defined update keys.</summary>
/// <param name="validOnly">Only return valid update keys.</param>
IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = true);
diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
index c449a51b..eedad0bc 100644
--- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
@@ -1,4 +1,6 @@
+using System;
using System.Collections.Generic;
+using StardewModdingAPI.Framework.Networking;
using StardewValley;
namespace StardewModdingAPI.Framework.ModHelpers
@@ -25,16 +27,50 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.Multiplayer = multiplayer;
}
+ /// <summary>Get a new multiplayer ID.</summary>
+ public long GetNewID()
+ {
+ return this.Multiplayer.getNewID();
+ }
+
/// <summary>Get the locations which are being actively synced from the host.</summary>
public IEnumerable<GameLocation> GetActiveLocations()
{
return this.Multiplayer.activeLocations();
}
- /// <summary>Get a new multiplayer ID.</summary>
- public long 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.getNewID();
+ 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;
+ }
+
+ /// <summary>Send a message to mods installed by connected players.</summary>
+ /// <typeparam name="TMessage">The data type. This can be a class with a default constructor, or a value type.</typeparam>
+ /// <param name="message">The data to send over the network.</param>
+ /// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
+ /// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
+ /// <param name="playerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
+ public void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null)
+ {
+ this.Multiplayer.BroadcastModMessage(
+ message: message,
+ messageType: messageType,
+ fromModID: this.ModID,
+ toModIDs: modIDs,
+ toPlayerIDs: playerIDs
+ );
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index 04aa679b..0cb62a75 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -153,6 +153,15 @@ namespace StardewModdingAPI.Framework.ModLoading
&& !string.IsNullOrWhiteSpace(this.Manifest.UniqueID);
}
+ /// <summary>Whether the mod has the given ID.</summary>
+ /// <param name="id">The mod ID to check.</param>
+ public bool HasID(string id)
+ {
+ return
+ this.HasID()
+ && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.InvariantCultureIgnoreCase);
+ }
+
/// <summary>Get the defined update keys.</summary>
/// <param name="validOnly">Only return valid update keys.</param>
public IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = false)
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 9992cc78..3ff70d64 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -379,7 +379,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="loadedMods">The loaded mods.</param>
private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods)
{
- IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase));
+ IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => m.HasID(id));
// yield dependencies
if (manifest.Dependencies != null)
diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs
index e7d4f89a..da68fce3 100644
--- a/src/SMAPI/Framework/ModRegistry.cs
+++ b/src/SMAPI/Framework/ModRegistry.cs
@@ -59,7 +59,7 @@ namespace StardewModdingAPI.Framework
uniqueID = uniqueID.Trim();
// find match
- return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
+ return this.GetAll().FirstOrDefault(p => p.HasID(uniqueID));
}
/// <summary>Get the mod metadata from one of its assemblies.</summary>
diff --git a/src/SMAPI/Framework/Networking/MessageType.cs b/src/SMAPI/Framework/Networking/MessageType.cs
new file mode 100644
index 00000000..bd9acfa9
--- /dev/null
+++ b/src/SMAPI/Framework/Networking/MessageType.cs
@@ -0,0 +1,26 @@
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.Networking
+{
+ /// <summary>Network message types recognised by SMAPI and Stardew Valley.</summary>
+ internal enum MessageType : byte
+ {
+ /*********
+ ** SMAPI
+ *********/
+ /// <summary>A data message intended for mods to consume.</summary>
+ ModMessage = 254,
+
+ /// <summary>Metadata context about a player synced by SMAPI.</summary>
+ ModContext = 255,
+
+ /*********
+ ** Vanilla
+ *********/
+ /// <summary>Metadata about the host server sent to a farmhand.</summary>
+ ServerIntroduction = Multiplayer.serverIntroduction,
+
+ /// <summary>Metadata about a player sent to a farmhand or server.</summary>
+ PlayerIntroduction = Multiplayer.playerIntroduction
+ }
+}
diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs
new file mode 100644
index 00000000..7ee39863
--- /dev/null
+++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs
@@ -0,0 +1,72 @@
+using System.Linq;
+using Newtonsoft.Json.Linq;
+
+namespace StardewModdingAPI.Framework.Networking
+{
+ /// <summary>The metadata for a mod message.</summary>
+ internal class ModMessageModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /****
+ ** Origin
+ ****/
+ /// <summary>The unique ID of the player who broadcast the message.</summary>
+ public long FromPlayerID { get; set; }
+
+ /// <summary>The unique ID of the mod which broadcast the message.</summary>
+ public string FromModID { get; set; }
+
+ /****
+ ** Destination
+ ****/
+ /// <summary>The players who should receive the message, or <c>null</c> for all players.</summary>
+ public long[] ToPlayerIDs { get; set; }
+
+ /// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary>
+ public string[] ToModIDs { get; set; }
+
+ /// <summary>A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</summary>
+ public string Type { get; set; }
+
+ /// <summary>The custom mod data being broadcast.</summary>
+ public JToken Data { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public ModMessageModel() { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fromPlayerID">The unique ID of the player who broadcast the message.</param>
+ /// <param name="fromModID">The unique ID of the mod which broadcast the message.</param>
+ /// <param name="toPlayerIDs">The players who should receive the message, or <c>null</c> for all players.</param>
+ /// <param name="toModIDs">The mods which should receive the message, or <c>null</c> for all mods.</param>
+ /// <param name="type">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
+ /// <param name="data">The custom mod data being broadcast.</param>
+ public ModMessageModel(long fromPlayerID, string fromModID, long[] toPlayerIDs, string[] toModIDs, string type, JToken data)
+ {
+ this.FromPlayerID = fromPlayerID;
+ this.FromModID = fromModID;
+ this.ToPlayerIDs = toPlayerIDs;
+ this.ToModIDs = toModIDs;
+ this.Type = type;
+ this.Data = data;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="message">The message to clone.</param>
+ public ModMessageModel(ModMessageModel message)
+ {
+ this.FromPlayerID = message.FromPlayerID;
+ this.FromModID = message.FromModID;
+ this.ToPlayerIDs = message.ToPlayerIDs?.ToArray();
+ this.ToModIDs = message.ToModIDs?.ToArray();
+ this.Type = message.Type;
+ this.Data = message.Data;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs
new file mode 100644
index 00000000..e703dbb1
--- /dev/null
+++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Lidgren.Network;
+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;
+