summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs49
-rw-r--r--src/SMAPI/Framework/DeprecationWarning.cs38
-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.cs8
-rw-r--r--src/SMAPI/Framework/ModRegistry.cs2
-rw-r--r--src/SMAPI/Framework/Monitor.cs15
-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.cs85
-rw-r--r--src/SMAPI/Framework/SGame.cs56
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs474
24 files changed, 1290 insertions, 87 deletions
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index 7a824a05..0fde67ee 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
namespace StardewModdingAPI.Framework
{
@@ -18,6 +19,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Tracks the installed mods.</summary>
private readonly ModRegistry ModRegistry;
+ /// <summary>The queued deprecation warnings to display.</summary>
+ private readonly IList<DeprecationWarning> QueuedWarnings = new List<DeprecationWarning>();
+
/*********
** Public methods
@@ -51,29 +55,40 @@ namespace StardewModdingAPI.Framework
if (!this.MarkWarned(source ?? "<unknown>", nounPhrase, version))
return;
- // build message
- string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase} is deprecated since SMAPI {version}).";
- if (source == null)
- message += $"{Environment.NewLine}{Environment.StackTrace}";
+ // queue warning
+ this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity));
+ }
- // log message
- switch (severity)
+ /// <summary>Print any queued messages.</summary>
+ public void PrintQueued()
+ {
+ foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase))
{
- case DeprecationLevel.Notice:
- this.Monitor.Log(message, LogLevel.Trace);
- break;
+ // build message
+ string message = $"{warning.ModName ?? "An unknown mod"} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version}).";
+ if (warning.ModName == null)
+ message += $"{Environment.NewLine}{Environment.StackTrace}";
+
+ // log message
+ switch (warning.Level)
+ {
+ case DeprecationLevel.Notice:
+ this.Monitor.Log(message, LogLevel.Trace);
+ break;
- case DeprecationLevel.Info:
- this.Monitor.Log(message, LogLevel.Debug);
- break;
+ case DeprecationLevel.Info:
+ this.Monitor.Log(message, LogLevel.Debug);
+ break;
- case DeprecationLevel.PendingRemoval:
- this.Monitor.Log(message, LogLevel.Warn);
- break;
+ case DeprecationLevel.PendingRemoval:
+ this.Monitor.Log(message, LogLevel.Warn);
+ break;
- default:
- throw new NotSupportedException($"Unknown deprecation level '{severity}'");
+ default:
+ throw new NotSupportedException($"Unknown deprecation level '{warning.Level}'.");
+ }
}
+ this.QueuedWarnings.Clear();
}
/// <summary>Mark a deprecation warning as already logged.</summary>
diff --git a/src/SMAPI/Framework/DeprecationWarning.cs b/src/SMAPI/Framework/DeprecationWarning.cs
new file mode 100644
index 00000000..25415012
--- /dev/null
+++ b/src/SMAPI/Framework/DeprecationWarning.cs
@@ -0,0 +1,38 @@
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>A deprecation warning for a mod.</summary>
+ internal class DeprecationWarning
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The affected mod's display name.</summary>
+ public string ModName { get; }
+
+ /// <summary>A noun phrase describing what is deprecated.</summary>
+ public string NounPhrase { get; }
+
+ /// <summary>The SMAPI version which deprecated it.</summary>
+ public string Version { get; }
+
+ /// <summary>The deprecation level for the affected code.</summary>
+ public DeprecationLevel Level { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modName">The affected mod's display name.</param>
+ /// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
+ /// <param name="version">The SMAPI version which deprecated it.</param>
+ /// <param name="level">The deprecation level for the affected code.</param>
+ public DeprecationWarning(string modName, string nounPhrase, string version, DeprecationLevel level)
+ {
+ this.ModName = modName;
+ this.NounPhrase = nounPhrase;
+ this.Version = version;
+ this.Level = level;
+ }
+ }
+}
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..ace84054 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -42,7 +42,9 @@ namespace StardewModdingAPI.Framework.ModLoading
? ModMetadataStatus.Found
: ModMetadataStatus.Failed;
string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName);
- yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded).SetStatus(status, folder.ManifestParseError ?? "disabled by dot convention");
+
+ yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded)
+ .SetStatus(status, !folder.ShouldBeLoaded ? "disabled by dot convention" : folder.ManifestParseError);
}
}
@@ -85,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModLoading
updateUrls.Add(mod.DataRecord.AlternativeUrl);
// default update URL
- updateUrls.Add("https://smapi.io/compat");
+ updateUrls.Add("https://mods.smapi.io");
// build error
string error = $"{reasonPhrase}. Please check for a ";
@@ -379,7 +381,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/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index 2812a9cc..a4d92e4b 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks.</summary>
public bool IsExiting => this.ExitTokenSource.IsCancellationRequested;
+ /// <summary>Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</summary>
+ public bool IsVerbose { get; }
+
/// <summary>Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger.</summary>
internal bool ShowFullStampInConsole { get; set; }
@@ -56,7 +59,8 @@ namespace StardewModdingAPI.Framework
/// <param name="logFile">The log file to which to write messages.</param>
/// <param name="exitTokenSource">Propagates notification that SMAPI should exit.</param>
/// <param name="colorScheme">The console color scheme to use.</param>
- public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme)
+ /// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param>
+ public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme, bool isVerbose)
{
// validate
if (string.IsNullOrWhiteSpace(source))
@@ -68,6 +72,7 @@ namespace StardewModdingAPI.Framework
this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme);
this.ConsoleInterceptor = consoleInterceptor;
this.ExitTokenSource = exitTokenSource;
+ this.IsVerbose = isVerbose;
}
/// <summary>Log a message for the player or developer.</summary>
@@ -78,6 +83,14 @@ namespace StardewModdingAPI.Framework
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
}
+ /// <summary>Log a message that only appears when <see cref="IMonitor.IsVerbose"/> is enabled.</summary>
+ /// <param name="message">The message to log.</param>
+ public void VerboseLog(string message)
+ {
+ if (this.IsVerbose)
+ this.Log(message, LogLevel.Trace);
+ }
+
/// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
/// <param name="reason">The reason for the shutdown.</param>
public void ExitGameImmediately(string reason)
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..7f0fa4f7
--- /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;
+
+ /// <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;
+
+
+ /*********
+ ** A