diff options
29 files changed, 587 insertions, 229 deletions
diff --git a/build/common.targets b/build/common.targets index a38f15a6..29acbb56 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ <!--set properties --> <PropertyGroup> - <Version>3.9.1</Version> + <Version>3.9.2</Version> <Product>SMAPI</Product> <LangVersion>latest</LangVersion> diff --git a/docs/release-notes-archived.md b/docs/release-notes-archived.md index 9f8de3cb..b5dd538b 100644 --- a/docs/release-notes-archived.md +++ b/docs/release-notes-archived.md @@ -19,7 +19,7 @@ Released 13 September 2019 for Stardew Valley 1.3.36. * Added log parser instructions for Android. * Fixed log parser failing in some cases due to time format localization. -* For modders: +* For mod authors: * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. The change will only take effect when you recompile the mod. * Fixed 'location list changed' verbose log not correctly listing changes. * Fixed mods able to directly load (and in some cases edit) a different mod's local assets using internal asset key forwarding. @@ -42,7 +42,7 @@ Released 17 March 2019 for Stardew Valley 1.3.36. * Updated mod compatibility list. * Fixed `world_clear` console command removing chests edited to have a debris name. -* For modders: +* For mod authors: * Added support for suppressing false-positive warnings in rare cases. * For the web UI: @@ -55,7 +55,7 @@ Released 01 March 2019 for Stardew Valley 1.3.36. * For players: * Updated for Stardew Valley 1.3.36. -* For modders: +* For mod authors: * Bumped all deprecation levels to _pending removal_. * For the web UI: @@ -80,7 +80,7 @@ Released 09 January 2019 for Stardew Valley 1.3.32–33. * Added beta status filter to compatibility list. * Fixed broken ModDrop links in the compatibility list. -* For modders: +* For mod authors: * Asset changes are now propagated into the parsed save being loaded if applicable. * Added locale to context trace logs. * Fixed error loading custom map tilesheets in some cases. @@ -106,7 +106,7 @@ Released 29 December 2018 for Stardew Valley 1.3.32–33. * Minor performance improvements. * Tweaked installer to reduce antivirus false positives. -* For modders: +* For mod authors: * Added [events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events): `GameLoop.OneSecondUpdateTicking`, `GameLoop.OneSecondUpdateTicked`, and `Specialized.LoadStageChanged`. * Added `e.IsCurrentLocation` event arg to `World` events. * You can now use `helper.Data.Read/WriteSaveData` as soon as the save is loaded (instead of once the world is initialized). @@ -133,7 +133,7 @@ Released 16 December 2018 for Stardew Valley 1.3.32. * Fixed game launch errors logged as `SMAPI` instead of `game`. * Fixed Windows installer adding unneeded Unix launcher to game folder. -* For modders: +* For mod authors: * Moved content pack methods into a new [content pack API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content_Packs). * Fixed invalid NPC data propagated when a mod changes NPC dispositions. * Fixed `Display.RenderedWorld` event broken in SMAPI 2.9.1. @@ -162,7 +162,7 @@ Released 07 December 2018 for Stardew Valley 1.3.32. * Fixed empty "mods with warnings" list in some cases due to hidden warnings. * Fixed Console Commands' handling of tool upgrade levels for item commands. -* For modders: +* For mod authors: * Added ModDrop update keys (see [docs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks)). * Added `IsLocalPlayer` to new player events. * Added `helper.CreateTemporaryContentPack` to replace the deprecated `CreateTransitionalContentPack`. @@ -225,7 +225,7 @@ Released 19 November 2018 for Stardew Valley 1.3.32. * Tweaked log parser UI (thanks to danvolchek!). * Fixed log parser instructions for Mac. -* For modders: +* For mod authors: * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data) to store mod data in the save file or app data. * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. * Added [verbose logging](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Logging#Verbose_logging) feature. @@ -275,7 +275,7 @@ Released 14 August 2018 for Stardew Valley 1.3.28. * Fixed some SMAPI logs not deleted when starting a new session. * Updated compatibility list. -* For modders: +* For mod authors: * Added support for `.json` data files in the content API (including Content Patcher). * Added propagation for asset changes through the content API for... * child sprites; @@ -336,7 +336,7 @@ Released 01 August 2018 for Stardew Valley 1.3.27. * Fixed log parser mangling crossplatform paths in some cases. * Fixed `smapi.io/install` not linking to a useful page. -* For modders: +* For mod authors: * Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input. * Added code analysis in the NuGet package to flag common issues as warnings. * Replaced `LocationEvents` to support multiplayer: @@ -418,7 +418,7 @@ Released 26 March 2018 for Stardew Valley 1.2.30–1.2.33. * For the [log parser](https://smapi.io/log): * Fixed error when log text contains certain tokens. -* For modders: +* For mod authors: * Updated to Json.NET 11.0.2. * For SMAPI developers: @@ -448,7 +448,7 @@ Released 13 March 2018 for Stardew Valley ~~1.2.30~~–1.2.33. ## 2.5.2 Released 25 February 2018 for Stardew Valley 1.2.30–1.2.33. -* For modders: +* For mod authors: * Fixed issue where replacing an asset through `asset.AsImage()` or `asset.AsDictionary()` didn't take effect. * For the [log parser](https://smapi.io/log): @@ -472,7 +472,7 @@ Released 24 February 2018 for Stardew Valley 1.2.30–1.2.33. * Fixed uninstall script not reporting when done on Linux/Mac. * Updated compatibility list and enabled update checks for more mods. -* For modders: +* For mod authors: * Added support for content packs and new APIs to read them. * Added support for `ISemanticVersion` in JSON models. * Added `SpecializedEvents.UnvalidatedUpdateTick` event for specialized use cases. @@ -506,7 +506,7 @@ Released 24 January 2018 for Stardew Valley 1.2.30–1.2.33. * For the [log parser](https://smapi.io/log): * Fixed error parsing logs with zero installed mods. -* For modders: +* For mod authors: * Added `SaveEvents.BeforeCreate` and `AfterCreate` events. * Added `SButton` `IsActionButton()` and `IsUseToolButton()` extensions. * Improved JSON parse errors to provide more useful info for troubleshooting. @@ -527,7 +527,7 @@ Released 26 December 2017 for Stardew Valley 1.2.30–1.2.33. * Improved cryptic libgdiplus errors on Mac when Mono isn't installed. * Fixed mod UIs hidden when menu backgrounds are enabled. -* For modders: +* For mod authors: * **Added mod-provided APIs** to allow simple integrations between mods, even without direct assembly references. * Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialized). * Added `IsSuppressed` to input events so mods can optionally avoid handling keys another mod has already handled. @@ -557,7 +557,7 @@ Released 02 December 2017 for Stardew Valley 1.2.30–1.2.33. * Fixed error when uploading very large logs. * Slightly improved the UI. -* For modders: +* For mod authors: * Added `helper.Content.NormalizeAssetName` method. * Added `SDate.DaysSinceStart` property. * Fixed input events' `e.SuppressButton(button)` method ignoring specified button. @@ -575,7 +575,7 @@ Released 01 November 2017 for Stardew Valley 1.2.30–1.2.33. * Fixed compatibility check for players with Stardew Valley 1.08. * Fixed `player_setlevel` command not setting XP too. -* For modders: +* For mod authors: * The reflection API now works with public code to simplify mod integrations. * The content API now lets you invalidated multiple assets at once. * The `InputEvents` have been improved: @@ -600,7 +600,7 @@ Released 14 October 2017 for Stardew Valley 1.2.30–1.2.33. * **Mod update checks** SMAPI now checks if your mods have updates available, and will alert you in the console with a convenient link to the mod page. This works with mods from the Chucklefish mod site, GitHub, or Nexus Mods. SMAPI 2.0 launches with - update-check support for over 250 existing mods, and more will be added as modders enable the feature. + update-check support for over 250 existing mods, and more will be added as mod authors enable the feature. * **Mod stability warnings** SMAPI now detects when a mod contains code which can destabilise your game or corrupt your save, and shows a warning @@ -610,7 +610,7 @@ Released 14 October 2017 for Stardew Valley 1.2.30–1.2.33. The console is now simpler and easier to read, some commands have been streamlined, and the colors now adjust to fit your terminal background color. -* **New features for modders** +* **New features for mod authors** SMAPI 2.0 adds several features to enable new kinds of mods (see [API documentation](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs)). @@ -688,7 +688,7 @@ For players: * Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader. * Fixed errors in rare cases when a mod calculates an in-game date. -For modders: +For mod authors: * Added UTC timestamp to log file. For SMAPI developers: @@ -726,7 +726,7 @@ For players: * Fixed controller mod input broken in 1.15. * Fixed TrainerMod packaging unneeded files. -For modders: +For mod authors: * Fixed mod registry lookups by unique ID not being case-insensitive. ## 1.15 @@ -744,7 +744,7 @@ For players: * Fixed invalid `ObjectInformation.xnb` causing a flood of warnings; SMAPI now shows one error instead. * Updated mod compatibility list. -For modders: +For mod authors: * Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Utilities#Dates)). * Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)). * Added more useful logging when loading mods. @@ -777,7 +777,7 @@ For players: * Bumped minimum game version to 1.2.30. * Updated mod compatibility list. -For modders: +For mod authors: * You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)). * You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation)). * You can now load unpacked `.tbin` files from your mod folder through the content API. @@ -788,7 +788,7 @@ For modders: * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. * Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. * Fixed `debug` command output not printed to console. -* Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most modders think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead. +* Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most mod authors think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead. ## 1.13.1 Released 19 May 2017 for Stardew Valley 1.2.26–1.2.29. diff --git a/docs/release-notes.md b/docs/release-notes.md index dabdada1..fb67d8dc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,10 +3,33 @@ # Release notes <!-- ## Future release -* For modders: +* For mod authors: * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). --> +## 3.9.2 +Released 21 February 2021 for Stardew Valley 1.5.4 or later. + +* For players: + * Added more aggressive memory optimization to reduce `OutOfMemoryException` errors with some mods. + * Improved error when `Stardew Valley.exe` exists but can't be loaded. + * Fixed error running `install on Windows.bat` in very rare cases. + * Fixed `world_settime` command not always updating outdoor ambient lighting _(in Console Commands)_. + +* For mod authors: + * Added early detection of disposed textures so the error details are more relevant _(in Error Handler)_. + * Added error details when an event command fails _(in Error Handler)_. + * Fixed asset propagation for `TileSheets/ChairTiles` not changing existing map seats. + * Fixed edge case when playing in non-English where translatable assets loaded via `IAssetLoader` would no longer be applied after returning to the title screen unless manually invalidated from the cache. + +* For the web UI: + * Updated compatibility list for the new wiki. + * Updated the JSON validator/schema for Content Patcher 1.20. + * Fixed mod compatibility list error if a mod has no name. + +* For SMAPI developers: + * Fixed SMAPI toolkit defaulting the mod type incorrectly if a mod's `manifest.json` has neither `EntryDll` nor `ContentPackFor`. This only affects external tools, since SMAPI itself validates those fields separately. + ## 3.9.1 Released 25 January 2021 for Stardew Valley 1.5.4 or later. @@ -27,7 +50,7 @@ Released 22 January 2021 for Stardew Valley 1.5.4 or later. See [release highlig * Fixed compatibility for very old content packs which still load maps from `.xnb` files. These were broken by map loading changes in Stardew Valley 1.5, but SMAPI now corrects them automatically. * Fixed some broken mods incorrectly listed as XNB mods under 'skipped mods'. -* For modders: +* For mod authors: * Added new input APIs: * Added an [API for multi-key bindings](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). * Added a new [`Input.ButtonsChanged` event](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged). @@ -61,7 +84,7 @@ Released 15 January 2021 for Stardew Valley 1.5.3 or later. * Updated for Stardew Valley 1.5.3. * Fixed issue where title screen music didn't stop after loading a save. -* For modders: +* For mod authors: * Fixed `SemanticVersion` comparisons returning wrong value in rare cases. ## 3.8.3 @@ -73,7 +96,7 @@ Released 08 January 2021 for Stardew Valley 1.5.2 or later. * You can now enter console commands for a specific screen in split-screen mode by adding `screen=ID` to the command. * Typing `help` in the SMAPI console is now more helpful. -* For modders: +* For mod authors: * Simplified tilesheet order warning added in SMAPI 3.8.2. * For the Console Commands mod: @@ -90,7 +113,7 @@ Released 03 January 2021 for Stardew Valley 1.5.1 or later. * Fixed 'skipped mods' section repeating mods in some cases. * Fixed out-of-date error text. -* For modders: +* For mod authors: * Added warning when a map replacement changes the order/IDs of the original tilesheets, which may cause errors and crashes. Doing so for a farm map is blocked outright since that causes a consistent crash in Stardew Valley 1.5. * Message data from the `ModMessageReceived` event now uses the same serializer settings as the rest of SMAPI. That mainly adds support for sending crossplatform `Color`, `Point`, `Vector2`, `Rectangle`, and `SemanticVersion` fields through network messages. * When a mod is blocked by SMAPI's compatibility override list, the `TRACE` messages while loading it now say so and indicate why. @@ -102,7 +125,7 @@ Released 26 December 2020 for Stardew Valley 1.5.1 or later. * For players: * Fixed broken community center bundles for non-English saves created in Stardew Valley 1.5. Affected saves will be fixed automatically on load. -* For modders: +* For mod authors: * World events are now raised for volcano dungeon levels. * Added `apply_save_fix` command to reapply a save migration in exceptional cases. This should be used very carefully. Type `help apply_save_fix` for details. * **Deprecation notice:** the `Helper.ConsoleCommands.Trigger` method is now deprecated and should no longer be used. See [integration APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations) for better mod integration options. It will eventually be removed in SMAPI 4.0. @@ -118,7 +141,7 @@ Released 21 December 2020 for Stardew Valley 1.5 or later. See [release highligh * You can now run the installer from a subfolder of your game folder to auto-detect it. That simplifies installation if you have multiple copies of the game or it can't otherwise auto-detect the game path. * Clarified error when the SMAPI installer is in the `Mods` folder. -* For modders: +* For mod authors: * 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. @@ -135,7 +158,7 @@ Released 21 November 2020 for Stardew Valley 1.4.1 or later. * Fixed error when heuristically rewriting an outdated mod in rare cases. * Fixed rare 'collection was modified' error when using `harmony summary` console command. -* For modders: +* For mod authors: * Updated TMXTile to 1.5.8 to fix exported `.tmx` files losing tile index properties. * For the Console Commands mod: @@ -145,7 +168,7 @@ Released 21 November 2020 for Stardew Valley 1.4.1 or later. ## 3.7.5 Released 16 October 2020 for Stardew Valley 1.4.1 or later. -* For modders: +* For mod authors: * Fixed changes to the town map asset not reapplying the game's community center, JojaMart, and Pam house changes. ## 3.7.4 @@ -155,7 +178,7 @@ Released 03 October 2020 for Stardew Valley 1.4.1 or later. * Improved performance on some older computers (thanks to millerscout!). * Fixed update alerts for Chucklefish forum mods broken by a recent site change. -* For modders: +* For mod authors: * Updated dependencies (including Mono.Cecil 0.11.2 → 0.11.3 and Platonymous.TMXTile 1.3.8 → 1.5.6). * Fixed asset propagation for `Data\MoviesReactions`. * Fixed error in content pack path handling when you pass a null path. @@ -174,7 +197,7 @@ Released 16 September 2020 for Stardew Valley 1.4.1 or later. * Fixed map rendering crash due to conflict between SMAPI and PyTK. * Fixed error in heuristically-rewritten mods in rare cases (thanks to collaboration with ZaneYork!). -* For modders: +* For mod authors: * File paths accessed through `IContentPack` are now case-insensitive (even on Linux). * For the web UI: @@ -186,7 +209,7 @@ Released 08 September 2020 for Stardew Valley 1.4.1 or later. * For players: * Fixed mod recipe changes not always applied in 3.7. -* For modders: +* For mod authors: * Renamed `PathUtilities.NormalizePathSeparators` to `NormalizePath`, and added normalization for more cases. ## 3.7.1 @@ -211,7 +234,7 @@ Released 07 September 2020 for Stardew Valley 1.4.1 or later. See [release highl * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * Internal changes to prepare for upcoming game updates. -* For modders: +* For mod authors: * Added `PathUtilities` to simplify working with file/asset names. * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). * Fixed asset propagation not updating title menu buttons immediately on Linux/Mac. @@ -241,7 +264,7 @@ Released 02 August 2020 for Stardew Valley 1.4.1 or later. * Fixed spawned Floor TV not functional as a TV (thanks to Platonymous!). * Fixed spawned sturgeon roe having incorrect color. -* For modders: +* For mod authors: * Updated internal dependencies. * SMAPI now ignores more file types when scanning for mod folders (`.doc`, `.docx`, `.rar`, and `.zip`). * Added current GPU to trace logs to simplify troubleshooting. @@ -274,7 +297,7 @@ Released 20 June 2020 for Stardew Valley 1.4.1 or later. See [release highlights * Updated ModDrop URLs. * Internal changes to improve performance and reliability. -* For modders: +* For mod authors: * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs. @@ -309,7 +332,7 @@ Released 27 April 2020 for Stardew Valley 1.4.1 or later. See [release highlight * Updated the JSON validator/schema for Content Patcher 1.13. * Fixed rare intermittent "CGI application encountered an error" errors. -* For modders: +* For mod authors: * Added map patching to the content API (via `asset.AsMap()`). * Added support for using patch helpers with arbitrary data (via `helper.Content.GetPatchHelper`). * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). @@ -323,7 +346,7 @@ Released 27 April 2020 for Stardew Valley 1.4.1 or later. See [release highlight ## 3.4.1 Released 24 March 2020 for Stardew Valley 1.4.1 or later. -* For modders: +* For mod authors: * Asset changes now propagate to NPCs in an event (e.g. wedding sprites). * Fixed mouse input suppression not working in SMAPI 3.4. @@ -336,7 +359,7 @@ Released 22 March 2020 for Stardew Valley 1.4.1 or later. See [release highlight * Removed invalid-location check now handled by the game. * Updated translations. Thanks to Annosz (added Hungarian)! -* For modders: +* For mod authors: * Added support for flipped and rotated map tiles (thanks to collaboration with Platonymous!). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). * Added `this.Monitor.LogOnce` method. @@ -380,7 +403,7 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later. See [release highli * Updated the JSON validator and Content Patcher schema for `.tmx` support. * The mod compatibility page now has a sticky table header. -* For modders: +* For mod authors: * Added support for [message sending](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Message_sending) to mods on the current computer (in addition to remote computers). * Added `ExtendImage` method to content API when editing files to resize textures. * Added `helper.Input.GetState` to get the low-level state of a button. @@ -415,7 +438,7 @@ Released 01 February 2020 for Stardew Valley 1.4.1 or later. See [release highli * Fixed extra files under `Saves` (e.g. manual backups) not being ignored. * Fixed Android issue where game files were backed up. -* For modders: +* For mod authors: * Added support for `.tmx` map files. (Thanks to [Platonymous for the underlying library](https://github.com/Platonymous/TMXTile)!) * Added special handling for `Vector2` values in `.json` files, so they work consistently crossplatform. * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. @@ -464,7 +487,7 @@ Released 05 January 2019 for Stardew Valley 1.4.1 or later. See [release highlig * Fixed log parser not correctly handling content packs with no author (thanks to danvolchek!). * Fixed main sidebar link pointing to wiki instead of home page. -* For modders: +* For mod authors: * Added `World.ChestInventoryChanged` event (thanks to collaboration with wartech0!). * Added asset propagation for... * grass textures; @@ -493,7 +516,7 @@ Released 02 December 2019 for Stardew Valley 1.4 or later. * If a log can't be uploaded to Pastebin (e.g. due to rate limits), it's now uploaded to Amazon S3 instead. Logs uploaded to S3 expire after one month. * Fixed JSON validator not letting you drag & drop a file. -* For modders: +* For mod authors: * `SemanticVersion` now supports [semver 2.0](https://semver.org/) build metadata. ## 3.0 @@ -523,7 +546,7 @@ For players: * **Fixed many bugs and edge cases.** -For modders: +For mod authors: * **New event system.** SMAPI 3.0 removes the deprecated static events in favor of the new `helper.Events` API. The event engine is rewritten to make events more efficient, add events that weren't possible before, make @@ -607,7 +630,7 @@ For modders: * Added instructions for Android. * The page now detects your OS and preselects the right instructions (thanks to danvolchek!). -### For modders +### For mod authors * Breaking changes: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialized when `Entry` is called; use the `GameLaunched` event if you need to run code when the game is initialized. * Removed all deprecated APIs. diff --git a/src/SMAPI.Installer/assets/windows-install.bat b/src/SMAPI.Installer/assets/windows-install.bat index d02dd4c6..2cc54e80 100644 --- a/src/SMAPI.Installer/assets/windows-install.bat +++ b/src/SMAPI.Installer/assets/windows-install.bat @@ -4,5 +4,5 @@ if not errorlevel 1 ( echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first. pause ) else ( - start /WAIT /B internal/windows-install.exe + start /WAIT /B ./internal/windows-install.exe ) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index 6782e38a..2d4b4565 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -1,5 +1,5 @@ -using System; using System.Linq; +using Microsoft.Xna.Framework; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -45,12 +45,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World /// <param name="time">The time of day.</param> private void SafelySetTime(int time) { - // define conversion between game time and TimeSpan - TimeSpan ToTimeSpan(int value) => new TimeSpan(0, value / 100, value % 100, 0); - int FromTimeSpan(TimeSpan span) => (span.Hours * 100) + span.Minutes; - // transition to new time - int intervals = (int)((ToTimeSpan(time) - ToTimeSpan(Game1.timeOfDay)).TotalMinutes / 10); + int intervals = Utility.CalculateMinutesBetweenTimes(Game1.timeOfDay, time) / 10; if (intervals > 0) { for (int i = 0; i < intervals; i++) @@ -60,10 +56,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { for (int i = 0; i > intervals; i--) { - Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 minutes so game updates to next interval + Game1.timeOfDay = Utility.ModifyTime(Game1.timeOfDay, -20); // offset 20 mins so game updates to next interval Game1.performTenMinuteClockUpdate(); } } + + // reset ambient light + // White is the default non-raining color. If it's raining or dark out, UpdateGameClock + // below will update it automatically. + Game1.outdoorLight = Color.White; + Game1.ambientLight = Color.White; + + // run clock update (to correct lighting, etc) + Game1.gameTimeInterval = 0; + Game1.UpdateGameClock(Game1.currentGameTime); } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 10611e08..aa3d6ceb 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.9.1", + "Version": "3.9.2", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.9.1" + "MinimumApiVersion": "3.9.2" } diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index 2f6f1939..d9426d75 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -26,21 +26,17 @@ namespace StardewModdingAPI.Mods.ErrorHandler public override void Entry(IModHelper helper) { // get SMAPI core types - SCore core = SCore.Instance; - LogManager logManager = core.GetType().GetField("LogManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(core) as LogManager; - if (logManager == null) - { - this.Monitor.Log($"Can't access SMAPI's internal log manager. Error-handling patches won't be applied.", LogLevel.Error); - return; - } + IMonitor monitorForGame = this.GetMonitorForGame(); // apply patches new GamePatcher(this.Monitor).Apply( - new EventErrorPatch(logManager.MonitorForGame), - new DialogueErrorPatch(logManager.MonitorForGame, this.Helper.Reflection), + new DialogueErrorPatch(monitorForGame, this.Helper.Reflection), + new EventPatches(monitorForGame), + new GameLocationPatches(monitorForGame), new ObjectErrorPatch(), new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved), - new ScheduleErrorPatch(logManager.MonitorForGame), + new ScheduleErrorPatch(monitorForGame), + new SpriteBatchValidationPatches(), new UtilityErrorPatches() ); @@ -61,7 +57,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler /// <summary>The method invoked when a save is loaded.</summary> /// <param name="sender">The event sender.</param> /// <param name="e">The event arguments.</param> - public void OnSaveLoaded(object sender, SaveLoadedEventArgs e) + private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) { // show in-game warning for removed save content if (this.IsSaveContentRemoved) @@ -70,5 +66,16 @@ namespace StardewModdingAPI.Mods.ErrorHandler Game1.addHUDMessage(new HUDMessage(this.Helper.Translation.Get("warn.invalid-content-removed"), HUDMessage.error_type)); } } + + /// <summary>Get the monitor with which to log game errors.</summary> + private IMonitor GetMonitorForGame() + { + SCore core = SCore.Instance; + LogManager logManager = core.GetType().GetField("LogManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(core) as LogManager; + if (logManager == null) + this.Monitor.Log("Can't access SMAPI's internal log manager. Some game errors may be reported as being from Error Handler.", LogLevel.Error); + + return logManager?.MonitorForGame ?? this.Monitor; + } } } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs new file mode 100644 index 00000000..a15c1d32 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics.CodeAnalysis; +#if HARMONY_2 +using HarmonyLib; +#else +using Harmony; +#endif +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// <summary>Harmony patches for <see cref="Event"/> which intercept errors to log more details.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class EventPatches : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// <summary>Writes messages to the console and log file on behalf of the game.</summary> + private static IMonitor MonitorForGame; + + + /********* + ** Accessors + *********/ + /// <inheritdoc /> + public string Name => nameof(EventPatches); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> + public EventPatches(IMonitor monitorForGame) + { + EventPatches.MonitorForGame = monitorForGame; + } + + /// <inheritdoc /> +#if HARMONY_2 + public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif + { + harmony.Patch( + original: AccessTools.Method(typeof(Event), nameof(Event.LogErrorAndHalt)), + postfix: new HarmonyMethod(this.GetType(), nameof(EventPatches.After_Event_LogErrorAndHalt)) + ); + } + + + /********* + ** Private methods + *********/ + /// <summary>The method to call after <see cref="Event.LogErrorAndHalt"/>.</summary> + /// <param name="e">The exception being logged.</param> + private static void After_Event_LogErrorAndHalt(Exception e) + { + EventPatches.MonitorForGame.Log(e.ToString(), LogLevel.Error); + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs index fabc6cad..c10f2de7 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs @@ -15,7 +15,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class EventErrorPatch : IHarmonyPatch + internal class GameLocationPatches : IHarmonyPatch { /********* ** Fields @@ -28,7 +28,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches ** Accessors *********/ /// <inheritdoc /> - public string Name => nameof(EventErrorPatch); + public string Name => nameof(GameLocationPatches); /********* @@ -36,9 +36,9 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches *********/ /// <summary>Construct an instance.</summary> /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param> - public EventErrorPatch(IMonitor monitorForGame) + public GameLocationPatches(IMonitor monitorForGame) { - EventErrorPatch.MonitorForGame = monitorForGame; + GameLocationPatches.MonitorForGame = monitorForGame; } /// <inheritdoc /> @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches { harmony.Patch( original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), - prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition)) + prefix: new HarmonyMethod(this.GetType(), nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition)) ); } #endif @@ -89,7 +89,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// <returns>Returns whether to execute the original method.</returns> private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) { - const string key = nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition); + const string key = nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition); if (!PatchHelper.StartIntercept(key)) return true; @@ -101,7 +101,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches catch (TargetInvocationException ex) { __result = -1; - EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + GameLocationPatches.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); return false; } finally diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs new file mode 100644 index 00000000..0211cfb1 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs @@ -0,0 +1,63 @@ +#if HARMONY_2 +using HarmonyLib; +#else +using Harmony; +#endif +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Patching; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// <summary>Harmony patch for <see cref="SpriteBatch"/> to validate textures earlier.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class SpriteBatchValidationPatches : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// <inheritdoc /> + public string Name => nameof(SpriteBatchValidationPatches); + + + /********* + ** Public methods + *********/ + /// <inheritdoc /> +#if HARMONY_2 + public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif + { + harmony.Patch( +#if SMAPI_FOR_WINDOWS + original: AccessTools.Method(typeof(SpriteBatch), "InternalDraw"), +#else + original: AccessTools.Method(typeof(SpriteBatch), "CheckValid", new[] { typeof(Texture2D) }), +#endif + postfix: new HarmonyMethod(this.GetType(), nameof(SpriteBatchValidationPatches.After_SpriteBatch_CheckValid)) + ); + } + + + /********* + ** Private methods + *********/ +#if SMAPI_FOR_WINDOWS + /// <summary>The method to call instead of <see cref="SpriteBatch.InternalDraw"/>.</summary> + /// <param name="texture">The texture to validate.</param> +#else + /// <summary>The method to call instead of <see cref="SpriteBatch.CheckValid"/>.</summary> + /// <param name="texture">The texture to validate.</param> +#endif + private static void After_SpriteBatch_CheckValid(Texture2D texture) + { + if (texture?.IsDisposed == true) + throw new ObjectDisposedException("Cannot draw this texture because it's disposed."); + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index bb9942d1..b6df0f49 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.9.1", + "Version": "3.9.2", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.9.1" + "MinimumApiVersion": "3.9.2" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 95ee5144..4d2003e2 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.9.1", + "Version": "3.9.2", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.9.1" + "MinimumApiVersion": "3.9.2" } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 89a22eaf..da312471 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -50,13 +50,13 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki doc.LoadHtml(html); // fetch game versions - string stableVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-stable-version']")?.InnerText; - string betaVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-beta-version']")?.InnerText; + string stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText; + string betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText; if (betaVersion == stableVersion) betaVersion = null; // find mod entries - HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); + HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("//table[@id='mod-list']//tr[@class='mod']"); if (modNodes == null) throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 86a97016..fd206d9d 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -177,12 +177,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } // get mod type - ModType type = ModType.Invalid; - if (manifest != null) + ModType type; { - type = !string.IsNullOrWhiteSpace(manifest.ContentPackFor?.UniqueID) - ? ModType.ContentPack - : ModType.Smapi; + bool isContentPack = !string.IsNullOrWhiteSpace(manifest?.ContentPackFor?.UniqueID); + bool isSmapi = !string.IsNullOrWhiteSpace(manifest?.EntryDll); + + if (isContentPack == isSmapi) + type = ModType.Invalid; + else if (isContentPack) + type = ModType.ContentPack; + else + type = ModType.Smapi; } // build result diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index 24e36709..c62ed605 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Web.Controllers mods: this.Cache .GetWikiMods() .Select(mod => new ModModel(mod.Data)) - .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting + .OrderBy(p => Regex.Replace((p.Name ?? "").ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting lastUpdated: metadata.LastUpdated, isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes) ); diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 92149f4d..21514979 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -11,9 +11,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", "type": "string", - "const": "1.19.0", + "const": "1.20.0", "@errorMessages": { - "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.19.0'." + "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.20.0'." } }, "ConfigSchema": { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 57c40bbf..54fb54ab 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -54,7 +54,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.2"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 77dd6c72..32195fff 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework /// <summary>An asset key prefix for assets from SMAPI mod folders.</summary> private readonly string ManagedPrefix = "SMAPI"; + /// <summary>Whether to enable more aggressive memory optimizations.</summary> + private readonly bool AggressiveMemoryOptimizations; + /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; @@ -51,7 +54,7 @@ namespace StardewModdingAPI.Framework private bool IsDisposed; /// <summary>A lock used to prevent asynchronous changes to the content manager list.</summary> - /// <remarks>The game may adds content managers in asynchronous threads (e.g. when populating the load screen).</remarks> + /// <remarks>The game may add content managers in asynchronous threads (e.g. when populating the load screen).</remarks> private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); /// <summary>A cache of ordered tilesheet IDs used by vanilla maps.</summary> @@ -91,8 +94,10 @@ namespace StardewModdingAPI.Framework /// <param name="reflection">Simplifies access to private code.</param> /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> /// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param> - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset) + /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations) { + this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Reflection = reflection; this.JsonHelper = jsonHelper; @@ -108,11 +113,25 @@ namespace StardewModdingAPI.Framework monitor: monitor, reflection: reflection, onDisposing: this.OnDisposing, - onLoadingFirstAsset: onLoadingFirstAsset + onLoadingFirstAsset: onLoadingFirstAsset, + aggressiveMemoryOptimizations: aggressiveMemoryOptimizations ) ); + var contentManagerForAssetPropagation = new GameContentManagerForAssetPropagation( + name: nameof(GameContentManagerForAssetPropagation), + serviceProvider: serviceProvider, + rootDirectory: rootDirectory, + currentCulture: currentCulture, + coordinator: this, + monitor: monitor, + reflection: reflection, + onDisposing: this.OnDisposing, + onLoadingFirstAsset: onLoadingFirstAsset, + aggressiveMemoryOptimizations: aggressiveMemoryOptimizations + ); + this.ContentManagers.Add(contentManagerForAssetPropagation); this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, reflection, aggressiveMemoryOptimizations); } /// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary> @@ -130,7 +149,8 @@ namespace StardewModdingAPI.Framework monitor: this.Monitor, reflection: this.Reflection, onDisposing: this.OnDisposing, - onLoadingFirstAsset: this.OnLoadingFirstAsset + onLoadingFirstAsset: this.OnLoadingFirstAsset, + aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations ); this.ContentManagers.Add(manager); return manager; @@ -157,7 +177,8 @@ namespace StardewModdingAPI.Framework monitor: this.Monitor, reflection: this.Reflection, jsonHelper: this.JsonHelper, - onDisposing: this.OnDisposing + onDisposing: this.OnDisposing, + aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations ); this.ContentManagers.Add(manager); return manager; @@ -182,6 +203,17 @@ namespace StardewModdingAPI.Framework }); } + /// <summary>Clean up when the player is returning to the title screen.</summary> + /// <remarks>This is called after the player returns to the title screen, but before <see cref="Game1.CleanupReturningToTitle"/> runs.</remarks> + public void OnReturningToTitleScreen() + { + this.ContentManagerLock.InReadLock(() => + { + foreach (IContentManager contentManager in this.ContentManagers) + contentManager.OnReturningToTitleScreen(); + }); + } + /// <summary>Get whether this asset is mapped to a mod folder.</summary> /// <param name="key">The asset key.</param> public bool IsManagedAssetKey(string key) @@ -290,7 +322,7 @@ namespace StardewModdingAPI.Framework // reload core game assets if (removedAssets.Any()) { - IDictionary<string, bool> propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager + IDictionary<string, bool> propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); } else diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 92264f8c..1a64dab8 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -5,12 +5,12 @@ using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewValley; +using xTile; namespace StardewModdingAPI.Framework.ContentManagers { @@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Encapsulates monitoring and logging.</summary> protected readonly IMonitor Monitor; + /// <summary>Whether to enable more aggressive memory optimizations.</summary> + protected readonly bool AggressiveMemoryOptimizations; + /// <summary>Whether the content coordinator has been disposed.</summary> private bool IsDisposed; @@ -49,16 +52,16 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Accessors *********/ - /// <summary>A name for the mod manager. Not guaranteed to be unique.</summary> + /// <inheritdoc /> public string Name { get; } - /// <summary>The current language as a constant.</summary> + /// <inheritdoc /> public LanguageCode Language => this.GetCurrentLanguage(); - /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary> + /// <inheritdoc /> public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - /// <summary>Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder).</summary> + /// <inheritdoc /> public bool IsNamespaced { get; } @@ -75,7 +78,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="reflection">Simplifies access to private code.</param> /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param> /// <param name="isNamespaced">Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder).</param> - protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced) + /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> + protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced, bool aggressiveMemoryOptimizations) : base(serviceProvider, rootDirectory, currentCulture) { // init @@ -85,59 +89,49 @@ namespace StardewModdingAPI.Framework.ContentManagers this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.OnDisposing = onDisposing; this.IsNamespaced = isNamespaced; + this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; // get asset data this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.OrdinalIgnoreCase); this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue(); } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <inheritdoc /> public override T Load<T>(string assetName) { return this.Load<T>(assetName, this.Language, useCache: true); } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language code for which to load content.</param> + /// <inheritdoc /> public override T Load<T>(string assetName, LanguageCode language) { return this.Load<T>(assetName, language, useCache: true); } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language code for which to load content.</param> - /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + /// <inheritdoc /> public abstract T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); - /// <summary>Load the base asset without localization.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <inheritdoc /> [Obsolete("This method is implemented for the base game and should not be used directly. To load an asset from the underlying content manager directly, use " + nameof(BaseContentManager.RawLoad) + " instead.")] public override T LoadBase<T>(string assetName) { return this.Load<T>(assetName, LanguageCode.en, useCache: true); } - /// <summary>Perform any cleanup needed when the locale changes.</summary> + /// <inheritdoc /> public virtual void OnLocaleChanged() { } - /// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary> - /// <param name="path">The file path to normalize.</param> + /// <inheritdoc /> + public virtual void OnReturningToTitleScreen() { } + + /// <inheritdoc /> [Pure] public string NormalizePathSeparators(string path) { return this.Cache.NormalizePathSeparators(path); } - /// <summary>Assert that the given key has a valid format and return a normalized form consistent with the underlying cache.</summary> - /// <param name="assetName">The asset key to check.</param> - /// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception> + /// <inheritdoc /> [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] public string AssertAndNormalizeAssetName(string assetName) { @@ -154,29 +148,26 @@ namespace StardewModdingAPI.Framework.ContentManagers /**** ** Content loading ****/ - /// <summary>Get the current content locale.</summary> + /// <inheritdoc /> public string GetLocale() { return this.GetLocale(this.GetCurrentLanguage()); } - /// <summary>The locale for a language.</summary> - /// <param name="language">The language.</param> + /// <inheritdoc /> public string GetLocale(LanguageCode language) { return this.LanguageCodeString(language); } - /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language.</param> + /// <inheritdoc /> public bool IsLoaded(string assetName, LanguageCode language) { assetName = this.Cache.NormalizeKey(assetName); return this.IsNormalizedKeyLoaded(assetName, language); } - /// <summary>Get the cached asset keys.</summary> + /// <inheritdoc /> public IEnumerable<string> GetAssetKeys() { return this.Cache.Keys @@ -187,10 +178,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /**** ** Cache invalidation ****/ - /// <summary>Purge matched assets from the cache.</summary> - /// <param name="predicate">Matches the asset keys to invalidate.</param> - /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param> - /// <returns>Returns the invalidated asset names and instances.</returns> + /// <inheritdoc /> public IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false) { IDictionary<string, object> removeAssets = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); @@ -198,21 +186,28 @@ namespace StardewModdingAPI.Framework.ContentManagers { this.ParseCacheKey(key, out string assetName, out _); - if (removeAssets.ContainsKey(assetName)) - return true; - if (predicate(assetName, asset.GetType())) + // check if asset should be removed + bool remove = removeAssets.ContainsKey(assetName); + if (!remove && predicate(assetName, asset.GetType())) { removeAssets[assetName] = asset; - return true; + remove = true; + } + + // dispose if safe + if (remove && this.AggressiveMemoryOptimizations) + { + if (asset is Map map) + map.DisposeTileSheets(Game1.mapDisplayDevice); } - return false; + + return remove; }, dispose); return removeAssets; } - /// <summary>Dispose held resources.</summary> - /// <param name="isDisposing">Whether the content manager is being disposed (rather than finalized).</param> + /// <inheritdoc /> protected override void Dispose(bool isDisposing) { // ignore if disposed diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 665c019b..8e78faba 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -52,17 +52,14 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="reflection">Simplifies access to private code.</param> /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param> /// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param> - 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) + /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> + public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations) { this.OnLoadingFirstAsset = onLoadingFirstAsset; } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language code for which to load content.</param> - /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + /// <inheritdoc /> public override T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache) { // raise first-load callback @@ -94,7 +91,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.AssetsBeingLoaded.Contains(assetName)) { this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}"); data = this.RawLoad<T>(assetName, language, useCache); } else @@ -116,7 +113,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } - /// <summary>Perform any cleanup needed when the locale changes.</summary> + /// <inheritdoc /> public override void OnLocaleChanged() { base.OnLocaleChanged(); @@ -136,10 +133,35 @@ namespace StardewModdingAPI.Framework.ContentManagers .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) .ToArray(); if (invalidated.Any()) - this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.", LogLevel.Trace); + this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change."); + } + + /// <inheritdoc /> + public override void OnReturningToTitleScreen() + { + // The game clears LocalizedContentManager.localizedAssetNames after returning to the title screen. That + // causes an inconsistency in the SMAPI asset cache, which leads to an edge case where assets already + // provided by mods via IAssetLoader when playing in non-English are ignored. + // + // For example, let's say a mod provides the 'Data\mail' asset through IAssetLoader when playing in + // Portuguese. Here's the normal load process after it's loaded: + // 1. The game requests Data\mail. + // 2. SMAPI sees that it's already cached, and calls LoadRaw to bypass asset interception. + // 3. LoadRaw sees that there's a localized key mapping, and gets the mapped key. + // 4. In this case "Data\mail" is mapped to "Data\mail" since it was loaded by a mod, so it loads that + // asset. + // + // When the game clears localizedAssetNames, that process goes wrong in step 4: + // 3. LoadRaw sees that there's no localized key mapping *and* the locale is non-English, so it attempts + // to load from the localized key format. + // 4. In this case that's 'Data\mail.pt-BR', so it successfully loads that asset. + // 5. Since we've bypassed asset interception at this point, it's loaded directly from the base content + // manager without mod changes. + if (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.en) + this.InvalidateCache((_, _) => true); } - /// <summary>Create a new content manager for temporary use.</summary> + /// <inheritdoc /> public override LocalizedContentManager CreateTemporary() { return this.Coordinator.CreateGameContentManager("(temporary)"); @@ -149,9 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// <summary>Get whether an asset has already been loaded.</summary> - /// <param name="normalizedAssetName">The normalized asset name.</param> - /// <param name="language">The language to check.</param> + /// <inheritdoc /> protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language) { string cachedKey = null; @@ -165,12 +185,7 @@ namespace StardewModdingAPI.Framework.ContentManagers : this.Cache.ContainsKey(normalizedAssetName); } - /// <summary>Add tracking data to an asset and add it to the cache.</summary> - /// <typeparam name="T">The type of asset to inject.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="value">The asset value.</param> - /// <param name="language">The language code for which to inject the asset.</param> - /// <param name="useCache">Whether to save the asset to the asset cache.</param> + /// <inheritdoc /> protected override void TrackAsset<T>(string assetName, T value, LanguageCode language, bool useCache) { // handle explicit language in asset name @@ -358,7 +373,7 @@ namespace StardewModdingAPI.Framework.ContentManagers try { editor.Edit<T>(asset); - this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}.", LogLevel.Trace); + this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}."); } catch (Exception ex) { diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs new file mode 100644 index 00000000..61683ce6 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs @@ -0,0 +1,47 @@ +using System; +using System.Globalization; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// <summary>An extension of <see cref="GameContentManager"/> specifically optimized for asset propagation.</summary> + /// <remarks>This avoids sharing an asset cache with <see cref="Game1.content"/> or mods, so that assets can be safely disposed when the vanilla game no longer references them.</remarks> + internal class GameContentManagerForAssetPropagation : GameContentManager + { + /********* + ** Fields + *********/ + /// <summary>A unique value used in <see cref="Texture2D"/> to identify assets loaded through this instance.</summary> + private readonly string Tag = $"Pathoschild.SMAPI/LoadedBy:{nameof(GameContentManagerForAssetPropagation)}"; + + + /********* + ** Public methods + *********/ + /// <inheritdoc /> + public GameContentManagerForAssetPropagation(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, aggressiveMemoryOptimizations) { } + + /// <inheritdoc /> + public override T Load<T>(string assetName, LanguageCode language, bool useCache) + { + T data = base.Load<T>(assetName, language, useCache); + + if (data is Texture2D texture) + texture.Tag = this.Tag; + + return data; + } + + /// <summary>Get whether a texture was loaded by this content manager.</summary> + /// <param name="texture">The texture to check.</param> + public bool IsResponsibleFor(Texture2D texture) + { + return + texture?.Tag is string tag + && tag.Contains(this.Tag); + } + } +} diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 0e7edd8f..1e222472 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -36,9 +36,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); - /// <summary>Perform any cleanup needed when the locale changes.</summary> - void OnLocaleChanged(); - /// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary> /// <param name="path">The file path to normalize.</param> [Pure] @@ -69,5 +66,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param> /// <returns>Returns the invalidated asset names and instances.</returns> IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false); + + /// <summary>Perform any cleanup needed when the locale changes.</summary> + void OnLocaleChanged(); + + /// <summary>Clean up when the player is returning to the title screen.</summary> + /// <remarks>This is called after the player returns to the title screen, but before <see cref="Game1.CleanupReturningToTitle"/> runs.</remarks> + void OnReturningToTitleScreen(); } } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1456d3c1..9af14cb5 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -50,36 +50,28 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="reflection">Simplifies access to private code.</param> /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> /// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param> - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing) - : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) + /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, bool aggressiveMemoryOptimizations) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations) { this.GameContentManager = gameContentManager; this.JsonHelper = jsonHelper; this.ModName = modName; } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <inheritdoc /> public override T Load<T>(string assetName) { return this.Load<T>(assetName, this.DefaultLanguage, useCache: false); } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language code for which to load content.</param> + /// <inheritdoc /> public override T Load<T>(string assetName, LanguageCode language) { return this.Load<T>(assetName, language, useCache: false); } - /// <summary>Load an asset that has been processed by the content pipeline.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> - /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - /// <param name="language">The language code for which to load content.</param> - /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param> + /// <inheritdoc /> public override T Load<T>(string assetName, LanguageCode language, bool useCache) { assetName = this.AssertAndNormalizeAssetName(assetName); @@ -189,7 +181,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return asset; } - /// <summary>Create a new content manager for temporary use.</summary> + /// <inheritdoc /> public override LocalizedContentManager CreateTemporary() { throw new NotSupportedException("Can't create a temporary mod content manager."); @@ -209,9 +201,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// <summary>Get whether an asset has already been loaded.</summary> - /// <param name="normalizedAssetName">The normalized asset name.</param> - /// <param name="language">The language to check.</param> + /// <inheritdoc /> protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language) { return this.Cache.ContainsKey(normalizedAssetName); diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index ff00cff7..2c7be399 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using StardewModdingAPI.Framework.Commands; +using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit.Framework.ModData; @@ -284,19 +285,22 @@ namespace StardewModdingAPI.Framework.Logging } /// <summary>Log details for settings that don't match the default.</summary> - /// <param name="isDeveloperMode">Whether to enable full console output for developers.</param> - /// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param> - /// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param> - public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates, bool rewriteMods) + /// <param name="settings">The settings to log.</param> + public void LogSettingsHeader(SConfig settings) { - if (isDeveloperMode) - this.Monitor.Log("You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI.", LogLevel.Info); - if (!checkForUpdates) - this.Monitor.Log("You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI.", LogLevel.Warn); - if (!rewriteMods) - this.Monitor.Log("You configured SMAPI to not rewrite broken mods. Many older mods may fail to load. You can undo this by reinstalling SMAPI.", LogLevel.Warn); + // developer mode + if (settings.DeveloperMode) + this.Monitor.Log("You enabled developer mode, so the console will be much more verbose. You can disable it by installing the non-developer version of SMAPI.", LogLevel.Info); + + // warnings + if (!settings.CheckForUpdates) + this.Monitor.Log("You disabled update checks, so you won't be notified of new SMAPI or mod updates. Running an old version of SMAPI is not recommended. You can undo this by reinstalling SMAPI.", LogLevel.Warn); + if (!settings.RewriteMods) + this.Monitor.Log("You disabled rewriting broken mods, so many older mods may fail to load. You can undo this by reinstalling SMAPI.", LogLevel.Info); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + + // verbose logging this.Monitor.VerboseLog("Verbose logging enabled."); } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index dea08717..4a80e34c 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -21,7 +21,8 @@ namespace StardewModdingAPI.Framework.Models [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", [nameof(VerboseLogging)] = false, [nameof(LogNetworkTraffic)] = false, - [nameof(RewriteMods)] = true + [nameof(RewriteMods)] = true, + [nameof(AggressiveMemoryOptimizations)] = true }; /// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary> @@ -60,6 +61,9 @@ namespace StardewModdingAPI.Framework.Models /// <summary>Whether SMAPI should rewrite mods for compatibility.</summary> public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; + /// <summary>Whether to enable more aggressive memory optimizations.</summary> + public bool AggressiveMemoryOptimizations { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)]; + /// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary> public bool LogNetworkTraffic { get; set; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index cd094ff4..2d783eb2 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -277,7 +277,7 @@ namespace StardewModdingAPI.Framework // log basic info this.LogManager.HandleMarkerFiles(); - this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates, this.Settings.RewriteMods); + this.LogManager.LogSettingsHeader(this.Settings); // set window titles this.SetWindowTitles( @@ -1118,6 +1118,7 @@ namespace StardewModdingAPI.Framework { // perform cleanup this.Multiplayer.CleanupOnMultiplayerExit(); + this.ContentCore.OnReturningToTitleScreen(); this.JustReturnedToTitle = true; } @@ -1149,7 +1150,7 @@ namespace StardewModdingAPI.Framework // Game1._temporaryContent initializing from SGame constructor if (this.ContentCore == null) { - this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); + this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded, this.Settings.AggressiveMemoryOptimizations); if (this.ContentCore.Language != this.Translator.LocaleEnum) this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 063804e0..8b591bc1 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework.Graphics; using Netcode; +using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -29,6 +30,15 @@ namespace StardewModdingAPI.Metadata /********* ** Fields *********/ + /// <summary>The main content manager through which to reload assets.</summary> + private readonly LocalizedContentManager MainContentManager; + + /// <summary>An internal content manager used only for asset propagation. See remarks on <see cref="GameContentManagerForAssetPropagation"/>.</summary> + private readonly GameContentManagerForAssetPropagation DisposableContentManager; + + /// <summary>Whether to enable more aggressive memory optimizations.</summary> + private readonly bool AggressiveMemoryOptimizations; + /// <summary>Normalizes an asset key to match the cache key and assert that it's valid.</summary> private readonly Func<string, string> AssertAndNormalizeAssetName; @@ -53,19 +63,24 @@ namespace StardewModdingAPI.Metadata ** Public methods *********/ /// <summary>Initialize the core asset data.</summary> - /// <param name="assertAndNormalizeAssetName">Normalizes an asset key to match the cache key and assert that it's valid.</param> + /// <param name="mainContent">The main content manager through which to reload assets.</param> + /// <param name="disposableContent">An internal content manager used only for asset propagation.</param> /// <param name="reflection">Simplifies access to private code.</param> - public CoreAssetPropagator(Func<string, string> assertAndNormalizeAssetName, Reflector reflection) + /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> + public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, Reflector reflection, bool aggressiveMemoryOptimizations) { - this.AssertAndNormalizeAssetName = assertAndNormalizeAssetName; + this.MainContentManager = mainContent; + this.DisposableContentManager = disposableContent; this.Reflection = reflection; + this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; + + this.AssertAndNormalizeAssetName = disposableContent.AssertAndNormalizeAssetName; } /// <summary>Reload one of the game's core assets (if applicable).</summary> - /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="assets">The asset keys and types to reload.</param> /// <returns>Returns a lookup of asset names to whether they've been propagated.</returns> - public IDictionary<string, bool> Propagate(LocalizedContentManager content, IDictionary<string, Type> assets) + public IDictionary<string, bool> Propagate(IDictionary<string, Type> assets) { // group into optimized lists var buckets = assets.GroupBy(p => @@ -86,16 +101,16 @@ namespace StardewModdingAPI.Metadata switch (bucket.Key) { case AssetBucket.Sprite: - this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated); + this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated); break; case AssetBucket.Portrait: - this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated); + this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated); break; default: foreach (var entry in bucket) - propagated[entry.Key] = this.PropagateOther(content, entry.Key, entry.Value); + propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value); break; } } @@ -107,13 +122,13 @@ namespace StardewModdingAPI.Metadata ** Private methods *********/ /// <summary>Reload one of the game's core assets (if applicable).</summary> - /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="key">The asset key to reload.</param> /// <param name="type">The asset type to reload.</param> /// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns> [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(LocalizedContentManager content, string key, Type type) + private bool PropagateOther(string key, Type type) { + var content = this.MainContentManager; key = this.AssertAndNormalizeAssetName(key); /**** @@ -163,14 +178,19 @@ namespace StardewModdingAPI.Metadata ** Buildings ****/ case "buildings\\houses": // Farm - reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key)); - return true; + { + var field = reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)); + field.SetValue( + this.LoadAndDisposeIfNeeded(field.GetValue(), key) + ); + return true; + } /**** ** Content\Characters\Farmer ****/ case "characters\\farmer\\accessories": // Game1.LoadContent - FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key); + FarmerRenderer.accessoriesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.accessoriesTexture, key); return true; case "characters\\farmer\\farmer_base": // Farmer @@ -180,19 +200,19 @@ namespace StardewModdingAPI.Metadata return this.ReloadPlayerSprites(key); case "characters\\farmer\\hairstyles": // Game1.LoadContent - FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key); + FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key); return true; case "characters\\farmer\\hats": // Game1.LoadContent - FarmerRenderer.hatsTexture = content.Load<Texture2D>(key); + FarmerRenderer.hatsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hatsTexture, key); return true; case "characters\\farmer\\pants": // Game1.LoadContent - FarmerRenderer.pantsTexture = content.Load<Texture2D>(key); + FarmerRenderer.pantsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.pantsTexture, key); return true; case "characters\\farmer\\shirts": // Game1.LoadContent - FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key); + FarmerRenderer.shirtsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.shirtsTexture, key); return true; /**** @@ -432,8 +452,7 @@ namespace StardewModdingAPI.Metadata return true; case "tilesheets\\chairtiles": // Game1.LoadContent - MapSeat.mapChairTexture = content.Load<Texture2D>(key); - return true; + return this.ReloadChairTiles(content, key); case "tilesheets\\craftables": // Game1.LoadContent Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key); @@ -582,7 +601,7 @@ namespace StardewModdingAPI.Metadata titleMenu.aboutButton.texture = texture; titleMenu.languageButton.texture = texture; foreach (ClickableTextureComponent button in titleMenu.buttons) - button.texture = titleMenu.titleButtonsTexture; + button.texture = texture; foreach (TemporaryAnimatedSprite bird in titleMenu.birds) bird.texture = texture; @@ -671,6 +690,28 @@ namespace StardewModdingAPI.Metadata return false; } + /// <summary>Reload map seat textures.</summary> + /// <param name="content">The content manager through which to reload the asset.</param> + /// <param name="key">The asset key to reload.</param> + /// <returns>Returns whether any textures were reloaded.</returns> + private bool ReloadChairTiles(LocalizedContentManager content, string key) + { + MapSeat.mapChairTexture = content.Load<Texture2D>(key); + + foreach (var location in this.GetLocations()) + { + foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) + { + string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile); + + if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase)) + seat.overlayTexture = MapSeat.mapChairTexture; + } + } + + return true; + } + /// <summary>Reload critter textures.</summary> /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="key">The asset key to reload.</param> @@ -785,6 +826,9 @@ namespace StardewModdingAPI.Metadata /// <param name="location">The location whose map to reload.</param> private void ReloadMap(GameLocation location) { + if (this.AggressiveMemoryOptimizations) + location.map.DisposeTileSheets(Game1.mapDisplayDevice); + // reload map location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist location.reloadMap(); @@ -822,10 +866,9 @@ namespace StardewModdingAPI.Metadata } /// <summary>Reload the sprites for matching NPCs.</summary> - /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="keys">The asset keys to reload.</param> /// <param name="propagated">The asset keys which have been propagated.</param> - private void ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated) + private void ReloadNpcSprites(IEnumerable<string> keys, IDictionary<string, bool> propagated) { // get NPCs HashSet<string> lookup = new HashSet<string>(keys, StringComparer.OrdinalIgnoreCase); @@ -843,16 +886,15 @@ namespace StardewModdingAPI.Metadata // update sprite foreach (var target in characters) { - target.Npc.Sprite.spriteTexture = content.Load<Texture2D>(target.Key); + target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.Key); propagated[target.Key] = true; } } /// <summary>Reload the portraits for matching NPCs.</summary> - /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="keys">The asset key to reload.</param> /// <param name="propagated">The asset keys which have been propagated.</param> - private void ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated) + private void ReloadNpcPortraits(IEnumerable<string> keys, IDictionary<string, bool> propagated) { // get NPCs HashSet<string> lookup = new HashSet<string>(keys, StringComparer.OrdinalIgnoreCase); @@ -881,7 +923,7 @@ namespace StardewModdingAPI.Metadata // update portrait foreach (var target in characters) { - target.Npc.Portrait = content.Load<Texture2D>(target.Key); + target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.Key); propagated[target.Key] = true; } } @@ -1146,5 +1188,27 @@ namespace StardewModdingAPI.Metadata { return this.GetSegments(path).Length; } + + /// <summary>Load a texture, and dispose the old one if <see cref="AggressiveMemoryOptimizations"/> is enabled and it's different from the new instance.</summary> + /// <param name="oldTexture">The previous texture to dispose.</param> + /// <param name="key">The asset key to load.</param> + private Texture2D LoadAndDisposeIfNeeded(Texture2D oldTexture, string key) + { + // if aggressive memory optimizations are enabled, load the asset from the disposable + // content manager and dispose the old instance if needed. + if (this.AggressiveMemoryOptimizations) + { + GameContentManagerForAssetPropagation content = this.DisposableContentManager; + + Texture2D newTexture = content.Load<Texture2D>(key); + if (oldTexture?.IsDisposed == false && !object.ReferenceEquals(oldTexture, newTexture) && content.IsResponsibleFor(oldTexture)) + oldTexture.Dispose(); + + return newTexture; + } + + // else just (re)load it from the main content manager + return this.MainContentManager.Load<Texture2D>(key); + } } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 23ee8453..986d2780 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -73,8 +73,22 @@ namespace StardewModdingAPI /// <remarks>This must be checked *before* any references to <see cref="Constants"/>, and this method should not reference <see cref="Constants"/> itself to avoid errors in Mono or when the game isn't present.</remarks> private static void AssertGamePresent() { - if (Type.GetType($"StardewValley.Game1, {EarlyConstants.GameAssemblyName}", throwOnError: false) == null) - Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder. See the readme.txt file for details."); + try + { + _ = Type.GetType($"StardewValley.Game1, {EarlyConstants.GameAssemblyName}", throwOnError: true); + } + catch (Exception ex) + { + // file doesn't exist + if (!File.Exists(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe"))) + Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder."); + + // can't load file + Program.PrintErrorAndExit( + message: "Oops! SMAPI couldn't load the game executable. The technical details below may have more info.", + technicalMessage: $"Technical details: {ex}" + ); + } } /// <summary>Assert that the game version is within <see cref="Constants.MinimumGameVersion"/> and <see cref="Constants.MaximumGameVersion"/>.</summary> @@ -130,11 +144,22 @@ namespace StardewModdingAPI /// <summary>Write an error directly to the console and exit.</summary> /// <param name="message">The error message to display.</param> - private static void PrintErrorAndExit(string message) + /// <param name="technicalMessage">An additional message to log with technical details.</param> + private static void PrintErrorAndExit(string message, string technicalMessage = null) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(message); Console.ResetColor(); + + if (technicalMessage != null) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(technicalMessage); + Console.ResetColor(); + Console.WriteLine(); + } + Program.PressAnyKeyToExit(showMessage: true); } diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 7a710f14..a9e6f389 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -40,6 +40,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future "RewriteMods": true, /** + * Whether to enable more aggressive memory optimizations. + * You can try disabling this if you get ObjectDisposedException errors. + */ + "AggressiveMemoryOptimizations": true, + + /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as * part of their normal functionality, so these warnings are meaningless without further |