diff options
42 files changed, 524 insertions, 331 deletions
diff --git a/build/common.targets b/build/common.targets index 70f0a741..5eb69901 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,18 +1,13 @@ <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <!--set general build properties --> - <Version>3.12.5</Version> + <Version>3.12.6</Version> <Product>SMAPI</Product> <LangVersion>latest</LangVersion> <AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> - <!--uncomment for 64-bit Stardew Valley on Windows--> - <!--<GamePath>D:\dev\SDV 64-bit\6125897</GamePath> - <DefineConstants>$(DefineConstants);SMAPI_FOR_WINDOWS_64BIT_HACK</DefineConstants>--> - <!--set platform--> - <DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants> - <DefineConstants Condition="$(OS) == 'Windows_NT' AND !$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))">$(DefineConstants);SMAPI_FOR_XNA</DefineConstants> + <DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS;SMAPI_FOR_XNA</DefineConstants> </PropertyGroup> <!--find game folder--> diff --git a/docs/release-notes.md b/docs/release-notes.md index db0c3a2a..4e0a9df6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,30 @@ ← [README](README.md) # Release notes +## 3.12.6 +Released 03 September 2021 for Stardew Valley 1.5.4 or later. + +* For players: + * Added friendly error when using SMAPI 3.2._x_ with Stardew Valley 1.5.5 or later. + * Improved mod compatibility in 64-bit mode (thanks to spacechase0!). + * Reduced load time when scanning/rewriting many mods for compatibility. + * **Dropped support for unofficial 64-bit mode**. You can now use the [official 64-bit Stardew Valley 1.5.5 beta](https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows) instead. + * Updated compatibility list. + +* For mod authors: + * Added `PathUtilities.NormalizeAssetName` and `PathUtilities.PreferredAssetSeparator` to prepare for the upcoming Stardew Valley 1.5.5. + * **SMAPI no longer propagates changes to `Data/Bundles`.** + _You can still load/edit the asset like usual, but if bundles have already been loaded for a save, SMAPI will no longer dynamically update the in-game bundles to reflect the changes. Unfortunately this caused bundle corruption when playing in non-English._ + * Fixed content packs created via `helper.ContentPacks.CreateFake` or `CreateTemporary` not initializing translations correctly. + +* For console commands: + * Added `hurry_all` command which immediately warps all NPCs to their scheduled positions. + +**Update note for mod authors:** +Stardew Valley 1.5.5 will change how asset names are formatted. If you use `PathUtilities.NormalizePath` +to format asset names, you should switch to `PathUtilities.NormalizeAssetName` now so your code will +continue working in the next game update. + ## 3.12.5 Released 26 August 2021 for Stardew Valley 1.5.4 or later. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index 586b17aa..4be062e2 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -57,8 +57,7 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled for Windows; if not set, the code assumes Linux/macOS. Set automatically in `common.targets`. -`SMAPI_FOR_WINDOWS_64BIT_HACK` | Whether SMAPI is being [compiled for Windows with a 64-bit Linux version of the game](https://github.com/Pathoschild/SMAPI/issues/767). This is highly specialized and shouldn't be used in most cases. False by default. -`SMAPI_FOR_XNA` | Whether SMAPI is being compiled for XNA Framework; if not set, the code assumes MonoGame. Set automatically in `common.targets` with the same value as `SMAPI_FOR_WINDOWS` (unless `SMAPI_FOR_WINDOWS_64BIT_HACK` is set). +`SMAPI_FOR_XNA` | Whether SMAPI is being compiled for XNA Framework; if not set, the code assumes MonoGame. Set automatically in `common.targets` with the same value as `SMAPI_FOR_WINDOWS`. ## For SMAPI developers ### Compiling from source @@ -81,9 +80,7 @@ To prepare a crossplatform SMAPI release, you'll need to compile it on two platf [crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) on the wiki for the first-time setup. -1. [Install a separate 64-bit version of Stardew Valley](https://github.com/Steviegt6/Stardew64Installer#readme) - on Windows. -2. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for +1. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for bundled mods. Make sure you use a [semantic version](https://semver.org). Recommended format: build type | format | example @@ -91,14 +88,9 @@ on the wiki for the first-time setup. dev build | `<version>-alpha.<date>` | `3.0.0-alpha.20171230` prerelease | `<version>-beta.<date>` | `3.0.0-beta.20171230` release | `<version>` | `3.0.0` -3. In Windows: +2. In Windows: 1. Rebuild the solution with the _release_ solution configuration. - 2. Back up the `bin/SMAPI installer` and `bin/SMAPI installer for developers` folders. - 3. Edit `common.targets` and uncomment the Stardew Valley 64-bit section at the top. - 4. Rebuild the solution again. - 5. Rename the compiled `StardewModdingAPI.exe` file to `StardewModdingAPI-x64.exe`, and copy it - into the `windows-install.dat` files from step ii. - 6. Copy the folders from step ii to Linux/MacOS. + 2. Copy the `bin/SMAPI installer` and `bin/SMAPI installer for developers` folders to Linux/macOS. 4. In Linux/macOS: 1. Rebuild the solution with the _release_ solution configuration. 2. Add the `windows-install.*` files from Windows to the `bin/SMAPI installer` and diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index b91d0dd3..9f49137f 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -275,21 +275,20 @@ namespace StardewModdingApi.Installer /********* - ** Step 4: detect 64-bit Stardew Valley + ** Step 4: validate assumptions *********/ - // detect 64-bit mode - bool isWindows64Bit = false; + // not 64-bit on Windows if (context.Platform == Platform.Windows) { FileInfo linuxExecutable = new FileInfo(Path.Combine(paths.GamePath, "StardewValley.exe")); - isWindows64Bit = linuxExecutable.Exists && this.Is64Bit(linuxExecutable.FullName); - if (isWindows64Bit) - paths.SetExecutableFileName(linuxExecutable.Name); + if (linuxExecutable.Exists && this.Is64Bit(linuxExecutable.FullName)) + { + this.PrintError("Oops! The detected game install path seems to be unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info."); + Console.ReadLine(); + return; + } } - /********* - ** Step 5: validate assumptions - *********/ // executable exists if (!File.Exists(paths.ExecutablePath)) { @@ -298,6 +297,14 @@ namespace StardewModdingApi.Installer return; } + // not Stardew Valley 1.5.5+ + if (File.Exists(Path.Combine(paths.GamePath, "Stardew Valley.dll"))) + { + this.PrintError("Oops! The detected game install path seems to be Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io."); + Console.ReadLine(); + return; + } + // game folder doesn't contain paths beyond the max limit { string[] tooLongPaths = PathUtilities.GetTooLongPaths(Path.Combine(paths.GamePath, "Mods")).ToArray(); @@ -312,7 +319,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 6: ask what to do + ** Step 5: ask what to do *********/ ScriptAction action; { @@ -320,7 +327,7 @@ namespace StardewModdingApi.Installer ** print header ****/ this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just one question first."); - this.PrintDebug($"Game path: {paths.GamePath}{(context.IsWindows ? $" [{(isWindows64Bit ? "64-bit" : "32-bit")}]" : "")}"); + this.PrintDebug($"Game path: {paths.GamePath}"); this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); @@ -358,14 +365,14 @@ namespace StardewModdingApi.Installer /********* - ** Step 7: apply + ** Step 6: apply *********/ { /**** ** print header ****/ this.PrintInfo($"That's all I need! I'll {action.ToString().ToLower()} SMAPI now."); - this.PrintDebug($"Game path: {paths.GamePath}{(context.IsWindows ? $" [{(isWindows64Bit ? "64-bit" : "32-bit")}]" : "")}"); + this.PrintDebug($"Game path: {paths.GamePath}"); this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); @@ -426,25 +433,6 @@ namespace StardewModdingApi.Installer this.RecursiveCopy(sourceEntry, paths.GameDir); } - // handle 64-bit file - { - FileInfo x64Executable = new FileInfo(Path.Combine(paths.GameDir.FullName, "StardewModdingAPI-x64.exe")); - if (isWindows64Bit) - { - this.PrintDebug("Making SMAPI 64-bit..."); - if (x64Executable.Exists) - { - string targetPath = Path.Combine(paths.GameDir.FullName, "StardewModdingAPI.exe"); - this.InteractivelyDelete(targetPath); - x64Executable.MoveTo(targetPath); - } - else - this.PrintError($"Oops! Could not find the required '{x64Executable.Name}' installer file. SMAPI may not work correctly."); - } - else if (x64Executable.Exists) - x64Executable.Delete(); - } - // replace mod launcher (if possible) if (context.IsUnix) { @@ -539,7 +527,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 7: final instructions + ** Step 6: final instructions *********/ if (context.IsWindows) { diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs new file mode 100644 index 00000000..2deac5f8 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs @@ -0,0 +1,54 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World +{ + /// <summary>A command which immediately warps all NPCs to their scheduled positions. To hurry a single NPC, see <c>debug hurry npc-name</c> instead.</summary> + internal class HurryAllCommand : ConsoleCommand + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public HurryAllCommand() + : base( + name: "hurry_all", + description: "Immediately warps all NPCs to their scheduled positions. (To hurry a single NPC, use `debug hurry npc-name` instead.)\n\n" + + "Usage: hurry_all" + ) + { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // check context + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + + // hurry all NPCs + foreach (NPC npc in Utility.getAllCharacters()) + { + if (!npc.isVillager()) + continue; + + monitor.Log($"Hurrying {npc.Name}..."); + try + { + npc.warpToPathControllerDestination(); + } + catch (Exception ex) + { + monitor.Log($"Failed hurrying {npc.Name}. Technical details:\n{ex}", LogLevel.Error); + } + } + + monitor.Log("Done!", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index 432fdc35..a187c1ff 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -19,7 +19,7 @@ <!-- Windows only --> <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" Condition="!$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))" /> + <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" /> </ItemGroup> <!-- Game framework --> diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 540d0488..de223c01 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.12.5", + "Version": "3.12.6", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.12.5" + "MinimumApiVersion": "3.12.6" } diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index ffda5f89..eb878bc5 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -21,7 +21,7 @@ <!-- Windows only --> <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" Condition="!$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))" /> + <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" /> </ItemGroup> <!-- Game framework --> diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 645a7514..fcb6d7eb 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.12.5", + "Version": "3.12.6", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.12.5" + "MinimumApiVersion": "3.12.6" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index d8b77339..1c84b5c2 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.12.5", + "Version": "3.12.6", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.12.5" + "MinimumApiVersion": "3.12.6" } diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 28262111..da3446bb 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -10,6 +10,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Toolkit.Serialization.Models; using SemanticVersion = StardewModdingAPI.SemanticVersion; @@ -489,7 +490,8 @@ namespace SMAPI.Tests.Core EntryDll = entryDll ?? $"{Sample.String()}.dll", ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - Dependencies = dependencies + Dependencies = dependencies ?? new IManifestDependency[0], + UpdateKeys = new string[0] }; } @@ -541,6 +543,7 @@ namespace SMAPI.Tests.Core mod.Setup(p => p.Manifest).Returns(this.GetManifest()); mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); mod.Setup(p => p.DataRecord).Returns(modRecord); + mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>()); } } } diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index 5a342974..c18f47a5 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -1,3 +1,4 @@ +using System.IO; using NUnit.Framework; using StardewModdingAPI.Toolkit.Utilities; @@ -175,9 +176,30 @@ namespace SMAPI.Tests.Utilities } /**** - ** NormalizePathSeparators + ** NormalizeAssetName ****/ - [Test(Description = "Assert that PathUtilities.NormalizePathSeparators normalizes paths correctly.")] + [Test(Description = "Assert that PathUtilities.NormalizeAssetName normalizes paths correctly.")] + [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] + public void NormalizeAssetName(SamplePath path) + { + if (Path.IsPathRooted(path.OriginalPath) || path.OriginalPath.StartsWith("/") || path.OriginalPath.StartsWith("\\")) + Assert.Ignore("Absolute paths can't be used as asset names."); + + // act + string normalized = PathUtilities.NormalizeAssetName(path.OriginalPath); + + // assert +#if SMAPI_FOR_WINDOWS + Assert.AreEqual(path.NormalizedOnWindows, normalized); +#else + Assert.AreEqual(path.NormalizedOnUnix, normalized); +#endif + } + + /**** + ** NormalizePath + ****/ + [Test(Description = "Assert that PathUtilities.NormalizePath normalizes paths correctly.")] [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] public void NormalizePath(SamplePath path) { diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 695a2c52..38a67ae5 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,6 +7,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.GameScanning; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Toolkit.Serialization; namespace StardewModdingAPI.Toolkit @@ -22,11 +22,11 @@ namespace StardewModdingAPI.Toolkit private readonly string UserAgent; /// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID). This doesn't affect update checks, which defer to the remote web API.</summary> - private readonly IDictionary<string, string> VendorModUrls = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + private readonly IDictionary<ModSiteKey, string> VendorModUrls = new Dictionary<ModSiteKey, string>() { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["GitHub"] = "https://github.com/{0}/releases", - ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" + [ModSiteKey.Chucklefish] = "https://community.playstarbound.com/resources/{0}", + [ModSiteKey.GitHub] = "https://github.com/{0}/releases", + [ModSiteKey.Nexus] = "https://www.nexusmods.com/stardewvalley/mods/{0}" }; @@ -89,15 +89,12 @@ namespace StardewModdingAPI.Toolkit /// <param name="updateKey">The update key.</param> public string GetUpdateUrl(string updateKey) { - string[] parts = updateKey.Split(new[] { ':' }, 2); - if (parts.Length != 2) + UpdateKey parsed = UpdateKey.Parse(updateKey); + if (!parsed.LooksValid) return null; - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (this.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - return string.Format(urlTemplate, modID); + if (this.VendorModUrls.TryGetValue(parsed.Site, out string urlTemplate)) + return string.Format(urlTemplate, parsed.ID); return null; } diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index babc0981..020ebc6d 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -23,9 +23,12 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>The possible directory separator characters in a file path.</summary> public static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// <summary>The preferred directory separator character in an asset key.</summary> + /// <summary>The preferred directory separator character in a file path.</summary> public static readonly char PreferredPathSeparator = Path.DirectorySeparatorChar; + /// <summary>The preferred directory separator character in an asset key.</summary> + public static readonly char PreferredAssetSeparator = PathUtilities.PreferredPathSeparator; + /********* ** Public methods @@ -41,8 +44,16 @@ namespace StardewModdingAPI.Toolkit.Utilities : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); } - /// <summary>Normalize separators in a file path.</summary> + /// <summary>Normalize an asset name to match how MonoGame's content APIs would normalize and cache it.</summary> + /// <param name="assetName">The asset name to normalize.</param> + public static string NormalizeAssetName(string assetName) + { + return string.Join(PathUtilities.PreferredAssetSeparator.ToString(), PathUtilities.GetSegments(assetName)); // based on MonoGame's ContentManager.Load<T> logic + } + + /// <summary>Normalize separators in a file path for the current platform.</summary> /// <param name="path">The file path to normalize.</param> + /// <remarks>This should only be used for file paths. For asset names, use <see cref="NormalizeAssetName"/> instead.</remarks> [Pure] public static string NormalizePath(string path) { diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 7dff16c4..dcdd6298 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -170,38 +170,28 @@ /********* ** Broke in SMAPI 3.12.0 *********/ - "PlatoTK": { - "ID": "Platonymous.PlatoTK", - "~1.9.3 | Status": "AssumeBroken", - "~1.9.3 | StatusReasonDetails": "fails to load with 'ReflectionTypeLoadException' error" - }, - "Stardew Hack": { - "ID": "bcmpinc.StardewHack", - "~5.0.0 | Status": "AssumeBroken", - "~5.0.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" - }, "Always Scroll Map": { "ID": "bcmpinc.AlwaysScrollMap", "~4.1.0 | Status": "AssumeBroken", "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author }, + "Big Silo": { + "ID": "lperkins2.BigSilo", + "~0.0.3 | Status": "AssumeBroken", + "~0.0.3 | StatusReasonDetails": "not compatible with Harmony 2.x" + }, "Fix Animal Tools": { "ID": "bcmpinc.FixAnimalTools", "~4.1.0 | Status": "AssumeBroken", "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author }, - "Harvest With Scythe (bcmpinc)": { - "ID": "bcmpinc.HarvestWithScythe", - "~4.1.0 | Status": "AssumeBroken", - "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author - }, "Grass Growth": { "ID": "bcmpinc.GrassGrowth", "~4.1.0 | Status": "AssumeBroken", "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author }, - "Tilled Soil Decay": { - "ID": "bcmpinc.TilledSoilDecay", + "Harvest With Scythe (bcmpinc)": { + "ID": "bcmpinc.HarvestWithScythe", "~4.1.0 | Status": "AssumeBroken", "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author }, @@ -210,6 +200,21 @@ "~4.1.0 | Status": "AssumeBroken", "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author }, + "PlatoTK": { + "ID": "Platonymous.PlatoTK", + "~1.9.3 | Status": "AssumeBroken", + "~1.9.3 | StatusReasonDetails": "fails to load with 'ReflectionTypeLoadException' error" + }, + "Stardew Hack": { + "ID": "bcmpinc.StardewHack", + "~5.0.0 | Status": "AssumeBroken", + "~5.0.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" + }, + "Tilled Soil Decay": { + "ID": "bcmpinc.TilledSoilDecay", + "~4.1.0 | Status": "AssumeBroken", + "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author + }, "Tree Spread": { "ID": "bcmpinc.TreeSpread", "~4.2.0 | Status": "AssumeBroken", diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 04e15998..d0c693bf 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -39,14 +39,6 @@ namespace StardewModdingAPI /// <summary>The target game platform.</summary> internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform()); - /// <summary>Whether SMAPI is being compiled for Windows with a 64-bit Linux version of the game. This is highly specialized and shouldn't be used in most cases.</summary> - internal static bool IsWindows64BitHack { get; } = -#if SMAPI_FOR_WINDOWS_64BIT_HACK - true; -#else - false; -#endif - /// <summary>The game framework running the game.</summary> internal static GameFramework GameFramework { get; } = #if SMAPI_FOR_XNA @@ -56,13 +48,13 @@ namespace StardewModdingAPI #endif /// <summary>The game's assembly name.</summary> - internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows && !EarlyConstants.IsWindows64BitHack ? "Stardew Valley" : "StardewValley"; + internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley"; /// <summary>The <see cref="Context.ScreenId"/> value which should appear in the SMAPI log, if any.</summary> internal static int? LogScreenId { get; set; } /// <summary>SMAPI's current raw semantic version.</summary> - internal static string RawApiVersion = "3.12.5"; + internal static string RawApiVersion = "3.12.6"; } /// <summary>Contains SMAPI's constants and assumptions.</summary> @@ -81,7 +73,7 @@ namespace StardewModdingAPI public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); /// <summary>The maximum supported version of Stardew Valley.</summary> - public static ISemanticVersion MaximumGameVersion { get; } = null; + public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.5.4"); /// <summary>The target game platform.</summary> public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform; @@ -269,7 +261,7 @@ namespace StardewModdingAPI targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly); // get changes for platform - if (Constants.Platform != Platform.Windows || EarlyConstants.IsWindows64BitHack) + if (Constants.Platform != Platform.Windows) { removeAssemblyReferences.AddRange(new[] { diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index 20f0ed0f..4f810948 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -153,9 +153,9 @@ namespace StardewModdingAPI.Framework.Content if (string.IsNullOrWhiteSpace(path)) return string.Empty; - path = PathUtilities.NormalizePath(path); - if (path.StartsWith($"Maps{PathUtilities.PreferredPathSeparator}", StringComparison.OrdinalIgnoreCase)) - path = path.Substring($"Maps{PathUtilities.PreferredPathSeparator}".Length); + path = PathUtilities.NormalizeAssetName(path); + if (path.StartsWith($"Maps{PathUtilities.PreferredAssetSeparator}", StringComparison.OrdinalIgnoreCase)) + path = path.Substring($"Maps{PathUtilities.PreferredAssetSeparator}".Length); if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) path = path.Substring(0, path.Length - 4); diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 5c7ad778..7edc9ab9 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -57,8 +57,6 @@ namespace StardewModdingAPI.Framework.Content IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); this.NormalizeAssetNameForPlatform = path => method.Invoke<string>(path); } - else if (EarlyConstants.IsWindows64BitHack) - this.NormalizeAssetNameForPlatform = PathUtilities.NormalizePath; else this.NormalizeAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic } diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 0660a367..b6add7b5 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; @@ -32,7 +33,10 @@ namespace StardewModdingAPI.Framework public IManifest Manifest { get; } /// <inheritdoc /> - public ITranslationHelper Translation { get; } + public ITranslationHelper Translation => this.TranslationImpl; + + /// <summary>The underlying translation helper.</summary> + internal TranslationHelper TranslationImpl { get; set; } /********* @@ -44,12 +48,12 @@ namespace StardewModdingAPI.Framework /// <param name="content">Provides an API for loading content assets.</param> /// <param name="translation">Provides translations stored in the content pack's <c>i18n</c> folder.</param> /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> - public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, ITranslationHelper translation, JsonHelper jsonHelper) + public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, TranslationHelper translation, JsonHelper jsonHelper) { this.DirectoryPath = directoryPath; this.Manifest = manifest; this.Content = content; - this.Translation = translation; + this.TranslationImpl = translation; this.JsonHelper = jsonHelper; foreach (string path in Directory.EnumerateFiles(this.DirectoryPath, "*", SearchOption.AllDirectories)) diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index f5babafb..cb876ee4 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; @@ -64,6 +65,9 @@ namespace StardewModdingAPI.Framework /// <summary>The update-check metadata for this mod (if any).</summary> ModEntryModel UpdateCheckData { get; } + /// <summary>The fake content packs created by this mod, if any.</summary> + ISet<WeakReference<ContentPack>> FakeContentPacks { get; } + /********* ** Public methods @@ -135,5 +139,8 @@ namespace StardewModdingAPI.Framework /// <summary>Get a relative path which includes the root folder name.</summary> string GetRelativePathWithRoot(); + + /// <summary>Get the currently live fake content packs created by this mod.</summary> + IEnumerable<ContentPack> GetFakeContentPacks(); } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index c6faa90d..6fe44d98 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -291,13 +291,7 @@ namespace StardewModdingAPI.Framework.Logging public void LogIntro(string modsPath, IDictionary<string, object> customSettings) { // log platform & patches - { - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); - - string[] patchLabels = this.GetPatchLabels().ToArray(); - if (patchLabels.Any()) - this.Monitor.Log($"Detected custom version: {string.Join(", ", patchLabels)}", LogLevel.Info); - } + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); // log basic info this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); @@ -416,20 +410,6 @@ namespace StardewModdingAPI.Framework.Logging gameMonitor.Log(message, level); } - /// <summary>Get human-readable labels to log for detected SMAPI and Stardew Valley customizations.</summary> - private IEnumerable<string> GetPatchLabels() - { - // custom game framework - if (EarlyConstants.IsWindows64BitHack) - yield return $"running 64-bit SMAPI with {Constants.GameFramework}"; - else if ((Constants.GameFramework == GameFramework.Xna) != (Constants.Platform == Platform.Windows)) - yield return $"running {Constants.GameFramework}"; - - // patched by Stardew64Installer - if (Constants.IsPatchedByStardew64Installer(out ISemanticVersion patchedByVersion)) - yield return $"patched by Stardew64Installer {patchedByVersion}"; - } - /// <summary>Write a summary of mod warnings to the console and log.</summary> /// <param name="mods">The loaded mods.</param> /// <param name="skippedMods">The mods which could not be loaded.</param> diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 3e35c9dd..57a76a35 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -300,10 +300,10 @@ namespace StardewModdingAPI.Framework.ModLoading // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} for OS..."); } } if (platformChanged) @@ -336,6 +336,13 @@ namespace StardewModdingAPI.Framework.ModLoading IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged, this.RewriteMods).ToArray(); RecursiveRewriter rewriter = new RecursiveRewriter( module: module, + rewriteModule: curModule => + { + bool rewritten = false; + foreach (IInstructionHandler handler in handlers) + rewritten |= handler.Handle(curModule); + return rewritten; + }, rewriteType: (type, replaceWith) => { bool rewritten = false; @@ -387,7 +394,7 @@ namespace StardewModdingAPI.Framework.ModLoading break; case InstructionHandleResult.DetectedGamePatch: - template = $"{logPrefix}Detected game patcher ($phrase) in assembly {filename}."; + template = $"{logPrefix}Detected game patcher in assembly {filename}."; // no need for phrase, which would confusingly be 'Harmony 1.x' here mod.SetWarning(ModWarning.PatchesGame); break; @@ -431,13 +438,10 @@ namespace StardewModdingAPI.Framework.ModLoading return; // format messages - if (handler.Phrases.Any()) - { - foreach (string message in handler.Phrases) - this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", message)); - } - else - this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); + string phrase = handler.Phrases.Any() + ? string.Join(", ", handler.Phrases) + : handler.DefaultPhrase ?? handler.GetType().Name; + this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", phrase)); } /// <summary>Get the correct reference to use for compatibility with the current platform.</summary> diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index 01ed153b..124951a5 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -13,8 +15,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <summary>The full type name for which to find references.</summary> private readonly string FullTypeName; - /// <summary>The event name for which to find references.</summary> - private readonly string EventName; + /// <summary>The method names for which to find references.</summary> + private readonly ISet<string> MethodNames; /// <summary>The result to return for matching instructions.</summary> private readonly InstructionHandleResult Result; @@ -25,38 +27,47 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> - /// <param name="eventName">The event name for which to find references.</param> + /// <param name="eventNames">The event names for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> - public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) - : base(defaultPhrase: $"{fullTypeName}.{eventName} event") + public EventFinder(string fullTypeName, string[] eventNames, InstructionHandleResult result) + : base(defaultPhrase: $"{string.Join(", ", eventNames.Select(p => $"{fullTypeName}.{p}"))} event{(eventNames.Length != 1 ? "s" : "")}") // default phrase should never be used { this.FullTypeName = fullTypeName; - this.EventName = eventName; this.Result = result; + + this.MethodNames = new HashSet<string>(); + foreach (string name in eventNames) + { + this.MethodNames.Add($"add_{name}"); + this.MethodNames.Add($"remove_{name}"); + } } + /// <summary>Construct an instance.</summary> + /// <param name="fullTypeName">The full type name for which to find references.</param> + /// <param name="eventName">The event name for which to find references.</param> + /// <param name="result">The result to return for matching instructions.</param> + public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) + : this(fullTypeName, new[] { eventName }, result) { } + /// <inheritdoc /> public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { - if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) - this.MarkFlag(this.Result); - - return false; - } + if (this.MethodNames.Any()) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null && methodRef.DeclaringType.FullName == this.FullTypeName && this.MethodNames.Contains(methodRef.Name)) + { + string eventName = methodRef.Name.Split(new[] { '_' }, 2)[1]; + this.MethodNames.Remove($"add_{eventName}"); + this.MethodNames.Remove($"remove_{eventName}"); + this.MarkFlag(this.Result); + this.Phrases.Add($"{this.FullTypeName}.{eventName} event"); + } + } - /********* - ** Protected methods - *********/ - /// <summary>Get whether a CIL instruction matches.</summary> - /// <param name="instruction">The IL instruction.</param> - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.FullTypeName - && (methodRef.Name == "add_" + this.EventName || methodRef.Name == "remove_" + this.EventName); + return false; } } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index 2c062243..68415123 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -13,8 +15,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <summary>The full type name for which to find references.</summary> private readonly string FullTypeName; - /// <summary>The field name for which to find references.</summary> - private readonly string FieldName; + /// <summary>The field names for which to find references.</summary> + private readonly ISet<string> FieldNames; /// <summary>The result to return for matching instructions.</summary> private readonly InstructionHandleResult Result; @@ -25,21 +27,37 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> - /// <param name="fieldName">The field name for which to find references.</param> + /// <param name="fieldNames">The field names for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> - public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) - : base(defaultPhrase: $"{fullTypeName}.{fieldName} field") + public FieldFinder(string fullTypeName, string[] fieldNames, InstructionHandleResult result) + : base(defaultPhrase: $"{string.Join(", ", fieldNames.Select(p => $"{fullTypeName}.{p}"))} field{(fieldNames.Length != 1 ? "s" : "")}") // default phrase should never be used { this.FullTypeName = fullTypeName; - this.FieldName = fieldName; + this.FieldNames = new HashSet<string>(fieldNames); this.Result = result; } + /// <summary>Construct an instance.</summary> + /// <param name="fullTypeName">The full type name for which to find references.</param> + /// <param name="fieldName">The field name for which to find references.</param> + /// <param name="result">The result to return for matching instructions.</param> + public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) + : this(fullTypeName, new[] { fieldName }, result) { } + /// <inheritdoc /> public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { - if (!this.Flags.Contains(this.Result) && RewriteHelper.IsFieldReferenceTo(instruction, this.FullTypeName, this.FieldName)) - this.MarkFlag(this.Result); + if (this.FieldNames.Any()) + { + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null && fieldRef.DeclaringType.FullName == this.FullTypeName && this.FieldNames.Contains(fieldRef.Name)) + { + this.FieldNames.Remove(fieldRef.Name); + + this.MarkFlag(this.Result); + this.Phrases.Add($"{this.FullTypeName}.{fieldRef.Name} field"); + } + } return false; } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index b01a3240..8c1cae2b 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders ** Fields *********/ /// <summary>The assembly names to which to heuristically detect broken references.</summary> - private readonly HashSet<string> ValidateReferencesToAssemblies; + private readonly ISet<string> ValidateReferencesToAssemblies; /********* @@ -22,10 +22,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders *********/ /// <summary>Construct an instance.</summary> /// <param name="validateReferencesToAssemblies">The assembly names to which to heuristically detect broken references.</param> - public ReferenceToMemberWithUnexpectedTypeFinder(string[] validateReferencesToAssemblies) + public ReferenceToMemberWithUnexpectedTypeFinder(ISet<string> validateReferencesToAssemblies) : base(defaultPhrase: "") { - this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); + this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; } /// <inheritdoc /> diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index b64a255e..d305daf4 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders ** Fields *********/ /// <summary>The assembly names to which to heuristically detect broken references.</summary> - private readonly HashSet<string> ValidateReferencesToAssemblies; + private readonly ISet<string> ValidateReferencesToAssemblies; /********* @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders *********/ /// <summary>Construct an instance.</summary> /// <param name="validateReferencesToAssemblies">The assembly names to which to heuristically detect broken references.</param> - public ReferenceToMissingMemberFinder(string[] validateReferencesToAssemblies) + public ReferenceToMissingMemberFinder(ISet<string> validateReferencesToAssemblies) : base(defaultPhrase: "") { - this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); + this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; } /// <inheritdoc /> diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index bbd081e8..260a8df8 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Mono.Cecil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -10,8 +11,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* ** Fields *********/ - /// <summary>The full type name to match.</summary> - private readonly string FullTypeName; + /// <summary>The full type names remaining to match.</summary> + private readonly ISet<string> FullTypeNames; /// <summary>The result to return for matching instructions.</summary> private readonly InstructionHandleResult Result; @@ -24,22 +25,34 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="fullTypeName">The full type name to match.</param> + /// <param name="fullTypeNames">The full type names to match.</param> /// <param name="result">The result to return for matching instructions.</param> /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> - public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null) - : base(defaultPhrase: $"{fullTypeName} type") + public TypeFinder(string[] fullTypeNames, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null) + : base(defaultPhrase: $"{string.Join(", ", fullTypeNames)} type{(fullTypeNames.Length != 1 ? "s" : "")}") // default phrase should never be used { - this.FullTypeName = fullTypeName; + this.FullTypeNames = new HashSet<string>(fullTypeNames); this.Result = result; this.ShouldIgnore = shouldIgnore; } + /// <summary>Construct an instance.</summary> + /// <param name="fullTypeName">The full type name to match.</param> + /// <param name="result">The result to return for matching instructions.</param> + /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> + public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null) + : this(new[] { fullTypeName }, result, shouldIgnore) { } + /// <inheritdoc /> public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { - if (type.FullName == this.FullTypeName && this.ShouldIgnore?.Invoke(type) != true) + if (this.FullTypeNames.Contains(type.FullName) && this.ShouldIgnore?.Invoke(type) != true) + { + this.FullTypeNames.Remove(type.FullName); + this.MarkFlag(this.Result); + this.Phrases.Add($"{type.FullName} type"); + } return false; } diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index 624113b3..d5d1b38e 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -25,6 +25,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework ** Public methods *********/ /// <inheritdoc /> + public virtual bool Handle(ModuleDefinition module) + { + return false; + } + + /// <inheritdoc /> public virtual bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { return false; diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 10f68f0d..4f14a579 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -13,6 +13,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Delegates *********/ + /// <summary>Rewrite a module definition in the assembly code.</summary> + /// <param name="module">The current module definition.</param> + /// <returns>Returns whether the module was changed.</returns> + public delegate bool RewriteModuleDelegate(ModuleDefinition module); + /// <summary>Rewrite a type reference in the assembly code.</summary> /// <param name="type">The current type reference.</param> /// <param name="replaceWith">Replaces the type reference with the given type.</param> @@ -32,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// <summary>The module to rewrite.</summary> public ModuleDefinition Module { get; } + /// <summary>Handle or rewrite a module definition if needed.</summary> + public RewriteModuleDelegate RewriteModuleImpl { get; } + /// <summary>Handle or rewrite a type reference if needed.</summary> public RewriteTypeDelegate RewriteTypeImpl { get; } @@ -44,11 +52,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework *********/ /// <summary>Construct an instance.</summary> /// <param name="module">The module to rewrite.</param> + /// <param name="rewriteModule">Handle or rewrite a module if needed.</param> /// <param name="rewriteType">Handle or rewrite a type reference if needed.</param> /// <param name="rewriteInstruction">Handle or rewrite a CIL instruction if needed.</param> - public RecursiveRewriter(ModuleDefinition module, RewriteTypeDelegate rewriteType, RewriteInstructionDelegate rewriteInstruction) + public RecursiveRewriter(ModuleDefinition module, RewriteModuleDelegate rewriteModule, RewriteTypeDelegate rewriteType, RewriteInstructionDelegate rewriteInstruction) { this.Module = module; + this.RewriteModuleImpl = rewriteModule; this.RewriteTypeImpl = rewriteType; this.RewriteInstructionImpl = rewriteInstruction; } @@ -63,6 +73,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework try { + changed |= this.RewriteModuleImpl(this.Module); + foreach (var type in types) changed |= this.RewriteTypeDefinition(type); } diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index 17c9ba68..d41732f8 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -24,6 +24,11 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Methods *********/ + /// <summary>Rewrite a module definition if needed.</summary> + /// <param name="module">The assembly module.</param> + /// <returns>Returns whether the module was changed.</returns> + bool Handle(ModuleDefinition module); + /// <summary>Rewrite a type reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="type">The type definition to handle.</param> diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 0ace084f..9e6bc61f 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -83,6 +83,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <inheritdoc /> public bool IsContentPack => this.Manifest?.ContentPackFor != null; + /// <summary>The fake content packs created by this mod, if any.</summary> + public ISet<WeakReference<ContentPack>> FakeContentPacks { get; } = new HashSet<WeakReference<ContentPack>>(); + /********* ** Public methods @@ -244,6 +247,21 @@ namespace StardewModdingAPI.Framework.ModLoading return Path.Combine(rootFolderName, this.RelativeDirectoryPath); } + /// <summary>Get the currently live fake content packs created by this mod.</summary> + public IEnumerable<ContentPack> GetFakeContentPacks() + { + foreach (var reference in this.FakeContentPacks.ToArray()) + { + if (!reference.TryGetTarget(out ContentPack pack)) + { + this.FakeContentPacks.Remove(reference); + continue; + } + + yield return pack; + } + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 2f506571..4b05d1e5 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -5,6 +5,7 @@ using System.Linq; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; @@ -82,9 +83,9 @@ namespace StardewModdingAPI.Framework.ModLoading // get update URLs List<string> updateUrls = new List<string>(); - foreach (string key in mod.Manifest.UpdateKeys) + foreach (UpdateKey key in mod.GetUpdateKeys(validOnly: true)) { - string url = getUpdateUrl(key); + string url = getUpdateUrl(key.ToString()); if (url != null) updateUrls.Add(url); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs new file mode 100644 index 00000000..cc830216 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs @@ -0,0 +1,31 @@ +using Mono.Cecil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// <summary>Removes the 32-bit-only from loaded assemblies.</summary> + internal class ArchitectureAssemblyRewriter : BaseInstructionHandler + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ArchitectureAssemblyRewriter() + : base(defaultPhrase: "32-bit architecture") { } + + + /// <inheritdoc /> + public override bool Handle(ModuleDefinition module) + { + if (module.Attributes.HasFlag(ModuleAttributes.Required32Bit)) + { + module.Attributes &= ~ModuleAttributes.Required32Bit; + this.MarkRewritten(); + return true; + } + + return false; + } + + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs index 7a3b428d..922d4bc4 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs @@ -7,8 +7,8 @@ using StardewModdingAPI.Framework.ModLoading.RewriteFacades; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { - /// <summary>Rewrites Harmony 1.x assembly references to work with Harmony 2.x.</summary> - internal class Harmony1AssemblyRewriter : BaseInstructionHandler + /// <summary>Detects Harmony references, and rewrites Harmony 1.x assembly references to work with Harmony 2.x.</summary> + internal class HarmonyRewriter : BaseInstructionHandler { /********* ** Fields @@ -16,19 +16,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <summary>Whether any Harmony 1.x types were replaced.</summary> private bool ReplacedTypes; + /// <summary>Whether to rewrite Harmony 1.x code.</summary> + private readonly bool ShouldRewrite; + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - public Harmony1AssemblyRewriter() - : base(defaultPhrase: "Harmony 1.x") { } + public HarmonyRewriter(bool shouldRewrite = true) + : base(defaultPhrase: "Harmony 1.x") + { + this.ShouldRewrite = shouldRewrite; + } /// <inheritdoc /> public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { + // detect Harmony + if (!(type.Scope is AssemblyNameReference scope) || scope.Name != "0Harmony") + return false; + // rewrite Harmony 1.x type to Harmony 2.0 type - if (type.Scope is AssemblyNameReference { Name: "0Harmony" } scope && scope.Version.Major == 1) + if (this.ShouldRewrite && scope.Version.Major == 1) { Type targetType = this.GetMappedType(type); replaceWith(module.ImportReference(targetType)); @@ -37,28 +47,32 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return true; } + this.MarkFlag(InstructionHandleResult.DetectedGamePatch); return false; } /// <inheritdoc /> public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { - // rewrite Harmony 1.x methods to Harmony 2.0 - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (this.TryRewriteMethodsToFacade(module, methodRef)) - { - this.OnChanged(); - return true; - } - - // rewrite renamed fields - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null) + if (this.ShouldRewrite) { - if (fieldRef.DeclaringType.FullName == "HarmonyLib.HarmonyMethod" && fieldRef.Name == "prioritiy") + // rewrite Harmony 1.x methods to Harmony 2.0 + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (this.TryRewriteMethodsToFacade(module, methodRef)) { - fieldRef.Name = nameof(HarmonyMethod.priority); this.OnChanged(); + return true; + } + + // rewrite renamed fields + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + if (fieldRef.DeclaringType.FullName == "HarmonyLib.HarmonyMethod" && fieldRef.Name == "prioritiy") + { + fieldRef.Name = nameof(HarmonyMethod.priority); + this.OnChanged(); + } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index f59a6ab1..57f1dd17 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Fields *********/ /// <summary>The assembly names to which to rewrite broken references.</summary> - private readonly HashSet<string> RewriteReferencesToAssemblies; + private readonly ISet<string> RewriteReferencesToAssemblies; /********* @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters *********/ /// <summary>Construct an instance.</summary> /// <param name="rewriteReferencesToAssemblies">The assembly names to which to rewrite broken references.</param> - public HeuristicFieldRewriter(string[] rewriteReferencesToAssemblies) + public HeuristicFieldRewriter(ISet<string> rewriteReferencesToAssemblies) : base(defaultPhrase: "field changed to property") // ignored since we specify phrases { - this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies); + this.RewriteReferencesToAssemblies = rewriteReferencesToAssemblies; } /// <inheritdoc /> diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs index e133b6fa..89de437e 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Fields *********/ /// <summary>The assembly names to which to rewrite broken references.</summary> - private readonly HashSet<string> RewriteReferencesToAssemblies; + private readonly ISet<string> RewriteReferencesToAssemblies; /********* @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters *********/ /// <summary>Construct an instance.</summary> /// <param name="rewriteReferencesToAssemblies">The assembly names to which to rewrite broken references.</param> - public HeuristicMethodRewriter(string[] rewriteReferencesToAssemblies) + public HeuristicMethodRewriter(ISet<string> rewriteReferencesToAssemblies) : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases { - this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies); + this.RewriteReferencesToAssemblies = rewriteReferencesToAssemblies; } /// <inheritdoc /> diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c1aa3721..5913430e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -12,10 +12,10 @@ using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Xna.Framework; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; #endif -using Microsoft.Xna.Framework; #if SMAPI_FOR_XNA using System.Windows.Forms; #endif @@ -49,6 +49,7 @@ using StardewModdingAPI.Utilities; using StardewValley; using xTile.Display; using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix; +using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework @@ -435,7 +436,7 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); // log GPU info -#if SMAPI_FOR_WINDOWS && !SMAPI_FOR_WINDOWS_64BIT_HACK +#if SMAPI_FOR_WINDOWS this.Monitor.Log($"Running on GPU: {Game1.game1.GraphicsDevice?.Adapter?.Description ?? "<unknown>"}"); #endif } @@ -786,9 +787,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log(context); - // apply save fixes - this.ApplySaveFixes(); - // raise events this.OnLoadStageChanged(LoadStage.Ready); events.SaveLoaded.RaiseEmpty(); @@ -1060,7 +1058,12 @@ namespace StardewModdingAPI.Framework // update mod translation helpers foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { mod.Translations.SetLocale(locale, languageCode); + + foreach (ContentPack contentPack in mod.GetFakeContentPacks()) + contentPack.TranslationImpl.SetLocale(locale, languageCode); + } } /// <summary>Raised when the low-level stage while loading a save changes.</summary> @@ -1101,40 +1104,6 @@ namespace StardewModdingAPI.Framework this.EventManager.ReturnedToTitle.RaiseEmpty(); } - /// <summary>Apply fixes to the save after it's loaded.</summary> - private void ApplySaveFixes() - { - // get last SMAPI version used with this save - const string migrationKey = "Pathoschild.SMAPI/api-version"; - if (!Game1.CustomData.TryGetValue(migrationKey, out string rawVersion) || !SemanticVersion.TryParse(rawVersion, out ISemanticVersion lastVersion)) - lastVersion = new SemanticVersion(3, 8, 0); - - // fix bundle corruption in SMAPI 3.8.0 - // For non-English players who created a new save in SMAPI 3.8.0, bundle data was - // incorrectly translated which caused the code to crash whenever the game tried to - // read it. - if (lastVersion.IsOlderThan(new SemanticVersion(3, 8, 1)) && Game1.netWorldState?.Value?.BundleData != null) - { - var oldData = new Dictionary<string, string>(Game1.netWorldState.Value.BundleData); - - try - { - Game1.applySaveFix(SaveGame.SaveFixes.FixBotchedBundleData); - bool changed = Game1.netWorldState.Value.BundleData.Any(p => oldData.TryGetValue(p.Key, out string oldValue) && oldValue != p.Value); - if (changed) - this.Monitor.Log("Found broken community center bundles and fixed them automatically.", LogLevel.Info); - } - catch (Exception ex) - { - this.Monitor.Log("Failed to verify community center data.", LogLevel.Error); // should never happen - this.Monitor.Log($"Technical details: {ex}"); - } - } - - // update last run - Game1.CustomData[migrationKey] = Constants.ApiVersion.ToString(); - } - /// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary> protected void OnNewDayAfterFade() { @@ -1261,10 +1230,8 @@ namespace StardewModdingAPI.Framework /// <summary>Set the titles for the game and console windows.</summary> private void UpdateWindowTitles() { - string smapiVersion = $"{Constants.ApiVersion}{(EarlyConstants.IsWindows64BitHack ? " [64-bit]" : "")}"; - - string consoleTitle = $"SMAPI {smapiVersion} - running Stardew Valley {Constants.GameVersion}"; - string gameTitle = $"Stardew Valley {Constants.GameVersion} - running SMAPI {smapiVersion}"; + string consoleTitle = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + string gameTitle = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; if (this.ModRegistry.AreAllModsLoaded) { @@ -1731,10 +1698,10 @@ namespace StardewModdingAPI.Framework catch (Exception ex) { errorReasonPhrase = "its DLL couldn't be loaded."; -#if SMAPI_FOR_WINDOWS_64BIT_HACK - if (!EnvironmentUtility.Is64BitAssembly(assemblyPath)) - errorReasonPhrase = "it needs to be updated for 64-bit mode."; -#endif + // re-enable in Stardew Valley 1.5.5 + //if (ex is BadImageFormatException && !EnvironmentUtility.Is64BitAssembly(assemblyPath)) + // errorReasonPhrase = "it needs to be updated for 64-bit mode."; + errorDetails = $"Error: {ex.GetLogSummary()}"; failReason = ModFailReason.LoadFailed; return false; @@ -1772,8 +1739,12 @@ namespace StardewModdingAPI.Framework { IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name); IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - ITranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); - return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); + TranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); + + ContentPack contentPack = new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); + this.ReloadTranslationsForTemporaryContentPack(mod, contentPack); + mod.FakeContentPacks.Add(new WeakReference<ContentPack>(contentPack)); + return contentPack; } IModEvents events = new ModEvents(mod, this.EventManager); @@ -1867,15 +1838,39 @@ namespace StardewModdingAPI.Framework // mod translations foreach (IModMetadata metadata in mods) { - var translations = this.ReadTranslationFiles(Path.Combine(metadata.DirectoryPath, "i18n"), out IList<string> errors); - if (errors.Any()) + // top-level mod { - metadata.LogAsMod("Mod couldn't load some translation files:", LogLevel.Warn); - foreach (string error in errors) - metadata.LogAsMod($" - {error}", LogLevel.Warn); + var translations = this.ReadTranslationFiles(Path.Combine(metadata.DirectoryPath, "i18n"), out IList<string> errors); + if (errors.Any()) + { + metadata.LogAsMod("Mod couldn't load some translation files:", LogLevel.Warn); + foreach (string error in errors) + metadata.LogAsMod($" - {error}", LogLevel.Warn); + } + + metadata.Translations.SetTranslations(translations); } - metadata.Translations.SetTranslations(translations); + + // fake content packs + foreach (ContentPack pack in metadata.GetFakeContentPacks()) + this.ReloadTranslationsForTemporaryContentPack(metadata, pack); + } + } + + /// <summary>Load or reload translations for a temporary content pack created by a mod.</summary> + /// <param name="parentMod">The parent mod which created the content pack.</param> + /// <param name="contentPack">The content pack instance.</param> + private void ReloadTranslationsForTemporaryContentPack(IModMetadata parentMod, ContentPack contentPack) + { + var translations = this.ReadTranslationFiles(Path.Combine(contentPack.DirectoryPath, "i18n"), out IList<string> errors); + if (errors.Any()) + { + parentMod.LogAsMod($"Generated content pack at '{PathUtilities.GetRelativePath(Constants.ModsPath, contentPack.DirectoryPath)}' couldn't load some translation files:", LogLevel.Warn); + foreach (string error in errors) + parentMod.LogAsMod($" - {error}", LogLevel.Warn); } + + contentPack.TranslationImpl.SetTranslations(translations); } /// <summary>Read translations from a directory containing JSON translation files.</summary> diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index a8686ca4..708673c3 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Microsoft.Xna.Framework.Graphics; -using Netcode; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Internal; @@ -16,7 +15,6 @@ using StardewValley.Characters; using StardewValley.GameData.Movies; using StardewValley.Locations; using StardewValley.Menus; -using StardewValley.Network; using StardewValley.Objects; using StardewValley.Projectiles; using StardewValley.TerrainFeatures; @@ -283,32 +281,6 @@ namespace StardewModdingAPI.Metadata Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key); return true; - case "data\\bundles": // NetWorldState constructor - if (Context.IsMainPlayer && Game1.netWorldState != null) - { - var bundles = this.Reflection.GetField<NetBundles>(Game1.netWorldState.Value, "bundles").GetValue(); - var rewards = this.Reflection.GetField<NetIntDictionary<bool, NetBool>>(Game1.netWorldState.Value, "bundleRewards").GetValue(); - foreach (var pair in content.Load<Dictionary<string, string>>(key)) - { - int bundleKey = int.Parse(pair.Key.Split('/')[1]); - int rewardsCount = pair.Value.Split('/')[2].Split(' ').Length; - - // add bundles - if (!bundles.TryGetValue(bundleKey, out bool[] values) || values.Length < rewardsCount) - { - values ??= new bool[0]; - - bundles.Remove(bundleKey); - bundles[bundleKey] = values.Concat(Enumerable.Repeat(false, rewardsCount - values.Length)).ToArray(); - } - - // add bundle rewards - if (!rewards.ContainsKey(bundleKey)) - rewards[bundleKey] = false; - } - } - break; - case "data\\clothinginformation": // Game1.LoadContent Game1.clothingInformation = content.Load<Dictionary<int, string>>(key); return true; diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index a787993a..76371e50 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Metadata *********/ /// <summary>The assembly names to which to heuristically detect broken references.</summary> /// <remarks>The current implementation only works correctly with assemblies that should always be present.</remarks> - private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley", "Netcode" }; + private readonly ISet<string> ValidateReferencesToAssemblies = new HashSet<string> { "StardewModdingAPI", "Stardew Valley", "StardewValley", "Netcode" }; /********* @@ -48,9 +48,15 @@ namespace StardewModdingAPI.Metadata yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); - // rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update) - yield return new Harmony1AssemblyRewriter(); + // rewrite for 64-bit mode + // re-enable in Stardew Valley 1.5.5 + //yield return new ArchitectureAssemblyRewriter(); + + // detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update) + yield return new HarmonyRewriter(); } + else + yield return new HarmonyRewriter(shouldRewrite: false); /**** ** detect mod issues @@ -62,13 +68,9 @@ namespace StardewModdingAPI.Metadata /**** ** detect code which may impact game stability ****/ - yield return new TypeFinder(typeof(HarmonyLib.Harmony).FullName, InstructionHandleResult.DetectedGamePatch); yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic); - yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerializer); - yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerializer); - yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerializer); - yield return new EventFinder(typeof(ISpecializedEvents).FullName, nameof(ISpecializedEvents.UnvalidatedUpdateTicked), InstructionHandleResult.DetectedUnvalidatedUpdateTick); - yield return new EventFinder(typeof(ISpecializedEvents).FullName, nameof(ISpecializedEvents.UnvalidatedUpdateTicking), InstructionHandleResult.DetectedUnvalidatedUpdateTick); + yield return new FieldFinder(typeof(SaveGame).FullName, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer); + yield return new EventFinder(typeof(ISpecializedEvents).FullName, new[] { nameof(ISpecializedEvents.UnvalidatedUpdateTicked), nameof(ISpecializedEvents.UnvalidatedUpdateTicking) }, InstructionHandleResult.DetectedUnvalidatedUpdateTick); /**** ** detect paranoid issues @@ -77,13 +79,19 @@ namespace StardewModdingAPI.Metadata { // filesystem access yield return new TypeFinder(typeof(System.Console).FullName, InstructionHandleResult.DetectedConsoleAccess); - yield return new TypeFinder(typeof(System.IO.File).FullName, InstructionHandleResult.DetectedFilesystemAccess); - yield return new TypeFinder(typeof(System.IO.FileStream).FullName, InstructionHandleResult.DetectedFilesystemAccess); - yield return new TypeFinder(typeof(System.IO.FileInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); - yield return new TypeFinder(typeof(System.IO.Directory).FullName, InstructionHandleResult.DetectedFilesystemAccess); - yield return new TypeFinder(typeof(System.IO.DirectoryInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); - yield return new TypeFinder(typeof(System.IO.DriveInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); - yield return new TypeFinder(typeof(System.IO.FileSystemWatcher).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder( + new[] + { + typeof(System.IO.File).FullName, + typeof(System.IO.FileStream).FullName, + typeof(System.IO.FileInfo).FullName, + typeof(System.IO.Directory).FullName, + typeof(System.IO.DirectoryInfo).FullName, + typeof(System.IO.DriveInfo).FullName, + typeof(System.IO.FileSystemWatcher).FullName + }, + InstructionHandleResult.DetectedFilesystemAccess + ); // shell access yield return new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3249e02f..3f97e531 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI /// <param name="args">The command-line arguments.</param> public static void Main(string[] args) { - Console.Title = $"SMAPI {EarlyConstants.RawApiVersion}{(EarlyConstants.IsWindows64BitHack ? " 64-bit" : "")} - {Console.Title}"; + Console.Title = $"SMAPI {EarlyConstants.RawApiVersion} - {Console.Title}"; try { @@ -84,10 +84,22 @@ namespace StardewModdingAPI } catch (Exception ex) { + // unofficial 64-bit + if (EarlyConstants.Platform == GamePlatform.Windows) + { + FileInfo linuxExecutable = new FileInfo(Path.Combine(EarlyConstants.ExecutionPath, "StardewValley.exe")); + if (linuxExecutable.Exists && LowLevelEnvironmentUtility.Is64BitAssembly(linuxExecutable.FullName)) + Program.PrintErrorAndExit("Oops! You're running Stardew Valley in unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info."); + } + // 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."); + // Stardew Valley 1.5.5+ + if (File.Exists(Path.Combine(EarlyConstants.ExecutionPath, "Stardew Valley.dll"))) + Program.PrintErrorAndExit("Oops! You're running Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io."); + // can't load file Program.PrintErrorAndExit( message: "Oops! SMAPI couldn't load the game executable. The technical details below may have more info.", @@ -112,16 +124,6 @@ namespace StardewModdingAPI // max version if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) Program.PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io."); - - // bitness - bool is64BitGame = LowLevelEnvironmentUtility.Is64BitAssembly(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe")); -#if SMAPI_FOR_WINDOWS_64BIT_HACK - if (!is64BitGame) - Program.PrintErrorAndExit("Oops! This is the 64-bit version of SMAPI, but you have the 32-bit version of Stardew Valley. You can reinstall SMAPI using its installer to automatically install the correct version of SMAPI."); -#elif SMAPI_FOR_WINDOWS - if (is64BitGame) - Program.PrintErrorAndExit("Oops! This is the 32-bit version of SMAPI, but you have the 64-bit version of Stardew Valley. You can reinstall SMAPI using its installer to automatically install the correct version of SMAPI."); -#endif } /// <summary>Assert that the versions of all SMAPI components are correct.</summary> diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 7d5e7ef9..0f1b0516 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,10 +14,6 @@ <Import Project="..\..\build\common.targets" /> - <PropertyGroup Condition="$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))"> - <PlatformTarget>x64</PlatformTarget> - </PropertyGroup> - <ItemGroup> <PackageReference Include="LargeAddressAware" Version="1.0.5" /> <PackageReference Include="Mono.Cecil" Version="0.11.4" /> @@ -39,7 +35,7 @@ <!-- Windows only --> <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" Condition="!$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))" /> + <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" /> <Reference Include="System.Windows.Forms" /> </ItemGroup> diff --git a/src/SMAPI/Utilities/PathUtilities.cs b/src/SMAPI/Utilities/PathUtilities.cs index 19f16ea9..541b163c 100644 --- a/src/SMAPI/Utilities/PathUtilities.cs +++ b/src/SMAPI/Utilities/PathUtilities.cs @@ -7,6 +7,13 @@ namespace StardewModdingAPI.Utilities public static class PathUtilities { /********* + ** Accessors + *********/ + /// <summary>The preferred directory separator character in an asset key.</summary> + public static char PreferredAssetSeparator { get; } = ToolkitPathUtilities.PreferredAssetSeparator; + + + /********* ** Public methods *********/ /// <summary>Get the segments from a path (e.g. <c>/usr/bin/example</c> => <c>usr</c>, <c>bin</c>, and <c>example</c>).</summary> @@ -18,8 +25,16 @@ namespace StardewModdingAPI.Utilities return ToolkitPathUtilities.GetSegments(path, limit); } - /// <summary>Normalize separators in a file path.</summary> + /// <summary>Normalize an asset name to match how MonoGame's content APIs would normalize and cache it.</summary> + /// <param name="assetName">The asset name to normalize.</param> + public static string NormalizeAssetName(string assetName) + { + return ToolkitPathUtilities.NormalizeAssetName(assetName); + } + + /// <summary>Normalize separators in a file path for the current platform.</summary> /// <param name="path">The file path to normalize.</param> + /// <remarks>This should only be used for file paths. For asset names, use <see cref="NormalizeAssetName"/> instead.</remarks> [Pure] public static string NormalizePath(string path) { |