summaryrefslogtreecommitdiff
path: root/src/SMAPI
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 /src/SMAPI
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
Diffstat (limited to 'src/SMAPI')
-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
19 files changed, 981 insertions, 559 deletions
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 cachedKey);
+
+ return localized
+ ? this.Cache.ContainsKey(cachedKey)
+ : this.Cache.ContainsKey(normalizedAssetName);
}
/// <summary>Add tracking data to an asset and add it to the cache.</summary>
@@ -197,22 +190,16 @@ namespace StardewModdingAPI.Framework.ContentManagers
// doesn't change the instance stored in the cache, e.g. using `asset.ReplaceWith`.
if (useCache)
{
- string keyWithLocale = $"{assetName}.{this.GetLocale(language)}";
+ string translatedKey = $"{assetName}.{this.GetLocale(language)}";
base.TrackAsset(assetName, value, language, useCache: true);
- if (this.Cache.ContainsKey(keyWithLocale))
- base.TrackAsset(keyWithLocale, value, language, useCache: true);
+ if (this.Cache.ContainsKey(translatedKey))
+ base.TrackAsset(translatedKey, value, language, useCache: true);
// track whether the injected asset is translatable for is-loaded lookups
- if (this.Cache.ContainsKey(keyWithLocale))
- {
- this.IsLocalizableLookup[assetName] = true;
- this.IsLocalizableLookup[keyWithLocale] = true;
- }
+ if (this.Cache.ContainsKey(translatedKey))
+ this.LocalizedAssetNames[assetName] = translatedKey;
else if (this.Cache.ContainsKey(assetName))
- {
- this.IsLocalizableLookup[assetName] = false;
- this.IsLocalizableLookup[keyWithLocale] = false;
- }
+ this.LocalizedAssetNames[assetName] = assetName;
else
this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error);
}
@@ -226,24 +213,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <remarks>Derived from <see cref="LocalizedContentManager.Load{T}(string, LocalizedContentManager.LanguageCode)"/>.</remarks>
private T RawLoad<T>(string assetName, LanguageCode language, bool useCache)
{
- // try translated asset
+ // use cached key
+ if (this.LocalizedAssetNames.TryGetValue(assetName, out string cachedKey))
+ return base.RawLoad<T>(cachedKey, useCache);
+
+ // try translated key
if (language != LocalizedContentManager.LanguageCode.en)
{
string translatedKey = $"{assetName}.{this.GetLocale(language)}";
- if (!this.IsLocalizableLookup.TryGetValue(translatedKey, out bool isTranslatable) || isTranslatable)
+ try
{
- try
- {
- T obj = base.RawLoad<T>(translatedKey, useCache);
- this.IsLocalizableLookup[assetName] = true;
- this.IsLocalizableLookup[translatedKey] = true;
- return obj;
- }
- catch (ContentLoadException)
- {
- this.IsLocalizableLookup[assetName] = false;
- this.IsLocalizableLookup[translatedKey] = false;
- }
+ T obj = base.RawLoad<T>(translatedKey, useCache);
+ this.LocalizedAssetNames[assetName] = translatedKey;
+ return obj;
+ }
+ catch (ContentLoadException)
+ {
+ this.LocalizedAssetNames[assetName] = assetName;
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 12d672cf..127705ea 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -12,7 +12,6 @@ using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using xTile;
using xTile.Format;
-using xTile.ObjectModel;
using xTile.Tiles;
namespace StardewModdingAPI.Framework.ContentManagers
@@ -127,8 +126,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
asset = this.RawLoad<T>(assetName, useCache: false);
if (asset is Map map)
{
- this.NormalizeTilesheetPaths(map);
- this.FixCustomTilesheetPaths(map, relativeMapPath: assetName);
+ map.assetPath = assetName;
+ this.FixTilesheetPaths(map, relativeMapPath: assetName);
}
}
break;
@@ -168,8 +167,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
// fetch & cache
FormatManager formatManager = FormatManager.Instance;
Map map = formatManager.LoadMap(file.FullName);
- this.NormalizeTilesheetPaths(map);
- this.FixCustomTilesheetPaths(map, relativeMapPath: assetName);
+ map.assetPath = assetName;
+ this.FixTilesheetPaths(map, relativeMapPath: assetName);
asset = (T)(object)map;
}
break;
@@ -257,44 +256,21 @@ namespace StardewModdingAPI.Framework.ContentManagers
return texture;
}
- /// <summary>Normalize map tilesheet paths for the current platform.</summary>
- /// <param name="map">The map whose tilesheets to fix.</param>
- private void NormalizeTilesheetPaths(Map map)
- {
- foreach (TileSheet tilesheet in map.TileSheets)
- tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource);
- }
-
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
/// <param name="map">The map whose tilesheets to fix.</param>
/// <param name="relativeMapPath">The relative map path within the mod folder.</param>
/// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception>
- /// <remarks>
- /// The game's logic for tilesheets in <see cref="Game1.setGraphicsForSeason"/> is a bit specialized. It boils
- /// down to this:
- /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded
- /// as-is relative to the <c>Content</c> folder.
- /// * Else it's loaded from <c>Content\Maps</c> with a seasonal prefix.
- ///
- /// That logic doesn't work well in our case, mainly because we have no location metadata at this point.
- /// Instead we use a more heuristic approach: check relative to the map file first, then relative to
- /// <c>Content\Maps</c>, then <c>Content</c>. If the image source filename contains a seasonal prefix, try for a
- /// seasonal variation and then an exact match.
- ///
- /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference.
- /// </remarks>
- private void FixCustomTilesheetPaths(Map map, string relativeMapPath)
+ private void FixTilesheetPaths(Map map, string relativeMapPath)
{
// get map info
- if (!map.TileSheets.Any())
- return;
relativeMapPath = this.AssertAndNormalizeAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators
string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder
- bool isOutdoors = map.Properties.TryGetValue("Outdoors", out PropertyValue outdoorsProperty) && outdoorsProperty != null;
// fix tilesheets
foreach (TileSheet tilesheet in map.TileSheets)
{
+ tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource);
+
string imageSource = tilesheet.ImageSource;
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
@@ -305,7 +281,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// load best match
try
{
- if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, isOutdoors, out string assetName, out string error))
+ if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out string assetName, out string error))
throw new SContentLoadException($"{errorPrefix} {error}");
tilesheet.ImageSource = assetName;
@@ -319,37 +295,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Get the actual asset name for a tilesheet.</summary>
/// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
- /// <param name="originalPath">The tilesheet path to load.</param>
- /// <param name="willSeasonalize">Whether the game will apply seasonal logic to the tilesheet.</param>
+ /// <param name="relativePath">The tilesheet path to load.</param>
/// <param name="assetName">The found asset name.</param>
/// <param name="error">A message indicating why the file couldn't be loaded.</param>
/// <returns>Returns whether the asset name was found.</returns>
- /// <remarks>See remarks on <see cref="FixCustomTilesheetPaths"/>.</remarks>
- private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string originalPath, bool willSeasonalize, out string assetName, out string error)
+ /// <remarks>See remarks on <see cref="FixTilesheetPaths"/>.</remarks>
+ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out string assetName, out string error)
{
assetName = null;
error = null;
// nothing to do
- if (string.IsNullOrWhiteSpace(originalPath))
+ if (string.IsNullOrWhiteSpace(relativePath))
{
- assetName = originalPath;
+ assetName = relativePath;
return true;
}
- // parse path
- string filename = Path.GetFileName(originalPath);
- bool isSeasonal = filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
- string relativePath = originalPath;
- if (willSeasonalize && isSeasonal)
- {
- string dirPath = Path.GetDirectoryName(originalPath);
- relativePath = Path.Combine(dirPath, $"{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}");
- }
-
// get relative to map file
{
string localKey = Path.Combine(modRelativeMapFolder, relativePath);
@@ -361,38 +323,24 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// get from game assets
- // Map tilesheet keys shouldn't include the "Maps/" prefix (the game will add it automatically) or ".png" extension.
+ string contentKey = this.GetContentKeyForTilesheetImageSource(relativePath);
+ try
{
- string contentKey = relativePath;
- foreach (char separator in PathUtilities.PossiblePathSeparators)
- {
- if (contentKey.StartsWith($"Maps{separator}"))
- {
- contentKey = contentKey.Substring(5);
- break;
- }
- }
- if (contentKey.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
- contentKey = contentKey.Substring(0, contentKey.Length - 4);
-
- try
- {
- this.GameContentManager.Load<Texture2D>(Path.Combine("Maps", contentKey), this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
- assetName = contentKey;
- return true;
- }
- catch
- {
- // ignore file-not-found errors
- // TODO: while it's useful to suppress an asset-not-found error here to avoid
- // confusion, this is a pretty naive approach. Even if the file doesn't exist,
- // the file may have been loaded through an IAssetLoader which failed. So even
- // if the content file doesn't exist, that doesn't mean the error here is a
- // content-not-found error. Unfortunately XNA doesn't provide a good way to
- // detect the error type.
- if (this.GetContentFolderFileExists(contentKey))
- throw;
- }
+ this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
+ assetName = contentKey;
+ return true;
+ }
+ catch
+ {
+ // ignore file-not-found errors
+ // TODO: while it's useful to suppress an asset-not-found error here to avoid
+ // confusion, this is a pretty naive approach. Even if the file doesn't exist,
+ // the file may have been loaded through an IAssetLoader which failed. So even
+ // if the content file doesn't exist, that doesn't mean the error here is a
+ // content-not-found error. Unfortunately XNA doesn't provide a good way to
+ // detect the error type.
+ if (this.GetContentFolderFileExists(contentKey))
+ throw;
}
// not found
@@ -412,5 +360,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
// get file
return new FileInfo(path).Exists;
}
+
+ /// <summary>Get the asset key for a tilesheet in the game's <c>Maps</c> content folder.</summary>
+ /// <param name="relativePath">The tilesheet image source.</param>
+ private string GetContentKeyForTilesheetImageSource(string relativePath)
+ {
+ string key = relativePath;
+ string topFolder = PathUtilities.GetSegments(key, limit: 2)[0];
+
+ // convert image source relative to map file into asset key
+ if (!topFolder.Equals("Maps", StringComparison.OrdinalIgnoreCase))
+ key = Path.Combine("Maps", key);
+
+ // remove file extension from unpacked file
+ if (key.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
+ key = key.Substring(0, key.Length - 4);
+
+ return key;
+ }
}
}
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
index f618608a..23670202 100644
--- a/src/SMAPI/Framework/Input/SInputState.cs
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -65,13 +65,16 @@ namespace StardewModdingAPI.Framework.Input
// update SMAPI extended data
try
{
- float zoomMultiplier = (1f / Game1.options.zoomLevel);
+ float scale = Game1.options.uiScale;
// get real values
var controller = new GamePadStateBuilder(base.GetGamePadState());
var keyboard = new KeyboardStateBuilder(base.GetKeyboardState());
var mouse = new MouseStateBuilder(base.GetMouseState());
- Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y);
+ Vector2 cursorAbsolutePos = new Vector2(
+ x: (mouse.X / scale) + Game1.uiViewport.X,
+ y: (mouse.Y / scale) + Game1.uiViewport.Y
+ );
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
HashSet<SButton> reallyDown = new HashSet<SButton>(this.GetPressedButtons(keyboard, mouse, controller));
@@ -106,7 +109,7 @@ namespace StardewModdingAPI.Framework.Input
if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile)
{
this.LastPlayerTile = playerTilePos;
- this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier);
+ this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, scale);
}
}
catch (InvalidOperationException)
@@ -199,11 +202,11 @@ namespace StardewModdingAPI.Framework.Input
/// <summary>Get the current cursor position.</summary>
/// <param name="mouseState">The current mouse state.</param>
/// <param name="absolutePixels">The absolute pixel position relative to the map, adjusted for pixel zoom.</param>
- /// <param name="zoomMultiplier">The multiplier applied to pixel coordinates to adjust them for pixel zoom.</param>
- private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels, float zoomMultiplier)
+ /// <param name="scale">The UI scale applied to pixel coordinates.</param>
+ private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels, float scale)
{
- Vector2 screenPixels = new Vector2(mouseState.X * zoomMultiplier, mouseState.Y * zoomMultiplier);
- Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize));
+ Vector2 screenPixels = new Vector2(mouseState.X / scale, mouseState.Y / scale);
+ Vector2 tile = new Vector2((int)((Game1.uiViewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.uiViewport.Y + screenPixels.Y) / Game1.tileSize));
Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton
? tile
: Game1.player.GetGrabTile();
diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs
index b6704f26..ba1879da 100644
--- a/src/SMAPI/Framework/InternalExtensions.cs
+++ b/src/SMAPI/Framework/InternalExtensions.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
+using StardewValley.Menus;
namespace StardewModdingAPI.Framework
{
@@ -154,6 +156,22 @@ namespace StardewModdingAPI.Framework
}
/****
+ ** IActiveClickableMenu
+ ****/
+ /// <summary>Get a string representation of the menu chain to the given menu (including the specified menu), in parent to child order.</summary>
+ /// <param name="menu">The menu whose chain to get.</param>
+ public static string GetMenuChainLabel(this IClickableMenu menu)
+ {
+ static IEnumerable<IClickableMenu> GetAncestors(IClickableMenu menu)
+ {
+ for (; menu != null; menu = menu.GetParentMenu())
+ yield return menu;
+ }
+
+ return string.Join(" > ", GetAncestors(menu).Reverse().Select(p => p.GetType().FullName));
+ }
+
+ /****
** Sprite batch
****/
/// <summary>Get whether the sprite batch is between a begin and end pair.</summary>
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 1e484709..ee013a85 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -32,10 +32,10 @@ namespace StardewModdingAPI.Framework.Logging
private readonly Regex[] SuppressConsolePatterns =
{