diff options
| author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-12-20 22:34:59 -0500 |
|---|---|---|
| committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-12-20 22:34:59 -0500 |
| commit | 2e8c7e06c5c46834b570b667cb7497fe4cc408ac (patch) | |
| tree | af2cb14a02d85fb2b435ceb38a81c9a97146bf87 | |
| parent | 50a146d1c9a228391c4201685a5e0df9daa529e9 (diff) | |
| download | SMAPI-2e8c7e06c5c46834b570b667cb7497fe4cc408ac.tar.gz SMAPI-2e8c7e06c5c46834b570b667cb7497fe4cc408ac.tar.bz2 SMAPI-2e8c7e06c5c46834b570b667cb7497fe4cc408ac.zip | |
update for split-screen mode
This includes splitting GameRunner (the main game instance) from Game1 (now a per-screen game state), adding a PerScreen<T> utility to simplify per-screen values, adding separate per-screen input handling and events, adding new Context fields for split-screen, and logging the screen ID in split-screen mode to distinguish log entries.
| -rw-r--r-- | docs/release-notes.md | 6 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs | 8 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 4 | ||||
| -rw-r--r-- | src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs | 3 | ||||
| -rw-r--r-- | src/SMAPI.Web/Views/LogParser/Index.cshtml | 11 | ||||
| -rw-r--r-- | src/SMAPI/Constants.cs | 3 | ||||
| -rw-r--r-- | src/SMAPI/Context.cs | 79 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Logging/LogManager.cs | 5 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/DataHelper.cs | 8 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/InputHelper.cs | 21 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModHelper.cs | 6 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Monitor.cs | 11 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SCore.cs | 358 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SGame.cs | 121 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SGameRunner.cs | 156 | ||||
| -rw-r--r-- | src/SMAPI/Utilities/PerScreen.cs | 79 |
16 files changed, 624 insertions, 255 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 00445832..176961d4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,11 @@ ## Upcoming release * For players: - * Updated for Stardew Valley 1.5. + * Updated for Stardew Valley 1.5, including split-screen support. + +* For modders: + * Added `PerScreen<T>` utility and new `Context` fields to simplify split-screen support in mods. + * Added screen ID to log when playing in split-screen mode. ## 3.7.6 Released 21 November 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs index 42e283a9..992876ef 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing /// <summary>The log level for the next log message.</summary> public LogLevel Level { get; set; } + /// <summary>The screen ID in split-screen mode.</summary> + public int ScreenId { get; set; } + /// <summary>The mod name for the next log message.</summary> public string Mod { get; set; } @@ -36,10 +39,11 @@ namespace StardewModdingAPI.Web.Framework.LogParsing /// <summary>Start accumulating values for a new log message.</summary> /// <param name="time">The local time when the log was posted.</param> /// <param name="level">The log level.</param> + /// <param name="screenId">The screen ID in split-screen mode.</param> /// <param name="mod">The mod name.</param> /// <param name="text">The initial log text.</param> /// <exception cref="InvalidOperationException">A log message is already started; call <see cref="Clear"/> before starting a new message.</exception> - public void Start(string time, LogLevel level, string mod, string text) + public void Start(string time, LogLevel level, int screenId, string mod, string text) { if (this.Started) throw new InvalidOperationException("Can't start new message, previous log message isn't done yet."); @@ -48,6 +52,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing this.Time = time; this.Level = level; + this.ScreenId = screenId; this.Mod = mod; this.Text.Append(text); } @@ -74,6 +79,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing { Time = this.Time, Level = this.Level, + ScreenId = this.ScreenId, Mod = this.Mod, Text = this.Text.ToString() }; diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 227dcd89..f69d4b6f 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing ** Fields *********/ /// <summary>A regex pattern matching the start of a SMAPI message.</summary> - private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?<time>\d\d[:\.]\d\d[:\.]\d\d) (?<level>[a-z]+) +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?<time>\d\d[:\.]\d\d[:\.]\d\d) (?<level>[a-z]+)(?: +screen_(?<screen>\d+))? +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// <summary>A regex pattern matching SMAPI's initial platform info message.</summary> private readonly Regex InfoLinePattern = new Regex(@"^SMAPI (?<apiVersion>.+) with Stardew Valley (?<gameVersion>.+) on (?<os>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -304,9 +304,11 @@ namespace StardewModdingAPI.Web.Framework.LogParsing builder.Clear(); } + var screenGroup = header.Groups["screen"]; builder.Start( time: header.Groups["time"].Value, level: Enum.Parse<LogLevel>(header.Groups["level"].Value, ignoreCase: true), + screenId: screenGroup.Success ? int.Parse(screenGroup.Value) : 0, // main player is always screen ID 0 mod: header.Groups["modName"].Value, text: line.Substring(header.Length) ); diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs index f7c99d02..1e08be78 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// <summary>The log level.</summary> public LogLevel Level { get; set; } + /// <summary>The screen ID in split-screen mode.</summary> + public int ScreenId { get; set; } + /// <summary>The mod name.</summary> public string Mod { get; set; } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d4ff4f10..fd472673 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -13,6 +13,8 @@ .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true); + + ISet<int> screenIds = new HashSet<int>(Model.ParsedLog?.Messages?.Select(p => p.ScreenId) ?? new int[0]); } @section Head { @@ -35,7 +37,8 @@ showMods: @this.ForJson(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true)), showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false)), showLevels: @this.ForJson(defaultFilters), - enableFilters: @this.ForJson(!Model.ShowRaw) + enableFilters: @this.ForJson(!Model.ShowRaw), + screenIds: @this.ForJson(screenIds) }, '@this.Url.PlainAction("Index", "LogParser", values: null)'); }); </script> @@ -305,6 +308,10 @@ else if (Model.ParsedLog?.IsValid == true) @if (message.IsStartOfSection) { <text> v-on:click="toggleSection('@message.Section')" </text> } v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter"> <td v-pre>@message.Time</td> + @if (screenIds.Count > 1) + { + <td v-pre>screen_@message.ScreenId</td> + } <td v-pre>@message.Level.ToString().ToUpper()</td> <td v-pre data-title="@message.Mod">@message.Mod</td> <td> @@ -325,7 +332,7 @@ else if (Model.ParsedLog?.IsValid == true) if (message.Repeated > 0) { <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter"> - <td colspan="3"></td> + <td colspan="4"></td> <td v-pre><i>repeats [@message.Repeated] times.</i></td> </tr> } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 5ff324b0..d0f34cee 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -39,6 +39,9 @@ namespace StardewModdingAPI /// <summary>The game's assembly name.</summary> internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley"; + + /// <summary>The <see cref="Context.ScreenId"/> value which should appear in the SMAPI log, if any.</summary> + internal static int? LogScreenId { get; set; } } /// <summary>Contains SMAPI's constants and assumptions.</summary> diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index a7238b32..b1b33cd6 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using StardewModdingAPI.Enums; using StardewModdingAPI.Events; +using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; @@ -9,16 +11,49 @@ namespace StardewModdingAPI public static class Context { /********* + ** Fields + *********/ + /// <summary>Whether the player has loaded a save and the world has finished initializing.</summary> + private static readonly PerScreen<bool> IsWorldReadyForScreen = new PerScreen<bool>(); + + /// <summary>The current stage in the game's loading process.</summary> + private static readonly PerScreen<LoadStage> LoadStageForScreen = new PerScreen<LoadStage>(); + + /// <summary>Whether a player save has been loaded.</summary> + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !(Game1.activeClickableMenu is TitleMenu); + + /// <summary>Whether the game is currently writing to the save file.</summary> + internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something + + /// <summary>The active split-screen instance IDs.</summary> + internal static readonly ISet<int> ActiveScreenIds = new HashSet<int>(); + + /// <summary>The last screen ID that was removed from the game, used to synchronize <see cref="PerScreen{T}"/>.</summary> + internal static int LastRemovedScreenId = -1; + + /// <summary>The current stage in the game's loading process.</summary> + internal static LoadStage LoadStage + { + get => Context.LoadStageForScreen.Value; + set => Context.LoadStageForScreen.Value = value; + } + + + /********* ** Accessors *********/ /**** - ** Public + ** Game/player state ****/ /// <summary>Whether the game has performed core initialization. This becomes true right before the first update tick.</summary> public static bool IsGameLaunched { get; internal set; } /// <summary>Whether the player has loaded a save and the world has finished initializing.</summary> - public static bool IsWorldReady { get; internal set; } + public static bool IsWorldReady + { + get => Context.IsWorldReadyForScreen.Value; + set => Context.IsWorldReadyForScreen.Value = value; + } /// <summary>Whether <see cref="IsWorldReady"/> is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc).</summary> public static bool IsPlayerFree => Context.IsWorldReady && Game1.currentLocation != null && Game1.activeClickableMenu == null && !Game1.dialogueUp && (!Game1.eventUp || Game1.isFestival()); @@ -29,22 +64,36 @@ namespace StardewModdingAPI /// <summary>Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use <see cref="IDisplayEvents"/> events to draw to the screen.</summary> public static bool IsInDrawLoop { get; internal set; } - /// <summary>Whether <see cref="IsWorldReady"/> and the player loaded the save in multiplayer mode (regardless of whether any other players are connected).</summary> - public static bool IsMultiplayer => Context.IsWorldReady && Game1.multiplayerMode != Game1.singlePlayer; - - /// <summary>Whether <see cref="IsWorldReady"/> and the current player is the main player. This is always true in single-player, and true when hosting in multiplayer.</summary> - public static bool IsMainPlayer => Context.IsWorldReady && Game1.IsMasterGame; - /**** - ** Internal + ** Multiplayer ****/ - /// <summary>Whether a player save has been loaded.</summary> - internal static bool IsSaveLoaded => Game1.hasLoadedGame && !(Game1.activeClickableMenu is TitleMenu); + /// <summary>The unique ID of the current screen in split-screen mode. A screen is always assigned a new ID when it's opened (so a player who quits and rejoins has a new screen ID).</summary> + public static int ScreenId => Game1.game1?.instanceId ?? 0; - /// <summary>Whether the game is currently writing to the save file.</summary> - internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something + /// <summary>Whether the game is running in multiplayer or split-screen mode (regardless of whether any other players are connected). See <see cref="IsSplitScreen"/> and <see cref="HasRemotePlayers"/> for more specific checks.</summary> + public static bool IsMultiplayer => Context.IsSplitScreen || (Context.IsWorldReady && Game1.multiplayerMode != Game1.singlePlayer); - /// <summary>The current stage in the game's loading process.</summary> - internal static LoadStage LoadStage { get; set; } + /// <summary>Whether this player is running on the main player's computer. This is true for both the main player and split-screen players.</summary> + public static bool IsOnHostComputer => Context.IsMainPlayer || Context.IsSplitScreen; + + /// <summary>Whether the current player is playing in a split-screen. This is only applicable when <see cref="IsOnHostComputer"/> is true, since split-screen players on another computer are just regular remote players.</summary> + public static bool IsSplitScreen => LocalMultiplayer.IsLocalMultiplayer(); + + /// <summary>Whether there are players connected over the network.</summary> + public static bool HasRemotePlayers => Context.IsMultiplayer && !Game1.hasLocalClientsOnly; + + /// <summary>Whether the current player is the main player. This is always true in single-player, and true when hosting in multiplayer.</summary> + public static bool IsMainPlayer => Game1.IsMasterGame && !(TitleMenu.subMenu is FarmhandMenu); + + + /********* + ** Public methods + *********/ + /// <summary>Get whether a screen ID is still active.</summary> + /// <param name="id">The screen ID.</param> + public static bool HasScreenId(int id) + { + return Context.ActiveScreenIds.Contains(id); + } } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 1e484709..a2c0125e 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -84,10 +84,11 @@ namespace StardewModdingAPI.Framework.Logging /// <param name="writeToConsole">Whether to output log messages to the console.</param> /// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param> /// <param name="isDeveloperMode">Whether to enable full console output for developers.</param> - public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode) + /// <param name="getScreenIdForLog">Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</param> + public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode, Func<int?> getScreenIdForLog) { // init construction logic - this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose) + this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 41612387..0fe3209f 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -69,8 +69,8 @@ namespace StardewModdingAPI.Framework.ModHelpers { if (Context.LoadStage == LoadStage.None) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded."); - if (!Game1.IsMasterGame) - throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); + if (!Context.IsOnHostComputer) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when connected to a remote host. (Save files are stored on the main player's computer.)"); string internalKey = this.GetSaveFileKey(key); @@ -87,8 +87,8 @@ namespace StardewModdingAPI.Framework.ModHelpers { if (Context.LoadStage == LoadStage.None) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded."); - if (!Game1.IsMasterGame) - throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); + if (!Context.IsOnHostComputer) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when connected to a remote host. (Save files are stored on the main player's computer.)"); string internalKey = this.GetSaveFileKey(key); string data = model != null diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index 09ce3c65..e1317544 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Framework.ModHelpers @@ -8,8 +9,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ - /// <summary>Manages the game's input state.</summary> - private readonly SInputState InputState; + /// <summary>Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</summary> + private readonly Func<SInputState> CurrentInputState; /********* @@ -17,41 +18,41 @@ namespace StardewModdingAPI.Framework.ModHelpers *********/ /// <summary>Construct an instance.</summary> /// <param name="modID">The unique ID of the relevant mod.</param> - /// <param name="inputState">Manages the game's input state.</param> - public InputHelper(string modID, SInputState inputState) + /// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param> + public InputHelper(string modID, Func<SInputState> currentInputState) : base(modID) { - this.InputState = inputState; + this.CurrentInputState = currentInputState; } /// <inheritdoc /> public ICursorPosition GetCursorPosition() { - return this.InputState.CursorPosition; + return this.CurrentInputState().CursorPosition; } /// <inheritdoc /> public bool IsDown(SButton button) { - return this.InputState.IsDown(button); + return this.CurrentInputState().IsDown(button); } /// <inheritdoc /> public bool IsSuppressed(SButton button) { - return this.InputState.IsSuppressed(button); + return this.CurrentInputState().IsSuppressed(button); } /// <inheritdoc /> public void Suppress(SButton button) { - this.InputState.OverrideButton(button, setDown: false); + this.CurrentInputState().OverrideButton(button, setDown: false); } /// <inheritdoc /> public SButtonState GetState(SButton button) { - return this.InputState.GetState(button); + return this.CurrentInputState().GetState(button); } } } diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index d9fc8621..058bff83 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -51,7 +51,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Construct an instance.</summary> /// <param name="modID">The mod's unique ID.</param> /// <param name="modDirectory">The full path to the mod's folder.</param> - /// <param name="inputState">Manages the game's input state.</param> + /// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param> /// <param name="events">Manages access to events raised by SMAPI.</param> /// <param name="contentHelper">An API for loading content assets.</param> /// <param name="contentPackHelper">An API for managing content packs.</param> @@ -63,7 +63,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> - public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) + public ModHelper(string modID, string modDirectory, Func<SInputState> currentInputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) : base(modID) { // validate directory @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper)); this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper)); - this.Input = new InputHelper(modID, inputState); + this.Input = new InputHelper(modID, currentInputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 533420a5..04e67d68 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework /// <summary>A cache of messages that should only be logged once.</summary> private readonly HashSet<string> LogOnceCache = new HashSet<string>(); + /// <summary>Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</summary> + private readonly Func<int?> GetScreenIdForLog; + /********* ** Accessors @@ -56,7 +59,8 @@ namespace StardewModdingAPI.Framework /// <param name="logFile">The log file to which to write messages.</param> /// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param> /// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param> - public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) + /// <param name="getScreenIdForLog">Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</param> + public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose, Func<int?> getScreenIdForLog) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -68,6 +72,7 @@ namespace StardewModdingAPI.Framework this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig); this.IgnoreChar = ignoreChar; this.IsVerbose = isVerbose; + this.GetScreenIdForLog = getScreenIdForLog; } /// <inheritdoc /> @@ -143,7 +148,9 @@ namespace StardewModdingAPI.Framework private string GenerateMessagePrefix(string source, ConsoleLogLevel level) { string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); - return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]"; + int? playerIndex = this.GetScreenIdForLog(); + + return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]"; } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1b4c32bb..a7f8fbed 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -85,17 +85,14 @@ namespace StardewModdingAPI.Framework private readonly CommandManager CommandManager = new CommandManager(); /// <summary>The underlying game instance.</summary> - private SGame Game; - - /// <summary>Manages input visible to the game.</summary> - private SInputState Input => SGame.Input; - - /// <summary>The game's core multiplayer utility.</summary> - private SMultiplayer Multiplayer => SGame.Multiplayer; + private SGameRunner Game; /// <summary>SMAPI's content manager.</summary> private ContentCoordinator ContentCore; + /// <summary>The game's core multiplayer utility for the main player.</summary> + private SMultiplayer Multiplayer; + /// <summary>Tracks the installed mods.</summary> /// <remarks>This is initialized after the game starts.</remarks> private readonly ModRegistry ModRegistry = new ModRegistry(); @@ -103,11 +100,6 @@ namespace StardewModdingAPI.Framework /// <summary>Manages SMAPI events for mods.</summary> private readonly EventManager EventManager; - /// <summary>Monitors the entire game state for changes.</summary> - private WatcherCore Watchers; - - /// <summary>A snapshot of the current <see cref="Watchers"/> state.</summary> - private readonly WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); /**** ** State @@ -127,25 +119,15 @@ namespace StardewModdingAPI.Framework /// <summary>Whether post-game-startup initialization has been performed.</summary> private bool IsInitialized; + /// <summary>Whether the player just returned to the title screen.</summary> + public bool JustReturnedToTitle { get; set; } + /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary> private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second - /// <summary>The number of ticks until SMAPI should notify mods that the game has loaded.</summary> - /// <remarks>Skipping a few frames ensures the game finishes initializing the world before mods try to change it.</remarks> - private readonly Countdown AfterLoadTimer = new Countdown(5); - /// <summary>Whether custom content was removed from the save data to avoid a crash.</summary> private bool IsSaveContentRemoved; - /// <summary>Whether the game is saving and SMAPI has already raised <see cref="IGameLoopEvents.Saving"/>.</summary> - private bool IsBetweenSaveEvents; - - /// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="IGameLoopEvents.SaveCreating"/>.</summary> - private bool IsBetweenCreateEvents; - - /// <summary>Whether the player just returned to the title screen.</summary> - private bool JustReturnedToTitle; - /// <summary>Asset interceptors added or removed since the last tick.</summary> private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>(); @@ -191,7 +173,7 @@ namespace StardewModdingAPI.Framework if (File.Exists(Constants.ApiUserConfigPath)) JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); - this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode); + this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog); SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); this.EventManager = new EventManager(this.ModRegistry, SCore.PerformanceMonitor); @@ -250,22 +232,22 @@ namespace StardewModdingAPI.Framework LocalizedContentManager.OnLanguageChange += locale => this.OnLocaleChanged(); // override game - var multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); - var modHooks = new SModHooks(this.OnNewDayAfterFade); + this.Multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); SGame.CreateContentManagerImpl = this.CreateContentManager; // must be static since the game accesses it before the SGame constructor is called - this.Game = new SGame( + this.Game = new SGameRunner( monitor: this.Monitor, reflection: this.Reflection, eventManager: this.EventManager, - modHooks: modHooks, - multiplayer: multiplayer, + modHooks: new SModHooks(this.OnNewDayAfterFade), + multiplayer: this.Multiplayer, exitGameImmediately: this.ExitGameImmediately, onGameContentLoaded: this.OnGameContentLoaded, onGameUpdating: this.OnGameUpdating, + onPlayerInstanceUpdating: this.OnPlayerInstanceUpdating, onGameExiting: this.OnGameExiting ); - StardewValley.Program.gamePtr = this.Game; + StardewValley.GameRunner.instance = this.Game; // apply game patches new GamePatcher(this.Monitor).Apply( @@ -422,12 +404,6 @@ namespace StardewModdingAPI.Framework /// <summary>Raised after the game finishes initializing.</summary> private void OnGameInitialized() { - // set initial state - this.Input.TrueUpdate(); - - // init watchers - this.Watchers = new WatcherCore(this.Input, this.Game.GetObservableLocations()); - // validate XNB integrity if (!this.ValidateContentIntegrity()) this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); @@ -460,8 +436,6 @@ namespace StardewModdingAPI.Framework /// <param name="runGameUpdate">Invoke the game's update logic.</param> private void OnGameUpdating(GameTime gameTime, Action runGameUpdate) { - var events = this.EventManager; - try { /********* @@ -471,15 +445,6 @@ namespace StardewModdingAPI.Framework SCore.DeprecationManager.PrintQueued(); SCore.PerformanceMonitor.PrintQueuedAlerts(); - // reapply overrides - if (this.JustReturnedToTitle) - { - if (!(Game1.mapDisplayDevice is SDisplayDevice)) - Game1.mapDisplayDevice = this.GetMapDisplayDevice(); - - this.JustReturnedToTitle = false; - } - /********* ** First-tick initialization *********/ @@ -490,25 +455,151 @@ namespace StardewModdingAPI.Framework } |
