diff options
19 files changed, 105 insertions, 27 deletions
diff --git a/build/common.targets b/build/common.targets index 258b48f2..c227190a 100644 --- a/build/common.targets +++ b/build/common.targets @@ -5,7 +5,10 @@ <Product>SMAPI</Product> <LangVersion>latest</LangVersion> <AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> - <Nullable>enable</Nullable> + + <!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported--> + <Nullable Condition="'$(TargetFramework)' == 'net5.0'">enable</Nullable> + <NoWarn Condition="'$(TargetFramework)' != 'net5.0'">$(NoWarn);CS8632</NoWarn> <!--set platform--> <DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants> diff --git a/docs/release-notes.md b/docs/release-notes.md index bb30f31a..9fc0d432 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,7 +20,6 @@ * Added `data-*` attributes to the log parser page for external tools. * Fixed JSON validator showing incorrect error for update keys without a subkey. - ### For mod authors This is a big release that includes the new APIs planned for SMAPI 4.0.0, alongside the old ones. @@ -40,6 +39,7 @@ the C# mod that loads them is updated. _This adds support for many previously unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more. Existing mod APIs should work fine as-is._ * Mod files loaded through SMAPI APIs (including `helper.Content.Load`) are now case-insensitive, even on Linux. * Other improvements: + * Added [command-line arguments](technical/smapi.md#command-line-arguments) to toggle developer mode (thanks to Tondorian!). * Added `IContentPack.ModContent` property. * Added `Constants.ContentPath`. * Added `IAssetName` fields to the info received by `IAssetEditor` and `IAssetLoader` methods. diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index 6123b28f..4c31f69b 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -413,19 +413,9 @@ when you compile it. ## Release notes ## Upcoming release -* Migrated from .NET Standard 2.0 to .NET 5.0. * Added detection for Xbox app game folders. * Internal refactoring. -**Troubleshooting:** -Due to the framework change, you might see build errors like _task failed unexpectedly_ that mentions `System.Runtime Version=5.0.0`. If so: - -1. Make sure you have Visual Studio 2022 or later. -2. Exit all instances of Visual Studio. -3. Delete the hidden `.vs` folder in your solution folder. -4. Delete the `bin` and `obj` folders in each project folder. -5. Reopen the solution and rebuild, and now it should work fine. - ## 4.0.0 Released 30 November 2021. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index 7da1e0f1..e117db2f 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -33,11 +33,12 @@ argument | purpose `--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. `--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. -SMAPI itself recognises two arguments **on Windows only**, but these are intended for internal use +SMAPI itself recognises five arguments **on Windows only**, but these are intended for internal use or testing and may change without warning. On Linux/macOS, see _environment variables_ below. argument | purpose -------- | ------- +`--developer-mode`<br />`--developer-mode-off` | Enable or disable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console. `--no-terminal` | The SMAPI launcher won't try to open a terminal window, and SMAPI won't log anything to the console. (Messages will still be written to the log file.) `--use-current-shell` | The SMAPI launcher won't try to open a terminal window, but SMAPI will still log to the console. (Messages will still be written to the log file.) `--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. @@ -49,6 +50,7 @@ can set temporary environment variables instead. For example: environment variable | purpose -------------------- | ------- +`SMAPI_DEVELOPER_MODE` | Equivalent to `--developer-mode` and `--developer-mode-off` above. The value must be `true` or `false`. `SMAPI_MODS_PATH` | Equivalent to `--mods-path` above. `SMAPI_NO_TERMINAL` | Equivalent to `--no-terminal` above. `SMAPI_USE_CURRENT_SHELL` | Equivalent to `--use-current-shell` above. diff --git a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj index 69fd3dbd..7ac3277e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <RootNamespace>StardewModdingAPI.ModBuildConfig.Analyzer</RootNamespace> <Version>3.0.0</Version> - <TargetFramework>net5.0</TargetFramework> + <TargetFramework>netstandard2.0</TargetFramework> <IncludeBuildOutput>false</IncludeBuildOutput> <OutputPath>bin</OutputPath> <LangVersion>latest</LangVersion> diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index 8e70293e..82eac7f6 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <!--build--> <RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace> - <TargetFramework>net5.0</TargetFramework> + <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking> diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj index d69d53d5..4c92b4db 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj +++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <RootNamespace>StardewModdingAPI</RootNamespace> <Description>Provides toolkit interfaces which are available to SMAPI mods.</Description> - <TargetFrameworks>net5.0</TargetFrameworks> + <TargetFrameworks>net5.0; netstandard2.0</TargetFrameworks> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs index 836b1134..939be771 100644 --- a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -105,7 +105,12 @@ namespace StardewModdingAPI.Toolkit.Framework /// <param name="raw">The raw characters to parse.</param> /// <param name="index">The index of the next character to read.</param> /// <param name="tag">The parsed tag.</param> - private static bool TryParseTag(char[] raw, ref int index, [NotNullWhen(true)] out string? tag) + private static bool TryParseTag(char[] raw, ref int index, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + out string? tag + ) { // read tag length int length = 0; diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index d40d8f2b..4c9ca2ff 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -16,7 +16,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public ModSiteKey Site { get; } /// <summary>The mod ID within the repository.</summary> +#if NET5_0_OR_GREATER [MemberNotNullWhen(true, nameof(LooksValid))] +#endif public string? ID { get; } /// <summary>If specified, a substring in download names/descriptions to match.</summary> diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 6a8c4c43..ec27bf79 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <RootNamespace>StardewModdingAPI.Toolkit</RootNamespace> <Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description> - <TargetFrameworks>net5.0</TargetFrameworks> + <TargetFrameworks>net5.0; netstandard2.0</TargetFrameworks> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index cbdd7a85..2cb27e11 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -198,7 +198,12 @@ namespace StardewModdingAPI.Toolkit /// <param name="version">The version string.</param> /// <param name="parsed">The parsed representation.</param> /// <returns>Returns whether parsing the version succeeded.</returns> - public static bool TryParse(string? version, [NotNullWhen(true)] out ISemanticVersion? parsed) + public static bool TryParse(string? version, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + out ISemanticVersion? parsed + ) { return SemanticVersion.TryParse(version, allowNonStandard: false, out parsed); } @@ -208,7 +213,12 @@ namespace StardewModdingAPI.Toolkit /// <param name="allowNonStandard">Whether to allow non-standard extensions to semantic versioning.</param> /// <param name="parsed">The parsed representation.</param> /// <returns>Returns whether parsing the version succeeded.</returns> - public static bool TryParse(string? version, bool allowNonStandard, [NotNullWhen(true)] out ISemanticVersion? parsed) + public static bool TryParse(string? version, bool allowNonStandard, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + out ISemanticVersion? parsed + ) { if (version == null) { diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs index 7d1804e5..3c9308f2 100644 --- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -37,7 +37,12 @@ namespace StardewModdingAPI.Toolkit.Serialization /// <returns>Returns false if the file doesn't exist, else true.</returns> /// <exception cref="ArgumentException">The given <paramref name="fullPath"/> is empty or invalid.</exception> /// <exception cref="JsonReaderException">The file contains invalid JSON.</exception> - public bool ReadJsonFileIfExists<TModel>(string fullPath, [NotNullWhen(true)] out TModel? result) + public bool ReadJsonFileIfExists<TModel>(string fullPath, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + out TModel? result + ) { // validate if (string.IsNullOrWhiteSpace(fullPath)) diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 3ab5edfb..01010602 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -115,7 +115,9 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models *********/ /// <summary>Normalize whitespace in a raw string.</summary> /// <param name="input">The input to strip.</param> +#if NET5_0_OR_GREATER [return: NotNullIfNotNull("input")] +#endif private string? NormalizeWhitespace(string? input) { return input diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs index dcdbcf74..f7dc8aa8 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs @@ -33,7 +33,9 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models *********/ /// <summary>Normalize whitespace in a raw string.</summary> /// <param name="input">The input to strip.</param> +#if NET5_0_OR_GREATER [return: NotNullIfNotNull("input")] +#endif private string? NormalizeWhitespace(string? input) { return input?.Trim(); diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs index 5d1b73b1..fa254ea7 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs @@ -54,7 +54,9 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models *********/ /// <summary>Normalize whitespace in a raw string.</summary> /// <param name="input">The input to strip.</param> +#if NET5_0_OR_GREATER [return: NotNullIfNotNull("input")] +#endif private string? NormalizeWhitespace(string? input) { return input?.Trim(); diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 9b7a78a0..136279f2 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -50,7 +50,9 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <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> [Pure] +#if NET5_0_OR_GREATER [return: NotNullIfNotNull("assetName")] +#endif public static string? NormalizeAssetName(string? assetName) { assetName = assetName?.Trim(); @@ -64,7 +66,9 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <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] +#if NET5_0_OR_GREATER [return: NotNullIfNotNull("path")] +#endif public static string? NormalizePath(string? path) { path = path?.Trim(); @@ -89,7 +93,7 @@ namespace StardewModdingAPI.Toolkit.Utilities } // keep trailing separator - if ((!hasRoot || segments.Any()) && PathUtilities.PossiblePathSeparators.Contains(path[^1])) + if ((!hasRoot || segments.Any()) && PathUtilities.PossiblePathSeparators.Contains(path[path.Length - 1])) newPath += PathUtilities.PreferredPathSeparator; return newPath; @@ -101,7 +105,43 @@ namespace StardewModdingAPI.Toolkit.Utilities [Pure] public static string GetRelativePath(string sourceDir, string targetPath) { +#if NET5_0 return Path.GetRelativePath(sourceDir, targetPath); +#else + // NOTE: + // this is a heuristic implementation that works in the cases SMAPI needs it for, but it + // doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between + // UNC paths on Windows). SMAPI and mods will use the more robust .NET 5 version anyway + // though, this is only for compatibility with the mod build package. + + // convert to URIs + Uri from = new(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); + + // get relative path + string rawUrl = Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()); + if (rawUrl.StartsWith("file://")) + rawUrl = PathUtilities.WindowsUncRoot + rawUrl.Substring("file://".Length); + string relative = PathUtilities.NormalizePath(rawUrl); + + // normalize + if (relative == "") + relative = "."; + else + { + // trim trailing slash from URL + if (relative.EndsWith(PathUtilities.PreferredPathSeparator.ToString())) + relative = relative.Substring(0, relative.Length - 1); + + // fix root + if (relative.StartsWith("file:") && !targetPath.Contains("file:")) + relative = relative.Substring("file:".Length); + } + + return relative; +#endif } /// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary> diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index 82b59290..3c51c95d 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -101,7 +101,7 @@ namespace StardewModdingAPI.Events mod: this.Mod, priority: priority, onBehalfOf: null, - _ => this.Mod.Mod.Helper.Content.Load<TAsset>(relativePath)) + _ => this.Mod.Mod.Helper.ModContent.Load<TAsset>(relativePath)) ); } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6ca463a2..1a58d84b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -173,7 +173,8 @@ namespace StardewModdingAPI.Framework /// <summary>Construct an instance.</summary> /// <param name="modsPath">The path to search for mods.</param> /// <param name="writeToConsole">Whether to output log messages to the console.</param> - public SCore(string modsPath, bool writeToConsole) + /// <param name="developerMode">Whether to enable development features, or <c>null</c> to use the value from the settings file.</param> + public SCore(string modsPath, bool writeToConsole, bool? developerMode) { SCore.Instance = this; @@ -190,6 +191,7 @@ namespace StardewModdingAPI.Framework this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)); if (File.Exists(Constants.ApiUserConfigPath)) JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); + this.Settings.DeveloperMode = developerMode ?? this.Settings.DeveloperMode; this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog); this.CommandManager = new CommandManager(this.Monitor); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a8664160..b2e213fe 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -181,27 +181,40 @@ namespace StardewModdingAPI bool writeToConsole = !args.Contains("--no-terminal") && Environment.GetEnvironmentVariable("SMAPI_NO_TERMINAL") == null; // get mods path + bool? developerMode = null; string modsPath; { string rawModsPath = null; - // get from command line args + // get mods path from command line args int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1; if (pathIndex >= 1 && args.Length >= pathIndex) rawModsPath = args[pathIndex]; + // get developer mode from command line args + if (args.Contains("--developer-mode")) + developerMode = true; + if (args.Contains("--developer-mode-off")) + developerMode = false; + // get from environment variables if (string.IsNullOrWhiteSpace(rawModsPath)) rawModsPath = Environment.GetEnvironmentVariable("SMAPI_MODS_PATH"); + if (developerMode is null) + { + string rawDeveloperMode = Environment.GetEnvironmentVariable("SMAPI_DEVELOPER_MODE"); + if (rawDeveloperMode != null) + developerMode = bool.Parse(rawDeveloperMode); + } - // normalise + // normalize modsPath = !string.IsNullOrWhiteSpace(rawModsPath) ? Path.Combine(Constants.GamePath, rawModsPath) : Constants.DefaultModsPath; } // load SMAPI - using SCore core = new(modsPath, writeToConsole); + using SCore core = new(modsPath, writeToConsole, developerMode); core.RunInteractively(); } |