summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets9
-rw-r--r--docs/release-notes.md24
-rw-r--r--docs/technical/smapi.md16
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs54
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs54
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj2
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Tests/Core/ModResolverTests.cs5
-rw-r--r--src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs26
-rw-r--r--src/SMAPI.Toolkit/ModToolkit.cs21
-rw-r--r--src/SMAPI.Toolkit/Utilities/PathUtilities.cs15
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json39
-rw-r--r--src/SMAPI/Constants.cs16
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForMap.cs6
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs2
-rw-r--r--src/SMAPI/Framework/ContentPack.cs10
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs7
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs22
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs22
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs57
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs34
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs27
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs14
-rw-r--r--src/SMAPI/Framework/ModLoading/IInstructionHandler.cs5
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs18
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs5
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs31
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs (renamed from src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs)50
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs6
-rw-r--r--src/SMAPI/Framework/SCore.cs105
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs28
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs40
-rw-r--r--src/SMAPI/Program.cs24
-rw-r--r--src/SMAPI/SMAPI.csproj6
-rw-r--r--src/SMAPI/Utilities/PathUtilities.cs17
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)
{