summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-12-20 22:35:58 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-12-20 22:35:58 -0500
commit77002d3e9965d9afa843a95129c6acb5d1c4a283 (patch)
treeb4f4a338945cd3f2cc5881e46fb0dd2b075a79ce
parent1c70736c00e6e70f46f539cb26b5fd253f4eff3b (diff)
parent5e2f6f565d6ef5330ea2e8c6a5e796f937289255 (diff)
downloadSMAPI-77002d3e9965d9afa843a95129c6acb5d1c4a283.tar.gz
SMAPI-77002d3e9965d9afa843a95129c6acb5d1c4a283.tar.bz2
SMAPI-77002d3e9965d9afa843a95129c6acb5d1c4a283.zip
Merge branch 'stardew-valley-1.5' into develop
# Conflicts: # docs/release-notes.md
-rw-r--r--docs/release-notes.md8
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs22
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs7
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs8
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs4
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs3
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml11
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json88
-rw-r--r--src/SMAPI/Constants.cs25
-rw-r--r--src/SMAPI/Context.cs79
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs76
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs130
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs17
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs18
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs9
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs21
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs6
-rw-r--r--src/SMAPI/Framework/Monitor.cs11
-rw-r--r--src/SMAPI/Framework/SCore.cs358
-rw-r--r--src/SMAPI/Framework/SGame.cs431
-rw-r--r--src/SMAPI/Framework/SGameRunner.cs156
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs104
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs6
-rw-r--r--src/SMAPI/Patches/DialogueErrorPatch.cs4
-rw-r--r--src/SMAPI/Patches/ScheduleErrorPatch.cs2
-rw-r--r--src/SMAPI/Utilities/PerScreen.cs79
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