diff options
| author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-12-20 22:35:58 -0500 |
|---|---|---|
| committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-12-20 22:35:58 -0500 |
| commit | 77002d3e9965d9afa843a95129c6acb5d1c4a283 (patch) | |
| tree | b4f4a338945cd3f2cc5881e46fb0dd2b075a79ce | |
| parent | 1c70736c00e6e70f46f539cb26b5fd253f4eff3b (diff) | |
| parent | 5e2f6f565d6ef5330ea2e8c6a5e796f937289255 (diff) | |
| download | SMAPI-77002d3e9965d9afa843a95129c6acb5d1c4a283.tar.gz SMAPI-77002d3e9965d9afa843a95129c6acb5d1c4a283.tar.bz2 SMAPI-77002d3e9965d9afa843a95129c6acb5d1c4a283.zip | |
Merge branch 'stardew-valley-1.5' into develop
# Conflicts:
# docs/release-notes.md
27 files changed, 1078 insertions, 613 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 0bbbeb58..b02b58d1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,9 +9,17 @@ ## Upcoming release * For players: + * Updated for Stardew Valley 1.5, including split-screen support. * When the installer is run from within a game folder, it now installs SMAPI to that folder. That simplifies installation if you have multiple copies of the game or it can't otherwise auto-detect the game path. * Clarified not-a-mod error when the SMAPI installer is in the `Mods` folder. +* 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. + +* For the Console Commands mod: + * Added `furniture` option to `world_clear`. + ## 3.7.6 Released 21 November 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 1190a4ab..29052be3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -16,7 +16,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World ** Fields *********/ /// <summary>The valid types that can be cleared.</summary> - private readonly string[] ValidTypes = { "crops", "debris", "fruit-trees", "grass", "trees", "everything" }; + private readonly string[] ValidTypes = { "crops", "debris", "fruit-trees", "furniture", "grass", "trees", "everything" }; /// <summary>The resource clump IDs to consider debris.</summary> private readonly int[] DebrisClumps = { ResourceClump.stumpIndex, ResourceClump.hollowLogIndex, ResourceClump.meteoriteIndex, ResourceClump.boulderIndex }; @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World description: "Clears in-game entities in a given location.\n\n" + "Usage: world_clear <location> <object type>\n" + "- location: the location name for which to clear objects (like Farm), or 'current' for the current location.\n" - + " - object type: the type of object clear. You can specify 'crops', 'debris' (stones/twigs/weeds and dead crops), 'grass', and 'trees' / 'fruit-trees'. You can also specify 'everything', which includes things not removed by the other types (like furniture or resource clumps)." + + " - object type: the type of object clear. You can specify 'crops', 'debris' (stones/twigs/weeds and dead crops), 'furniture', 'grass', and 'trees' / 'fruit-trees'. You can also specify 'everything', which includes things not removed by the other types (like resource clumps)." ) { } @@ -113,6 +113,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World break; } + case "furniture": + { + int removed = this.RemoveFurniture(location, furniture => true); + monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); + break; + } + case "grass": { int removed = this.RemoveTerrainFeatures(location, feature => feature is Grass); @@ -244,15 +251,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { int removed = 0; - if (location is DecoratableLocation decoratableLocation) + foreach (Furniture furniture in location.furniture.ToArray()) { - foreach (Furniture furniture in decoratableLocation.furniture.ToArray()) + if (shouldRemove(furniture)) { - if (shouldRemove(furniture)) - { - decoratableLocation.furniture.Remove(furniture); - removed++; - } + location.furniture.Remove(furniture); + removed++; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index d1dd758b..34149209 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -107,12 +107,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // furniture foreach (int id in this.TryLoad<int, string>("Data\\Furniture").Keys) - { - if (id == 1466 || id == 1468 || id == 1680) - yield return this.TryCreate(ItemType.Furniture, id, p => new TV(p.ID, Vector2.Zero)); - else - yield return this.TryCreate(ItemType.Furniture, id, p => new Furniture(p.ID, Vector2.Zero)); - } + yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID)); // craftables foreach (int id in Game1.bigCraftablesInformation.Keys) 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.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 179ef42a..2a81e12a 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -54,15 +54,15 @@ "Default | UpdateKey": "Nexus:2270" }, - //"Content Patcher": { - // "ID": "Pathoschild.ContentPatcher", - // "Default | UpdateKey": "Nexus:1915" - //}, + "Content Patcher": { + "ID": "Pathoschild.ContentPatcher", + "Default | UpdateKey": "Nexus:1915" + }, - //"Custom Farming Redux": { - // "ID": "Platonymous.CustomFarming", - // "Default | UpdateKey": "Nexus:991" - //}, + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991" + }, "Custom Shirts": { "ID": "Platonymous.CustomShirts", @@ -150,6 +150,51 @@ "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." }, + "Split Screen": { + "ID": "Ilyaki.SplitScreen", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "split-screen mode was added in Stardew Valley 1.5" + }, + + /********* + ** Broke in SDV 1.5 + *********/ + "Audio Devices": { + "ID": "maxvollmer.audiodevices", + "~2.0.0 | Status": "AssumeBroken" // causes crash to desktop when starting the game + }, + + "Custom Localization": { + "ID": "ZaneYork.CustomLocalization", + "FormerIDs": "SMAPI.CustomLocalization", // changed in 1.0.1 + "~1.1 | Status": "AssumeBroken" // reflection error for _localizedAssets field + }, + + "Mod Settings Tab": { + "ID": "GilarF.ModSettingsTab", + "~0.2.1 | Status": "AssumeBroken" // fails extending title menu + }, + + "More Grass": { + "ID": "EpicBellyFlop45.MoreGrass", + "~1.0.8 | Status": "AssumeBroken" // crashes save load + }, + + "Movement Speed": { + "ID": "bcmpinc.MovementSpeed", + "~3.0.0 | Status": "AssumeBroken" // transpiler errors + }, + + "Tree Spread": { + "ID": "bcmpinc.TreeSpread", + "~3.0.0 | Status": "AssumeBroken" // transpiler errors + }, + + "TreeTransplant": { + "ID": "TreeTransplant", + "~1.0.9 | Status": "AssumeBroken" // causes AccessViolationException which prevents game launch + }, + /********* ** Broke in SDV 1.4 *********/ @@ -221,12 +266,6 @@ "~2.3.1-unofficial.7-pathoschild | Status": "AssumeBroken" }, - "Content Patcher": { - "ID": "Pathoschild.ContentPatcher", - "Default | UpdateKey": "Nexus:1915", - "~1.6.4 | Status": "AssumeBroken" - }, - "Current Location (Vrakyas)": { "ID": "Vrakyas.CurrentLocation", "~1.5.4 | Status": "AssumeBroken" @@ -237,12 +276,6 @@ "~1.8 | Status": "AssumeBroken" }, - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991", - "~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK - }, - "Decrafting Mod": { "ID": "MSCFC.DecraftingMod", "~1.0 | Status": "AssumeBroken" // NRE in ModEntry @@ -408,11 +441,6 @@ "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.3 }, - "Movement Speed": { - "ID": "bcmpinc.MovementSpeed", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - "No Added Flying Mine Monsters": { "ID": "Drynwynn.NoAddedFlyingMineMonsters", "~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ @@ -429,11 +457,6 @@ "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained }, - "Split Screen": { - "ID": "Ilyaki.SplitScreen", - "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - "Stardew Hack": { "ID": "bcmpinc.StardewHack", "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) @@ -455,11 +478,6 @@ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) }, - "Tree Spread": { - "ID": "bcmpinc.TreeSpread", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - /********* ** Broke circa SDV 1.2 *********/ diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 88f79811..5aa08887 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> @@ -54,10 +57,10 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.7.6"); /// <summary>The minimum supported version of Stardew Valley.</summary> - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.0"); /// <summary>The maximum supported version of Stardew Valley.</summary> - public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.4.5"); + public static ISemanticVersion MaximumGameVersion { get; } = null; /// <summary>The target game platform.</summary> public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform; @@ -272,21 +275,13 @@ namespace StardewModdingAPI return null; // get basic info - string playerName; - ulong saveID; - if (Context.LoadStage == LoadStage.SaveParsed) - { - playerName = SaveGame.loaded.player.Name; - saveID = SaveGame.loaded.uniqueIDForThisGame; - } - else - { - playerName = Game1.player.Name; - saveID = Game1.uniqueIDForThisGame; - } + string saveName = Game1.GetSaveGameName(set_value: false); + ulong saveID = Context.LoadStage == LoadStage.SaveParsed + ? SaveGame.loaded.uniqueIDForThisGame + : Game1.uniqueIDForThisGame; // build folder name - return $"{new string(playerName.Where(char.IsLetterOrDigit).ToArray())}_{saveID}"; + return $"{new string(saveName.Where(char.IsLetterOrDigit).ToArray())}_{saveID}"; } /// <summary>Get the path to the current save folder, if any.</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/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index f20580e1..83a63986 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -29,8 +29,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Interceptors which edit matching assets after they're loaded.</summary> private IList<ModLinked<IAssetEditor>> Editors => this.Coordinator.Editors; - /// <summary>A lookup which indicates whether the asset is localizable (i.e. the filename contains the locale), if previously loaded.</summary> - private readonly IDictionary<string, bool> IsLocalizableLookup; + /// <summary>Maps asset names to their localized form, like <c>LooseSprites\Billboard => LooseSprites\Billboard.fr-FR</c> (localized) or <c>Maps\AnimalShop => Maps\AnimalShop</c> (not localized).</summary> + private IDictionary<string, string> LocalizedAssetNames => LocalizedContentManager.localizedAssetNames; /// <summary>Whether the next load is the first for any game content manager.</summary> private static bool IsFirstLoad = true; @@ -55,7 +55,6 @@ namespace StardewModdingAPI.Framework.ContentManagers public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false) { - this.IsLocalizableLookup = reflection.GetField<IDictionary<string, bool>>(this, "_localizedAsset").GetValue(); this.OnLoadingFirstAsset = onLoadingFirstAsset; } @@ -124,7 +123,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // find assets for which a translatable version was loaded HashSet<string> removeAssetNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); - foreach (string key in this.IsLocalizableLookup.Where(p => p.Value).Select(p => p.Key)) + foreach (string key in this.LocalizedAssetNames.Where(p => p.Key != p.Value).Select(p => p.Key)) removeAssetNames.Add(this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) ? assetName : key); // invalidate translatable assets @@ -154,21 +153,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="normalizedAssetName">The normalized asset name.</param> protected override bool IsNormalizedKeyLoaded(string normalizedAssetName) { - // default English - if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalizedAssetName)) - return this.Cache.ContainsKey(normalizedAssetName); - - // translated - string keyWithLocale = $"{normalizedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalizableLookup.TryGetValue(keyWithLocale, out bool localizable)) - { - return localizable - ? this.Cache.ContainsKey(keyWithLocale) - : this.Cache.ContainsKey(normalizedAssetName); - } - - // not loaded yet - return false; + string cachedKey = null; + bool localized = + this.Language != LocalizedContentManager.LanguageCode.en + && !this.Coordinator.IsManagedAssetKey(normalizedAssetName) + && this.LocalizedAssetNames.TryGetValue(normalizedAssetName, out ca |
