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 /src/SMAPI | |
| 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
Diffstat (limited to 'src/SMAPI')
| -rw-r--r-- | src/SMAPI/Constants.cs | 25 | ||||
| -rw-r--r-- | src/SMAPI/Context.cs | 79 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 76 | ||||
| -rw-r--r-- | src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 130 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Input/SInputState.cs | 17 | ||||
| -rw-r--r-- | src/SMAPI/Framework/InternalExtensions.cs | 18 | ||||
| -rw-r--r-- | src/SMAPI/Framework/Logging/LogManager.cs | 9 | ||||
| -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 | 431 | ||||
| -rw-r--r-- | src/SMAPI/Framework/SGameRunner.cs | 156 | ||||
| -rw-r--r-- | src/SMAPI/Metadata/CoreAssetPropagator.cs | 104 | ||||
| -rw-r--r-- | src/SMAPI/Metadata/InstructionMetadata.cs | 6 | ||||
| -rw-r--r-- | src/SMAPI/Patches/DialogueErrorPatch.cs | 4 | ||||
| -rw-r--r-- | src/SMAPI/Patches/ScheduleErrorPatch.cs | 2 | ||||
| -rw-r--r-- | src/SMAPI/Utilities/PerScreen.cs | 79 |
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 = { |
