Merge branch 'develop' into stable
This commit is contained in:
commit
93a748996c
Binary file not shown.
|
@ -845,7 +845,7 @@
|
|||
|
||||
</member>
|
||||
<member name="F:HarmonyLib.ExceptionBlockType.BeginExceptFilterBlock">
|
||||
<summary>The beginning of an except filter block</summary>
|
||||
<summary>The beginning of an except filter block (currently not supported to use in a patch)</summary>
|
||||
|
||||
</member>
|
||||
<member name="F:HarmonyLib.ExceptionBlockType.BeginFaultBlock">
|
||||
|
@ -2660,6 +2660,18 @@
|
|||
<param name="operand">The optional operand</param>
|
||||
<param name="name">The optional name</param>
|
||||
|
||||
</member>
|
||||
<member name="M:HarmonyLib.CodeMatch.#ctor(System.Linq.Expressions.Expression{System.Action},System.String)">
|
||||
<summary>Creates a code match that calls a method</summary>
|
||||
<param name="expression">The lambda expression using the method</param>
|
||||
<param name="name">The optional name</param>
|
||||
|
||||
</member>
|
||||
<member name="M:HarmonyLib.CodeMatch.#ctor(System.Linq.Expressions.LambdaExpression,System.String)">
|
||||
<summary>Creates a code match that calls a method</summary>
|
||||
<param name="expression">The lambda expression using the method</param>
|
||||
<param name="name">The optional name</param>
|
||||
|
||||
</member>
|
||||
<member name="M:HarmonyLib.CodeMatch.#ctor(HarmonyLib.CodeInstruction,System.String)">
|
||||
<summary>Creates a code match</summary>
|
||||
|
@ -3216,6 +3228,13 @@
|
|||
<param name="e">The enum</param>
|
||||
<returns>True if the instruction loads the constant</returns>
|
||||
|
||||
</member>
|
||||
<member name="M:HarmonyLib.CodeInstructionExtensions.LoadsConstant(HarmonyLib.CodeInstruction,System.String)">
|
||||
<summary>Tests if the code instruction loads a string constant</summary>
|
||||
<param name="code">The <see cref="T:HarmonyLib.CodeInstruction"/></param>
|
||||
<param name="str">The string</param>
|
||||
<returns>True if the instruction loads the constant</returns>
|
||||
|
||||
</member>
|
||||
<member name="M:HarmonyLib.CodeInstructionExtensions.LoadsField(HarmonyLib.CodeInstruction,System.Reflection.FieldInfo,System.Boolean)">
|
||||
<summary>Tests if the code instruction loads a field</summary>
|
||||
|
@ -3346,7 +3365,11 @@
|
|||
<summary>A file log for debugging</summary>
|
||||
|
||||
</member>
|
||||
<member name="F:HarmonyLib.FileLog.logPath">
|
||||
<member name="P:HarmonyLib.FileLog.LogWriter">
|
||||
<summary>Set this to make Harmony write its log content to this stream</summary>
|
||||
|
||||
</member>
|
||||
<member name="P:HarmonyLib.FileLog.LogPath">
|
||||
<summary>Full pathname of the log file, defaults to a file called <c>harmony.log.txt</c> on your Desktop</summary>
|
||||
|
||||
</member>
|
||||
|
|
|
@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed.
|
|||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!--set general build properties -->
|
||||
<Version>3.16.2</Version>
|
||||
<Version>3.17.0</Version>
|
||||
<Product>SMAPI</Product>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||
|
|
|
@ -158,6 +158,10 @@ foreach ($folder in $folders) {
|
|||
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
|
||||
}
|
||||
|
||||
if ($folder -eq "windows") {
|
||||
cp "$smapiBin/VdfConverter.dll" "$bundlePath/smapi-internal"
|
||||
}
|
||||
|
||||
cp "$smapiBin/SMAPI.config.json" "$bundlePath/smapi-internal/config.json"
|
||||
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
|
||||
if ($folder -eq "linux" -or $folder -eq "macOS") {
|
||||
|
|
|
@ -7,6 +7,28 @@
|
|||
_If needed, you can update to SMAPI 3.16.0 first and then install the latest version._
|
||||
-->
|
||||
|
||||
## 3.17.0
|
||||
Released 09 October 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/73090322).
|
||||
|
||||
* For players:
|
||||
* You can now download SMAPI 'strict mode' from [Nexus files](https://www.nexusmods.com/stardewvalley/mods/2400/?tab=files), which removes all deprecated APIs. This may significantly improve performance, but mods which still show deprecation warnings won't work.
|
||||
* The SMAPI installer now also detects game folders in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!).
|
||||
* SMAPI now prevents mods from enabling Harmony debug mode, which impacts performance and creates a file on your desktop.
|
||||
_You can allow debug mode by editing `smapi-internal/config.json` in your game folder._
|
||||
* Optimized performance and memory usage (thanks to atravita!).
|
||||
* Other internal optimizations.
|
||||
* Added more file extensions to ignore when searching for mod folders: `.7z`, `.tar`, `.tar.gz`, and `.xcf` (thanks to atravita!).
|
||||
* Removed transitional `UseRawImageLoading` option added in 3.15.0. This is now always enabled, except when PyTK is installed.
|
||||
* Fixed update alerts incorrectly shown for prerelease versions on GitHub that aren't marked as prerelease.
|
||||
|
||||
* For mod authors:
|
||||
* When [providing a mod API in a C# mod](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations), you can now get the mod requesting it as an optional parameter (thanks to KhloeLeclair!).
|
||||
* SMAPI now treats square brackets in the manifest `Name` field as round ones to avoid breaking tools which parse log files.
|
||||
* Made deprecation message wording stronger for the upcoming SMAPI 4.0.0 release.
|
||||
* The `Texture2D.Name` field is now set earlier to support mods like SpriteMaster.
|
||||
* Updated dependencies: [Harmony](https://harmony.pardeike.net) 2.2.2 (see [changes](https://github.com/pardeike/Harmony/releases/tag/v2.2.2.0)) and [FluentHttpClient](https://github.com/Pathoschild/FluentHttpClient#readme) 4.2.0 (see [changes](https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#420)).
|
||||
* Fixed `LocationListChanged` event not raised & memory leak occurring when a generated mine/volcano is removed (thanks to tylergibbs2!).
|
||||
|
||||
## 3.16.2
|
||||
Released 31 August 2022 for Stardew Valley 1.5.6 or later.
|
||||
|
||||
|
|
|
@ -412,8 +412,12 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
|
|||
when you compile it.
|
||||
|
||||
## Release notes
|
||||
### Upcoming release
|
||||
### 4.0.2
|
||||
Released 09 October 2022.
|
||||
|
||||
* Switched to the newer crossplatform `portable` debug symbols (thanks to lanturnalis!).
|
||||
* Fixed `BundleExtraAssemblies` option being partly case-sensitive.
|
||||
* Fixed `BundleExtraAssemblies` not applying `All` value to game assemblies.
|
||||
|
||||
### 4.0.1
|
||||
Released 14 April 2022.
|
||||
|
|
|
@ -78,8 +78,8 @@ the `SMAPI` project with debugging from Visual Studio or Rider should launch SMA
|
|||
debugger attached, so you can intercept errors and step through the code being executed.
|
||||
|
||||
### Custom Harmony build
|
||||
SMAPI uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme), which is
|
||||
included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
|
||||
SMAPI uses [a custom build of Harmony 2.2.2](https://github.com/Pathoschild/Harmony#readme), which
|
||||
is included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
|
||||
folder before compiling.
|
||||
|
||||
## Prepare a release
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<!--NuGet package-->
|
||||
<PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId>
|
||||
<Title>Build package for SMAPI mods</Title>
|
||||
<Version>4.0.1</Version>
|
||||
<Version>4.0.2</Version>
|
||||
<Authors>Pathoschild</Authors>
|
||||
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
|
@ -27,8 +27,12 @@
|
|||
<EnableGameDebugging Condition="'$(EnableGameDebugging)' == ''">true</EnableGameDebugging>
|
||||
<BundleExtraAssemblies Condition="'$(BundleExtraAssemblies)' == ''"></BundleExtraAssemblies>
|
||||
|
||||
<!-- simplify conditions -->
|
||||
<_BundleExtraAssembliesForGame>$([System.Text.RegularExpressions.Regex]::IsMatch('$(BundleExtraAssemblies)', '\bGame|All\b', RegexOptions.IgnoreCase))</_BundleExtraAssembliesForGame>
|
||||
<_BundleExtraAssembliesForAny>$([System.Text.RegularExpressions.Regex]::IsMatch('$(BundleExtraAssemblies)', '\bGame|System|ThirdParty|All\b', RegexOptions.IgnoreCase))</_BundleExtraAssembliesForAny>
|
||||
|
||||
<!-- coppy referenced DLLs into build output -->
|
||||
<CopyLocalLockFileAssemblies Condition="$(BundleExtraAssemblies.Contains('ThirdParty')) OR $(BundleExtraAssemblies.Contains('Game')) OR $(BundleExtraAssemblies.Contains('System')) OR $(BundleExtraAssemblies.Contains('All'))">true</CopyLocalLockFileAssemblies>
|
||||
<CopyLocalLockFileAssemblies Condition="$(_BundleExtraAssembliesForAny)">true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(OS)' == 'Windows_NT' AND '$(EnableGameDebugging)' == 'true'">
|
||||
|
@ -44,17 +48,17 @@
|
|||
**********************************************-->
|
||||
<ItemGroup>
|
||||
<!-- game -->
|
||||
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
|
||||
<!-- SMAPI -->
|
||||
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
|
||||
<!-- Harmony -->
|
||||
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
|
||||
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(_BundleExtraAssembliesForGame)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <param name="color">The color to set.</param>
|
||||
private bool TryParseColor(string input, out Color color)
|
||||
{
|
||||
string[] colorHexes = input.Split(new[] { ',' }, 3);
|
||||
string[] colorHexes = input.Split(',', 3);
|
||||
if (int.TryParse(colorHexes[0], out int r) && int.TryParse(colorHexes[1], out int g) && int.TryParse(colorHexes[2], out int b))
|
||||
{
|
||||
color = new Color(r, g, b);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.16.2",
|
||||
"Version": "3.17.0",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.16.2"
|
||||
"MinimumApiVersion": "3.17.0"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Error Handler",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.16.2",
|
||||
"Version": "3.17.0",
|
||||
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
||||
"UniqueID": "SMAPI.ErrorHandler",
|
||||
"EntryDll": "ErrorHandler.dll",
|
||||
"MinimumApiVersion": "3.16.2"
|
||||
"MinimumApiVersion": "3.17.0"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.16.2",
|
||||
"Version": "3.17.0",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.16.2"
|
||||
"MinimumApiVersion": "3.17.0"
|
||||
}
|
||||
|
|
|
@ -136,11 +136,11 @@ namespace SMAPI.Tests.Utilities
|
|||
foreach (string rawPair in stateMap.Split(','))
|
||||
{
|
||||
// parse values
|
||||
string[] parts = rawPair.Split(new[] { ':' }, 2);
|
||||
string[] parts = rawPair.Split(':', 2, StringSplitOptions.TrimEntries);
|
||||
if (!Enum.TryParse(parts[0], ignoreCase: true, out SButton curButton))
|
||||
Assert.Fail($"The state map is invalid: unknown button value '{parts[0].Trim()}'");
|
||||
Assert.Fail($"The state map is invalid: unknown button value '{parts[0]}'");
|
||||
if (!Enum.TryParse(parts[1], ignoreCase: true, out SButtonState state))
|
||||
Assert.Fail($"The state map is invalid: unknown state value '{parts[1].Trim()}'");
|
||||
Assert.Fail($"The state map is invalid: unknown state value '{parts[1]}'");
|
||||
|
||||
// get state
|
||||
if (curButton == button)
|
||||
|
|
|
@ -382,11 +382,11 @@ namespace SMAPI.Tests.Utilities
|
|||
{
|
||||
// act
|
||||
string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr));
|
||||
SemanticVersion after = JsonConvert.DeserializeObject<SemanticVersion>(json);
|
||||
SemanticVersion? after = JsonConvert.DeserializeObject<SemanticVersion>(json);
|
||||
|
||||
// assert
|
||||
Assert.IsNotNull(after, "The semantic version after deserialization is unexpectedly null.");
|
||||
Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialization doesn't match the input version.");
|
||||
Assert.AreEqual(versionStr, after!.ToString(), "The semantic version after deserialization doesn't match the input version.");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ using StardewModdingAPI.Toolkit.Utilities;
|
|||
using System.Reflection;
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
using Microsoft.Win32;
|
||||
using VdfParser;
|
||||
#endif
|
||||
|
||||
namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
||||
|
@ -23,6 +24,9 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
|||
/// <summary>The current OS.</summary>
|
||||
private readonly Platform Platform;
|
||||
|
||||
/// <summary>The Steam app ID for Stardew Valley.</summary>
|
||||
private const string SteamAppId = "413150";
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -145,7 +149,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
|||
#if SMAPI_FOR_WINDOWS
|
||||
IDictionary<string, string> registryKeys = new Dictionary<string, string>
|
||||
{
|
||||
[@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam
|
||||
[@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App " + GameScanner.SteamAppId] = "InstallLocation", // Steam
|
||||
[@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows
|
||||
};
|
||||
foreach (var pair in registryKeys)
|
||||
|
@ -158,7 +162,15 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
|||
// via Steam library path
|
||||
string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
|
||||
if (steamPath != null)
|
||||
{
|
||||
// conventional path
|
||||
yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
|
||||
|
||||
// from Steam's .vdf file
|
||||
string? path = this.GetPathFromSteamLibrary(steamPath);
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
yield return path;
|
||||
}
|
||||
#endif
|
||||
|
||||
// default GOG/Steam paths
|
||||
|
@ -243,6 +255,42 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
|||
using (openKey)
|
||||
return (string?)openKey.GetValue(name);
|
||||
}
|
||||
|
||||
/// <summary>Get the game directory path from alternative Steam library locations.</summary>
|
||||
/// <param name="steamPath">The full path to the directory containing steam.exe.</param>
|
||||
/// <returns>The game directory, if found.</returns>
|
||||
private string? GetPathFromSteamLibrary(string? steamPath)
|
||||
{
|
||||
if (steamPath == null)
|
||||
return null;
|
||||
|
||||
// get raw .vdf data
|
||||
string libraryFoldersPath = Path.Combine(steamPath.Replace('/', '\\'), "steamapps\\libraryfolders.vdf");
|
||||
using FileStream fileStream = File.OpenRead(libraryFoldersPath);
|
||||
VdfDeserializer deserializer = new();
|
||||
dynamic libraries = deserializer.Deserialize(fileStream);
|
||||
if (libraries?.libraryfolders is null)
|
||||
return null;
|
||||
|
||||
// get path from Stardew Valley app (if any)
|
||||
foreach (dynamic pair in libraries.libraryfolders)
|
||||
{
|
||||
dynamic library = pair.Value;
|
||||
|
||||
foreach (dynamic app in library.apps)
|
||||
{
|
||||
string key = app.Key;
|
||||
if (key == GameScanner.SteamAppId)
|
||||
{
|
||||
string path = library.path;
|
||||
|
||||
return Path.Combine(path.Replace("\\\\", "\\"), "steamapps", "common", "Stardew Valley");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,10 +45,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
|||
".png",
|
||||
".psd",
|
||||
".tif",
|
||||
".xcf", // gimp files
|
||||
|
||||
// archives
|
||||
".rar",
|
||||
".zip",
|
||||
".7z",
|
||||
".tar",
|
||||
".tar.gz",
|
||||
|
||||
// backup files
|
||||
".backup",
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
|
||||
<PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
|
||||
<PackageReference Include="VdfConverter" Version="1.0.3" Condition="'$(OS)' == 'Windows_NT'" Private="False" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Serialization.Converters;
|
||||
|
||||
|
@ -90,13 +91,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
|
|||
[JsonConstructor]
|
||||
public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys)
|
||||
{
|
||||
this.UniqueID = this.NormalizeWhitespace(uniqueId);
|
||||
this.Name = this.NormalizeWhitespace(name);
|
||||
this.Author = this.NormalizeWhitespace(author);
|
||||
this.Description = this.NormalizeWhitespace(description);
|
||||
this.UniqueID = this.NormalizeField(uniqueId);
|
||||
this.Name = this.NormalizeField(name, replaceSquareBrackets: true);
|
||||
this.Author = this.NormalizeField(author);
|
||||
this.Description = this.NormalizeField(description);
|
||||
this.Version = version;
|
||||
this.MinimumApiVersion = minimumApiVersion;
|
||||
this.EntryDll = this.NormalizeWhitespace(entryDll);
|
||||
this.EntryDll = this.NormalizeField(entryDll);
|
||||
this.ContentPackFor = contentPackFor;
|
||||
this.Dependencies = dependencies ?? Array.Empty<IManifestDependency>();
|
||||
this.UpdateKeys = updateKeys ?? Array.Empty<string>();
|
||||
|
@ -113,17 +114,47 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Normalize whitespace in a raw string.</summary>
|
||||
/// <summary>Normalize a manifest field to strip newlines, trim whitespace, and optionally strip square brackets.</summary>
|
||||
/// <param name="input">The input to strip.</param>
|
||||
/// <param name="replaceSquareBrackets">Whether to replace square brackets with round ones. This is used in the mod name to avoid breaking the log format.</param>
|
||||
#if NET5_0_OR_GREATER
|
||||
[return: NotNullIfNotNull("input")]
|
||||
#endif
|
||||
private string? NormalizeWhitespace(string? input)
|
||||
private string? NormalizeField(string? input, bool replaceSquareBrackets = false)
|
||||
{
|
||||
return input
|
||||
?.Trim()
|
||||
.Replace("\r", "")
|
||||
.Replace("\n", "");
|
||||
input = input?.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(input))
|
||||
{
|
||||
StringBuilder? builder = null;
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
switch (input[i])
|
||||
{
|
||||
case '\r':
|
||||
case '\n':
|
||||
builder ??= new StringBuilder(input);
|
||||
builder[i] = ' ';
|
||||
break;
|
||||
|
||||
case '[' when replaceSquareBrackets:
|
||||
builder ??= new StringBuilder(input);
|
||||
builder[i] = '(';
|
||||
break;
|
||||
|
||||
case ']' when replaceSquareBrackets:
|
||||
builder ??= new StringBuilder(input);
|
||||
builder[i] = ')';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (builder != null)
|
||||
input = builder.ToString();
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,11 +159,20 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
continue;
|
||||
}
|
||||
|
||||
// if there's only a prerelease version (e.g. from GitHub), don't override the main version
|
||||
ISemanticVersion? curMain = data.Version;
|
||||
ISemanticVersion? curPreview = data.PreviewVersion;
|
||||
if (curPreview == null && curMain?.IsPrerelease() == true)
|
||||
{
|
||||
curPreview = curMain;
|
||||
curMain = null;
|
||||
}
|
||||
|
||||
// handle versions
|
||||
if (this.IsNewer(data.Version, main?.Version))
|
||||
main = new ModEntryVersionModel(data.Version, data.Url!);
|
||||
if (this.IsNewer(data.PreviewVersion, optional?.Version))
|
||||
optional = new ModEntryVersionModel(data.PreviewVersion, data.Url!);
|
||||
if (this.IsNewer(curMain, main?.Version))
|
||||
main = new ModEntryVersionModel(curMain, data.Url!);
|
||||
if (this.IsNewer(curPreview, optional?.Version))
|
||||
optional = new ModEntryVersionModel(curPreview, data.Url!);
|
||||
}
|
||||
|
||||
// get unofficial version
|
||||
|
|
|
@ -121,10 +121,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
HtmlNode? node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]");
|
||||
if (node != null)
|
||||
{
|
||||
string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
string[] errorParts = node.InnerText.Trim().Split('\n', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
string errorCode = errorParts[0];
|
||||
string? errorText = errorParts.Length > 1 ? errorParts[1] : null;
|
||||
switch (errorCode.Trim().ToLower())
|
||||
switch (errorCode.ToLower())
|
||||
{
|
||||
case "not found":
|
||||
return null;
|
||||
|
|
|
@ -199,8 +199,15 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
log.ApiVersion = match.Groups["apiVersion"].Value;
|
||||
log.GameVersion = match.Groups["gameVersion"].Value;
|
||||
log.OperatingSystem = match.Groups["os"].Value;
|
||||
smapiMod.OverrideVersion(log.ApiVersion);
|
||||
|
||||
const string strictModeSuffix = " (strict mode)";
|
||||
if (log.ApiVersion.EndsWith(strictModeSuffix))
|
||||
{
|
||||
log.IsStrictMode = true;
|
||||
log.ApiVersion = log.ApiVersion[..^strictModeSuffix.Length];
|
||||
}
|
||||
|
||||
smapiMod.OverrideVersion(log.ApiVersion);
|
||||
log.ApiVersionParsed = smapiMod.GetParsedVersion();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
|||
/****
|
||||
** Log data
|
||||
****/
|
||||
/// <summary>Whether SMAPI is running in strict mode, which disables all deprecated APIs.</summary>
|
||||
public bool IsStrictMode { get; set; }
|
||||
|
||||
/// <summary>The SMAPI version.</summary>
|
||||
public string? ApiVersion { get; set; }
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Pathoschild.FluentNexus" Version="1.0.5" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<meta name="robots" content="noindex" />
|
||||
}
|
||||
<link rel="stylesheet" href="~/Content/css/file-upload.css" />
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css" />
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=20221009" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script>
|
||||
|
@ -243,7 +243,7 @@ else if (log?.IsValid == true)
|
|||
@if (log?.IsValid == true)
|
||||
{
|
||||
<div id="output">
|
||||
@if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode)
|
||||
@if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode || log.IsStrictMode)
|
||||
{
|
||||
<h2>Suggested fixes</h2>
|
||||
<ul id="fix-list">
|
||||
|
@ -256,9 +256,16 @@ else if (log?.IsValid == true)
|
|||
<li>Your <strong>Error Handler</strong> mod is older than SMAPI. You may be missing some game/mod error fixes. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to update it.</li>
|
||||
}
|
||||
@if (isPyTkCompatibilityMode)
|
||||
{
|
||||
if (log.IsStrictMode)
|
||||
{
|
||||
<li>PyTK's image scaling isn't compatible with SMAPI strict mode.</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li>PyTK 1.23.* or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.</li>
|
||||
}
|
||||
}
|
||||
@if (outdatedMods.Any())
|
||||
{
|
||||
<li>
|
||||
|
@ -307,6 +314,10 @@ else if (log?.IsValid == true)
|
|||
</table>
|
||||
</li>
|
||||
}
|
||||
@if (log.IsStrictMode)
|
||||
{
|
||||
<li class="notice">SMAPI is running in 'strict mode', which removes all deprecated APIs. This can significantly improve performance, but some mods may not work. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to disable it if you run into problems.</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
|
@ -329,7 +340,13 @@ else if (log?.IsValid == true)
|
|||
</tr>
|
||||
<tr>
|
||||
<th>SMAPI:</th>
|
||||
<td v-pre>@log.ApiVersion</td>
|
||||
<td v-pre>
|
||||
@log.ApiVersion
|
||||
@if (log.IsStrictMode)
|
||||
{
|
||||
<strong>(strict mode)</strong>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Folder:</th>
|
||||
|
@ -361,7 +378,7 @@ else if (log?.IsValid == true)
|
|||
ContentPacks: contentPacks?.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) == true ? contentPackList : Array.Empty<LogModInfo>()
|
||||
))
|
||||
.ToList();
|
||||
if (contentPacks?.TryGetValue("", out LogModInfo[] invalidPacks) == true)
|
||||
if (contentPacks?.TryGetValue("", out LogModInfo[]? invalidPacks) == true)
|
||||
{
|
||||
modsWithContentPacks.Add((
|
||||
Mod: new LogModInfo(ModType.CodeMod, "<invalid content packs>", "", "", ""),
|
||||
|
|
|
@ -73,6 +73,11 @@ table caption {
|
|||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#fix-list li.notice {
|
||||
background: #EEFFEE;
|
||||
border-color: #080;
|
||||
}
|
||||
|
||||
#fix-list li.important {
|
||||
background: #FCC;
|
||||
border-color: #800;
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace StardewModdingAPI
|
|||
internal static int? LogScreenId { get; set; }
|
||||
|
||||
/// <summary>SMAPI's current raw semantic version.</summary>
|
||||
internal static string RawApiVersion = "3.16.2";
|
||||
internal static string RawApiVersion = "3.17.0";
|
||||
}
|
||||
|
||||
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -32,60 +33,71 @@ namespace StardewModdingAPI.Framework.Content
|
|||
/// <inheritdoc />
|
||||
public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
|
||||
{
|
||||
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
||||
|
||||
// validate source data
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source), "Can't patch from null source data.");
|
||||
|
||||
// get the pixels for the source area
|
||||
Color[] sourceData;
|
||||
{
|
||||
// get normalized bounds
|
||||
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
||||
if (source.Data.Length < (sourceArea.Value.Bottom - 1) * source.Width + sourceArea.Value.Right)
|
||||
throw new ArgumentException("Can't apply image patch because the source image is smaller than the source area.", nameof(source));
|
||||
int areaX = sourceArea.Value.X;
|
||||
int areaY = sourceArea.Value.Y;
|
||||
int areaWidth = sourceArea.Value.Width;
|
||||
int areaHeight = sourceArea.Value.Height;
|
||||
|
||||
if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height)
|
||||
sourceData = source.Data;
|
||||
else
|
||||
// shortcut: if the area width matches the source image, we can apply the image as-is without needing
|
||||
// to copy the pixels into a smaller subset. It's fine if the source is taller than the area, since we'll
|
||||
// just ignore the extra data at the end of the pixel array.
|
||||
if (areaWidth == source.Width)
|
||||
{
|
||||
sourceData = new Color[areaWidth * areaHeight];
|
||||
int i = 0;
|
||||
for (int y = areaY, maxY = areaY + areaHeight - 1; y <= maxY; y++)
|
||||
{
|
||||
for (int x = areaX, maxX = areaX + areaWidth - 1; x <= maxX; x++)
|
||||
{
|
||||
int targetIndex = (y * source.Width) + x;
|
||||
sourceData[i++] = source.Data[targetIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.PatchImageImpl(source.Data, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY);
|
||||
return;
|
||||
}
|
||||
|
||||
// else copy the pixels within the smaller area & apply that
|
||||
int pixelCount = areaWidth * areaHeight;
|
||||
Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
||||
try
|
||||
{
|
||||
for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++)
|
||||
{
|
||||
int sourceIndex = (y * source.Width) + areaX;
|
||||
int targetIndex = (y - areaY) * areaWidth;
|
||||
Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth);
|
||||
}
|
||||
|
||||
// apply
|
||||
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<Color>.Shared.Return(sourceData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
|
||||
{
|
||||
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
||||
|
||||
// validate source texture
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture.");
|
||||
|
||||
// get normalized bounds
|
||||
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
||||
if (!source.Bounds.Contains(sourceArea.Value))
|
||||
throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
|
||||
|
||||
// get source data
|
||||
// get source data & apply
|
||||
int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height;
|
||||
Color[] sourceData = GC.AllocateUninitializedArray<Color>(pixelCount);
|
||||
Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
||||
try
|
||||
{
|
||||
source.GetData(0, sourceArea, sourceData, 0, pixelCount);
|
||||
|
||||
// apply
|
||||
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<Color>.Shared.Return(sourceData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ExtendImage(int minWidth, int minHeight)
|
||||
|
@ -94,7 +106,7 @@ namespace StardewModdingAPI.Framework.Content
|
|||
return false;
|
||||
|
||||
Texture2D original = this.Data;
|
||||
Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight));
|
||||
Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)).SetName(original.Name);
|
||||
this.ReplaceWith(texture);
|
||||
this.PatchImage(original);
|
||||
return true;
|
||||
|
@ -117,15 +129,16 @@ namespace StardewModdingAPI.Framework.Content
|
|||
|
||||
/// <summary>Overwrite part of the image.</summary>
|
||||
/// <param name="sourceData">The image data to patch into the content.</param>
|
||||
/// <param name="sourceWidth">The pixel width of the source image.</param>
|
||||
/// <param name="sourceHeight">The pixel height of the source image.</param>
|
||||
/// <param name="sourceWidth">The pixel width of the original source image.</param>
|
||||
/// <param name="sourceHeight">The pixel height of the original source image.</param>
|
||||
/// <param name="sourceArea">The part of the <paramref name="sourceData"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="sourceData"/> texture.</param>
|
||||
/// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param>
|
||||
/// <param name="patchMode">Indicates how an image should be patched.</param>
|
||||
/// <param name="startRow">The row to start on, for the sourceData.</param>
|
||||
/// <exception cref="ArgumentNullException">One of the arguments is null.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception>
|
||||
/// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
|
||||
private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode)
|
||||
private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode, int startRow = 0)
|
||||
{
|
||||
// get texture
|
||||
Texture2D target = this.Data;
|
||||
|
@ -139,24 +152,69 @@ namespace StardewModdingAPI.Framework.Content
|
|||
if (sourceArea.Size != targetArea.Size)
|
||||
throw new InvalidOperationException("The source and target areas must be the same size.");
|
||||
|
||||
// merge data
|
||||
if (patchMode == PatchMode.Overlay)
|
||||
// shortcut: replace the entire area
|
||||
if (patchMode == PatchMode.Replace)
|
||||
{
|
||||
target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// skip transparent pixels at the start & end (e.g. large spritesheet with a few sprites replaced)
|
||||
int startIndex = -1;
|
||||
int endIndex = -1;
|
||||
{
|
||||
for (int i = startRow * sourceArea.Width; i < pixelCount; i++)
|
||||
{
|
||||
if (sourceData[i].A >= AssetDataForImage.MinOpacity)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (startIndex == -1)
|
||||
return; // blank texture
|
||||
|
||||
for (int i = startRow * sourceArea.Width + pixelCount - 1; i >= startIndex; i--)
|
||||
{
|
||||
if (sourceData[i].A >= AssetDataForImage.MinOpacity)
|
||||
{
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (endIndex == -1)
|
||||
return; // ???
|
||||
}
|
||||
|
||||
// update target rectangle
|
||||
int sourceOffset;
|
||||
{
|
||||
int topOffset = startIndex / sourceArea.Width;
|
||||
int bottomOffset = endIndex / sourceArea.Width;
|
||||
|
||||
targetArea = new(targetArea.X, targetArea.Y + topOffset, targetArea.Width, bottomOffset - topOffset + 1);
|
||||
pixelCount = targetArea.Width * targetArea.Height;
|
||||
sourceOffset = topOffset * sourceArea.Width;
|
||||
}
|
||||
|
||||
// apply
|
||||
Color[] mergedData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
||||
try
|
||||
{
|
||||
// get target data
|
||||
Color[] mergedData = GC.AllocateUninitializedArray<Color>(pixelCount);
|
||||
target.GetData(0, targetArea, mergedData, 0, pixelCount);
|
||||
|
||||
// merge pixels
|
||||
for (int i = 0; i < pixelCount; i++)
|
||||
for (int i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
int targetIndex = i - sourceOffset;
|
||||
|
||||
Color above = sourceData[i];
|
||||
Color below = mergedData[i];
|
||||
Color below = mergedData[targetIndex];
|
||||
|
||||
// shortcut transparency
|
||||
if (above.A < MinOpacity)
|
||||
if (above.A < AssetDataForImage.MinOpacity)
|
||||
continue;
|
||||
if (below.A < MinOpacity)
|
||||
mergedData[i] = above;
|
||||
if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue)
|
||||
mergedData[targetIndex] = above;
|
||||
|
||||
// merge pixels
|
||||
else
|
||||
|
@ -165,7 +223,7 @@ namespace StardewModdingAPI.Framework.Content
|
|||
// premultiplied by the content pipeline. The formula is derived from
|
||||
// https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/.
|
||||
float alphaBelow = 1 - (above.A / 255f);
|
||||
mergedData[i] = new Color(
|
||||
mergedData[targetIndex] = new Color(
|
||||
r: (int)(above.R + (below.R * alphaBelow)),
|
||||
g: (int)(above.G + (below.G * alphaBelow)),
|
||||
b: (int)(above.B + (below.B * alphaBelow)),
|
||||
|
@ -176,8 +234,10 @@ namespace StardewModdingAPI.Framework.Content
|
|||
|
||||
target.SetData(0, targetArea, mergedData, 0, pixelCount);
|
||||
}
|
||||
else
|
||||
target.SetData(0, targetArea, sourceData, 0, pixelCount);
|
||||
finally
|
||||
{
|
||||
ArrayPool<Color>.Shared.Return(mergedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,6 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>An asset key prefix for assets from SMAPI mod folders.</summary>
|
||||
private readonly string ManagedPrefix = "SMAPI";
|
||||
|
||||
/// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary>
|
||||
private readonly bool UseRawImageLoading;
|
||||
|
||||
/// <summary>Get a file lookup for the given directory.</summary>
|
||||
private readonly Func<string, IFileLookup> GetFileLookup;
|
||||
|
||||
|
@ -139,8 +136,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
|
||||
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
|
||||
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
|
||||
/// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param>
|
||||
public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations, bool useRawImageLoading)
|
||||
public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
|
||||
{
|
||||
this.GetFileLookup = getFileLookup;
|
||||
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
|
||||
|
@ -151,7 +147,6 @@ namespace StardewModdingAPI.Framework
|
|||
this.OnAssetsInvalidated = onAssetsInvalidated;
|
||||
this.RequestAssetOperations = requestAssetOperations;
|
||||
this.FullRootDirectory = Path.Combine(Constants.GamePath, rootDirectory);
|
||||
this.UseRawImageLoading = useRawImageLoading;
|
||||
this.ContentManagers.Add(
|
||||
this.MainContentManager = new GameContentManager(
|
||||
name: "Game1.content",
|
||||
|
@ -230,8 +225,7 @@ namespace StardewModdingAPI.Framework
|
|||
reflection: this.Reflection,
|
||||
jsonHelper: this.JsonHelper,
|
||||
onDisposing: this.OnDisposing,
|
||||
fileLookup: this.GetFileLookup(rootDirectory),
|
||||
useRawImageLoading: this.UseRawImageLoading
|
||||
fileLookup: this.GetFileLookup(rootDirectory)
|
||||
);
|
||||
this.ContentManagers.Add(manager);
|
||||
return manager;
|
||||
|
|
|
@ -336,7 +336,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
{
|
||||
// track asset key
|
||||
if (value is Texture2D texture)
|
||||
texture.Name = assetName.Name;
|
||||
texture.SetName(assetName);
|
||||
|
||||
// save to cache
|
||||
// Note: even if the asset was loaded and cached right before this method was called,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BmFont;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
|
@ -28,9 +30,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary>
|
||||
private readonly bool UseRawImageLoading;
|
||||
|
||||
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
|
||||
private readonly JsonHelper JsonHelper;
|
||||
|
||||
|
@ -72,15 +71,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
|
||||
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
|
||||
/// <param name="fileLookup">A lookup for files within the <paramref name="rootDirectory"/>.</param>
|
||||
/// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param>
|
||||
public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFileLookup fileLookup, bool useRawImageLoading)
|
||||
public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFileLookup fileLookup)
|
||||
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
|
||||
{
|
||||
this.GameContentManager = gameContentManager;
|
||||
this.FileLookup = fileLookup;
|
||||
this.JsonHelper = jsonHelper;
|
||||
this.ModName = modName;
|
||||
this.UseRawImageLoading = useRawImageLoading;
|
||||
|
||||
this.TryLocalizeKeys = false;
|
||||
}
|
||||
|
@ -111,7 +108,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
|
||||
{
|
||||
if (contentManagerID != this.Name)
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager.");
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager.");
|
||||
assetName = relativePath;
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +120,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
// get file
|
||||
FileInfo file = this.GetModFile<T>(assetName.Name);
|
||||
if (!file.Exists)
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist.");
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist.");
|
||||
|
||||
// load content
|
||||
asset = file.Extension.ToLower() switch
|
||||
|
@ -141,7 +138,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
if (ex is SContentLoadException)
|
||||
throw;
|
||||
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex);
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex);
|
||||
return default;
|
||||
}
|
||||
|
||||
// track & return asset
|
||||
|
@ -189,7 +187,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
private T LoadDataFile<T>(IAssetName assetName, FileInfo file)
|
||||
{
|
||||
if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset))
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
@ -201,48 +199,52 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
private T LoadImageFile<T>(IAssetName assetName, FileInfo file)
|
||||
{
|
||||
this.AssertValidType<T>(assetName, file, typeof(Texture2D), typeof(IRawTextureData));
|
||||
bool expectsRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData));
|
||||
bool asRawData = expectsRawData || this.UseRawImageLoading;
|
||||
bool returnRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData));
|
||||
|
||||
#if SMAPI_DEPRECATED
|
||||
// disable raw data if PyTK will rescale the image (until it supports raw data)
|
||||
if (asRawData && !expectsRawData)
|
||||
if (!returnRawData && this.ShouldDisableIntermediateRawDataLoad<T>(assetName, file))
|
||||
{
|
||||
using FileStream stream = File.OpenRead(file.FullName);
|
||||
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream).SetName(assetName);
|
||||
this.PremultiplyTransparency(texture);
|
||||
return (T)(object)texture;
|
||||
}
|
||||
#endif
|
||||
|
||||
IRawTextureData raw = this.LoadRawImageData(file, returnRawData);
|
||||
|
||||
if (returnRawData)
|
||||
return (T)raw;
|
||||
else
|
||||
{
|
||||
Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, raw.Width, raw.Height).SetName(assetName);
|
||||
texture.SetData(raw.Data);
|
||||
return (T)(object)texture;
|
||||
}
|
||||
}
|
||||
|
||||
#if SMAPI_DEPRECATED
|
||||
/// <summary>Get whether to disable loading an image as <see cref="IRawTextureData"/> before building a <see cref="Texture2D"/> instance. This isn't called if the mod requested <see cref="IRawTextureData"/> directly.</summary>
|
||||
/// <typeparam name="T">The type of asset being loaded.</typeparam>
|
||||
/// <param name="assetName">The asset name relative to the loader root directory.</param>
|
||||
/// <param name="file">The file being loaded.</param>
|
||||
private bool ShouldDisableIntermediateRawDataLoad<T>(IAssetName assetName, FileInfo file)
|
||||
{
|
||||
// disable raw data if PyTK will rescale the image (until it supports raw data)
|
||||
if (ModContentManager.EnablePyTkLegacyMode)
|
||||
{
|
||||
// PyTK intercepts Texture2D file loads to rescale them (e.g. for HD portraits),
|
||||
// but doesn't support IRawTextureData loads yet. We can't just check if the
|
||||
// current file has a '.pytk.json' rescale file though, since PyTK may still
|
||||
// rescale it if the original asset or another edit gets rescaled.
|
||||
asRawData = false;
|
||||
this.Monitor.LogOnce("Enabled compatibility mode for PyTK 1.23.* or earlier. This won't cause any issues, but may impact performance. This will no longer be supported in the upcoming SMAPI 4.0.0.", LogLevel.Warn);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// load
|
||||
if (asRawData)
|
||||
{
|
||||
IRawTextureData raw = this.LoadRawImageData(file, expectsRawData);
|
||||
|
||||
if (expectsRawData)
|
||||
return (T)raw;
|
||||
else
|
||||
{
|
||||
Texture2D texture = new(Game1.graphics.GraphicsDevice, raw.Width, raw.Height);
|
||||
texture.SetData(raw.Data);
|
||||
return (T)(object)texture;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using FileStream stream = File.OpenRead(file.FullName);
|
||||
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
|
||||
texture = this.PremultiplyTransparency(texture);
|
||||
return (T)(object)texture;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Load the raw image data from a file on disk.</summary>
|
||||
/// <param name="file">The file whose data to load.</param>
|
||||
/// <param name="forRawData">Whether the data is being loaded for an <see cref="IRawTextureData"/> (true) or <see cref="Texture2D"/> (false) instance.</param>
|
||||
|
@ -301,7 +303,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
private T LoadXnbFile<T>(IAssetName assetName)
|
||||
{
|
||||
if (typeof(IRawTextureData).IsAssignableFrom(typeof(T)))
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file.");
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file.");
|
||||
|
||||
// the underlying content manager adds a .xnb extension implicitly, so
|
||||
// we need to strip it here to avoid trying to load a '.xnb.xnb' file.
|
||||
|
@ -326,7 +328,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="file">The file to load.</param>
|
||||
private T HandleUnknownFileType<T>(IAssetName assetName, FileInfo file)
|
||||
{
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>Assert that the asset type is compatible with one of the allowed types.</summary>
|
||||
|
@ -338,18 +341,21 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
private void AssertValidType<TAsset>(IAssetName assetName, FileInfo file, params Type[] validTypes)
|
||||
{
|
||||
if (!validTypes.Any(validType => validType.IsAssignableFrom(typeof(TAsset))))
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TAsset)}'; must be type '{string.Join("' or '", validTypes.Select(p => p.FullName))}'.");
|
||||
this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TAsset)}'; must be type '{string.Join("' or '", validTypes.Select(p => p.FullName))}'.");
|
||||
}
|
||||
|
||||
/// <summary>Get an error which indicates that an asset couldn't be loaded.</summary>
|
||||
/// <summary>Throw an error which indicates that an asset couldn't be loaded.</summary>
|
||||
/// <param name="errorType">Why loading an asset through the content pipeline failed.</param>
|
||||
/// <param name="assetName">The asset name that failed to load.</param>
|
||||
/// <param name="reasonPhrase">The reason the file couldn't be loaded.</param>
|
||||
/// <param name="exception">The underlying exception, if applicable.</param>
|
||||
/// <exception cref="SContentLoadException" />
|
||||
[DoesNotReturn]
|
||||
[DebuggerStepThrough, DebuggerHidden]
|
||||
private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
|
||||
{
|
||||
return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
|
||||
throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
|
||||
}
|
||||
|
||||
/// <summary>Get a file from the mod folder.</summary>
|
||||
|
@ -381,15 +387,18 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="texture">The texture to premultiply.</param>
|
||||
/// <returns>Returns a premultiplied texture.</returns>
|
||||
/// <remarks>Based on <a href="https://gamedev.stackexchange.com/a/26037">code by David Gouveia</a>.</remarks>
|
||||
private Texture2D PremultiplyTransparency(Texture2D texture)
|
||||
private void PremultiplyTransparency(Texture2D texture)
|
||||
{
|
||||
// premultiply pixels
|
||||
Color[] data = GC.AllocateUninitializedArray<Color>(texture.Width * texture.Height);
|
||||
texture.GetData(data);
|
||||
int count = texture.Width * texture.Height;
|
||||
Color[] data = ArrayPool<Color>.Shared.Rent(count);
|
||||
try
|
||||
{
|
||||
texture.GetData(data, 0, count);
|
||||
|
||||
bool changed = false;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Color pixel = data[i];
|
||||
ref Color pixel = ref data[i];
|
||||
if (pixel.A is (byte.MinValue or byte.MaxValue))
|
||||
continue; // no need to change fully transparent/opaque pixels
|
||||
|
||||
|
@ -398,9 +407,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
}
|
||||
|
||||
if (changed)
|
||||
texture.SetData(data);
|
||||
|
||||
return texture;
|
||||
texture.SetData(data, 0, count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<Color>.Shared.Return(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
|
||||
|
|
|
@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework.Deprecations
|
|||
foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase))
|
||||
{
|
||||
// build message
|
||||
string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version}).";
|
||||
string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase}) and will break in the upcoming SMAPI 4.0.0.";
|
||||
|
||||
// get log level
|
||||
LogLevel level;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -163,5 +164,34 @@ namespace StardewModdingAPI.Framework
|
|||
{
|
||||
return reflection.GetField<bool>(spriteBatch, "_beginCalled").GetValue();
|
||||
}
|
||||
|
||||
/****
|
||||
** Texture2D
|
||||
****/
|
||||
/// <summary>Set the texture name field.</summary>
|
||||
/// <param name="texture">The texture whose name to set.</param>
|
||||
/// <param name="assetName">The asset name to set.</param>
|
||||
/// <returns>Returns the texture for chaining.</returns>
|
||||
[return: NotNullIfNotNull("texture")]
|
||||
public static Texture2D? SetName(this Texture2D? texture, IAssetName assetName)
|
||||
{
|
||||
if (texture != null)
|
||||
texture.Name = assetName.Name;
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
/// <summary>Set the texture name field.</summary>
|
||||
/// <param name="texture">The texture whose name to set.</param>
|
||||
/// <param name="assetName">The asset name to set.</param>
|
||||
/// <returns>Returns the texture for chaining.</returns>
|
||||
[return: NotNullIfNotNull("texture")]
|
||||
public static Texture2D? SetName(this Texture2D? texture, string assetName)
|
||||
{
|
||||
if (texture != null)
|
||||
texture.Name = assetName;
|
||||
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -223,7 +223,7 @@ namespace StardewModdingAPI.Framework.Logging
|
|||
// show update alert
|
||||
if (File.Exists(Constants.UpdateMarker))
|
||||
{
|
||||
string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split(new[] { '|' }, 2);
|
||||
string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split('|', 2);
|
||||
if (SemanticVersion.TryParse(rawUpdateFound[0], out ISemanticVersion? updateFound))
|
||||
{
|
||||
if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion))
|
||||
|
@ -269,7 +269,11 @@ namespace StardewModdingAPI.Framework.Logging
|
|||
public void LogIntro(string modsPath, IDictionary<string, object?> customSettings)
|
||||
{
|
||||
// log platform
|
||||
this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
|
||||
this.Monitor.Log($"SMAPI {Constants.ApiVersion} "
|
||||
#if !SMAPI_DEPRECATED
|
||||
+ "(strict mode) "
|
||||
#endif
|
||||
+ $"with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
|
||||
|
||||
// log basic info
|
||||
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
|
||||
|
@ -280,6 +284,10 @@ namespace StardewModdingAPI.Framework.Logging
|
|||
// log custom settings
|
||||
if (customSettings.Any())
|
||||
this.Monitor.Log($"Loaded with custom settings: {string.Join(", ", customSettings.OrderBy(p => p.Key).Select(p => $"{p.Key}: {p.Value}"))}");
|
||||
|
||||
#if !SMAPI_DEPRECATED
|
||||
this.Monitor.Log("SMAPI is running in 'strict mode', which removes all deprecated APIs. This can significantly improve performance, but some mods may not work. You can reinstall SMAPI to disable it if you run into problems.", LogLevel.Info);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Log details for settings that don't match the default.</summary>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Logging
|
||||
{
|
||||
/// <summary>The cache key for the <see cref="Monitor.LogOnceCache"/>.</summary>
|
||||
/// <param name="Message">The log message.</param>
|
||||
/// <param name="Level">The log level.</param>
|
||||
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local", Justification = "This is only used as a lookup key.")]
|
||||
internal readonly record struct LogOnceCacheKey(string Message, LogLevel Level);
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StardewModdingAPI.Framework.Reflection;
|
||||
using StardewModdingAPI.Internal;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModHelpers
|
||||
{
|
||||
|
@ -15,8 +17,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <summary>Encapsulates monitoring and logging for the mod.</summary>
|
||||
private readonly IMonitor Monitor;
|
||||
|
||||
/// <summary>The mod IDs for APIs accessed by this instanced.</summary>
|
||||
private readonly HashSet<string> AccessedModApis = new();
|
||||
/// <summary>The APIs accessed by this instance.</summary>
|
||||
private readonly Dictionary<string, object?> AccessedModApis = new();
|
||||
|
||||
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
|
||||
private readonly IInterfaceProxyFactory ProxyFactory;
|
||||
|
@ -66,11 +68,44 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
return null;
|
||||
}
|
||||
|
||||
// get raw API
|
||||
// get the target mod
|
||||
IModMetadata? mod = this.Registry.Get(uniqueID);
|
||||
if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID))
|
||||
this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.");
|
||||
return mod?.Api;
|
||||
if (mod == null)
|
||||
return null;
|
||||
|
||||
// fetch API
|
||||
if (!this.AccessedModApis.TryGetValue(mod.Manifest.UniqueID, out object? api))
|
||||
{
|
||||
// if the target has a global API, this is mutually exclusive with per-mod APIs
|
||||
if (mod.Api != null)
|
||||
api = mod.Api;
|
||||
|
||||
// else try to get a per-mod API
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
api = mod.Mod?.GetApi(this.Mod);
|
||||
if (api != null && !api.GetType().IsPublic)
|
||||
{
|
||||
api = null;
|
||||
this.Monitor.Log($"{mod.DisplayName} provides a per-mod API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"Failed loading the per-mod API instance from {mod.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
|
||||
api = null;
|
||||
}
|
||||
}
|
||||
|
||||
// cache & log API access
|
||||
this.AccessedModApis[mod.Manifest.UniqueID] = api;
|
||||
if (api != null)
|
||||
this.Monitor.Log($"Accessed mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}.");
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -221,7 +221,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
/// </remarks>
|
||||
public static Assembly? ResolveAssembly(string name)
|
||||
{
|
||||
string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture)
|
||||
string shortName = name.Split(',', 2).First(); // get simple name (without version and culture)
|
||||
return AppDomain.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.FirstOrDefault(p => p.GetName().Name == shortName);
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
|||
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];
|
||||
string eventName = methodRef.Name.Split('_', 2)[1];
|
||||
this.MethodNames.Remove($"add_{eventName}");
|
||||
this.MethodNames.Remove($"remove_{eventName}");
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@ namespace StardewModdingAPI.Framework.Models
|
|||
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
|
||||
[nameof(LogNetworkTraffic)] = false,
|
||||
[nameof(RewriteMods)] = true,
|
||||
[nameof(UseRawImageLoading)] = true,
|
||||
[nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux
|
||||
[nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux,
|
||||
[nameof(SuppressHarmonyDebugMode)] = true
|
||||
};
|
||||
|
||||
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
|
||||
|
@ -67,9 +67,6 @@ namespace StardewModdingAPI.Framework.Models
|
|||
/// <summary>Whether SMAPI should rewrite mods for compatibility.</summary>
|
||||
public bool RewriteMods { get; set; }
|
||||
|
||||
/// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary>
|
||||
public bool UseRawImageLoading { get; set; }
|
||||
|
||||
/// <summary>Whether to make SMAPI file APIs case-insensitive, even on Linux.</summary>
|
||||
public bool UseCaseInsensitivePaths { get; set; }
|
||||
|
||||
|
@ -79,6 +76,9 @@ namespace StardewModdingAPI.Framework.Models
|
|||
/// <summary>The colors to use for text written to the SMAPI console.</summary>
|
||||
public ColorSchemeConfig ConsoleColors { get; set; }
|
||||
|
||||
/// <summary>Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod.</summary>
|
||||
public bool SuppressHarmonyDebugMode { get; set; }
|
||||
|
||||
/// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary>
|
||||
public HashSet<string> SuppressUpdateChecks { get; set; }
|
||||
|
||||
|
@ -95,12 +95,12 @@ namespace StardewModdingAPI.Framework.Models
|
|||
/// <param name="webApiBaseUrl">The base URL for SMAPI's web API, used to perform update checks.</param>
|
||||
/// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param>
|
||||
/// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param>
|
||||
/// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param>
|
||||
/// <param name="useCaseInsensitivePaths">>Whether to make SMAPI file APIs case-insensitive, even on Linux.</param>
|
||||
/// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param>
|
||||
/// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param>
|
||||
/// <param name="suppressHarmonyDebugMode">Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and creates a file on your desktop. Debug mode should never be enabled by a released mod.</param>
|
||||
/// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param>
|
||||
public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
|
||||
public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks)
|
||||
{
|
||||
this.DeveloperMode = developerMode;
|
||||
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
|
||||
|
@ -110,10 +110,10 @@ namespace StardewModdingAPI.Framework.Models
|
|||
this.WebApiBaseUrl = webApiBaseUrl;
|
||||
this.VerboseLogging = new HashSet<string>(verboseLogging ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)];
|
||||
this.UseRawImageLoading = useRawImageLoading ?? (bool)SConfig.DefaultValues[nameof(this.UseRawImageLoading)];
|
||||
this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(this.UseCaseInsensitivePaths)];
|
||||
this.LogNetworkTraffic = logNetworkTraffic ?? (bool)SConfig.DefaultValues[nameof(this.LogNetworkTraffic)];
|
||||
this.ConsoleColors = consoleColors;
|
||||
this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)];
|
||||
this.SuppressUpdateChecks = new HashSet<string>(suppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,10 +25,13 @@ namespace StardewModdingAPI.Framework
|
|||
private readonly LogFileManager LogFile;
|
||||
|
||||
/// <summary>The maximum length of the <see cref="LogLevel"/> values.</summary>
|
||||
private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max();
|
||||
private static readonly int MaxLevelLength = Enum.GetValues<LogLevel>().Max(level => level.ToString().Length);
|
||||
|
||||
/// <summary>The cached representation for each level when added to a log header.</summary>
|
||||
private static readonly Dictionary<ConsoleLogLevel, string> LogStrings = Enum.GetValues<ConsoleLogLevel>().ToDictionary(level => level, level => level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength));
|
||||
|
||||
/// <summary>A cache of messages that should only be logged once.</summary>
|
||||
private readonly HashSet<string> LogOnceCache = new();
|
||||
private readonly HashSet<LogOnceCacheKey> LogOnceCache = new();
|
||||
|
||||
/// <summary>Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</summary>
|
||||
private readonly Func<int?> GetScreenIdForLog;
|
||||
|
@ -84,7 +87,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <inheritdoc />
|
||||
public void LogOnce(string message, LogLevel level = LogLevel.Trace)
|
||||
{
|
||||
if (this.LogOnceCache.Add($"{message}|{level}"))
|
||||
if (this.LogOnceCache.Add(new LogOnceCacheKey(message, level)))
|
||||
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
|
||||
}
|
||||
|
||||
|
@ -147,7 +150,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="level">The log level.</param>
|
||||
private string GenerateMessagePrefix(string source, ConsoleLogLevel level)
|
||||
{
|
||||
string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength);
|
||||
string levelStr = Monitor.LogStrings[level];
|
||||
int? playerIndex = this.GetScreenIdForLog();
|
||||
|
||||
return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]";
|
||||
|
|
|
@ -501,6 +501,15 @@ namespace StardewModdingAPI.Framework
|
|||
return;
|
||||
}
|
||||
|
||||
/*********
|
||||
** Prevent Harmony debug mode
|
||||
*********/
|
||||
if (HarmonyLib.Harmony.DEBUG && this.Settings.SuppressHarmonyDebugMode)
|
||||
{
|
||||
HarmonyLib.Harmony.DEBUG = false;
|
||||
this.Monitor.LogOnce("A mod enabled Harmony debug mode, which impacts performance and creates a file on your desktop. SMAPI will try to keep it disabled. (You can allow debug mode by editing the smapi-internal/config.json file.)", LogLevel.Warn);
|
||||
}
|
||||
|
||||
#if SMAPI_DEPRECATED
|
||||
/*********
|
||||
** Reload assets when interceptors are added/removed
|
||||
|
@ -1324,8 +1333,7 @@ namespace StardewModdingAPI.Framework
|
|||
onAssetLoaded: this.OnAssetLoaded,
|
||||
onAssetsInvalidated: this.OnAssetsInvalidated,
|
||||
getFileLookup: this.GetFileLookup,
|
||||
requestAssetOperations: this.RequestAssetOperations,
|
||||
useRawImageLoading: this.Settings.UseRawImageLoading
|
||||
requestAssetOperations: this.RequestAssetOperations
|
||||
);
|
||||
if (this.ContentCore.Language != this.Translator.LocaleEnum)
|
||||
this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language);
|
||||
|
@ -1384,7 +1392,7 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
|
||||
// check min length for specific types
|
||||
switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0])
|
||||
switch (fields[SObject.objectInfoTypeIndex].Split(' ', 2)[0])
|
||||
{
|
||||
case "Cooking":
|
||||
if (fields.Length < SObject.objectInfoBuffDurationIndex + 1)
|
||||
|
@ -1672,26 +1680,33 @@ namespace StardewModdingAPI.Framework
|
|||
// initialize translations
|
||||
this.ReloadTranslations(loaded);
|
||||
|
||||
#if SMAPI_DEPRECATED
|
||||
// set temporary PyTK compatibility mode
|
||||
// This is part of a three-part fix for PyTK 1.23.* and earlier. When removing this,
|
||||
// search 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher.
|
||||
{
|
||||
IModInfo? pyTk = this.ModRegistry.Get("Platonymous.Toolkit");
|
||||
ModContentManager.EnablePyTkLegacyMode = pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.24.0");
|
||||
}
|
||||
if (pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.24.0"))
|
||||
#if SMAPI_DEPRECATED
|
||||
ModContentManager.EnablePyTkLegacyMode = true;
|
||||
#else
|
||||
this.Monitor.Log("PyTK's image scaling is not compatible with SMAPI strict mode.", LogLevel.Warn);
|
||||
#endif
|
||||
}
|
||||
|
||||
// initialize loaded non-content-pack mods
|
||||
this.Monitor.Log("Launching mods...", LogLevel.Debug);
|
||||
foreach (IModMetadata metadata in loadedMods)
|
||||
{
|
||||
IMod mod =
|
||||
metadata.Mod
|
||||
?? throw new InvalidOperationException($"The '{metadata.DisplayName}' mod is not initialized correctly."); // should never happen, but avoids nullability warnings
|
||||
|
||||
#if SMAPI_DEPRECATED
|
||||
// add interceptors
|
||||
if (metadata.Mod?.Helper is ModHelper helper)
|
||||
if (mod.Helper is ModHelper helper)
|
||||
{
|
||||
// ReSharper disable SuspiciousTypeConversion.Global
|
||||
if (metadata.Mod is IAssetEditor editor)
|
||||
if (mod is IAssetEditor editor)
|
||||
{
|
||||
SCore.DeprecationManager.Warn(
|
||||
source: metadata,
|
||||
|
@ -1704,7 +1719,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.ContentCore.Editors.Add(new ModLinked<IAssetEditor>(metadata, editor));
|
||||
}
|
||||
|
||||
if (metadata.Mod is IAssetLoader loader)
|
||||
if (mod is IAssetLoader loader)
|
||||
{
|
||||
SCore.DeprecationManager.Warn(
|
||||
source: metadata,
|
||||
|
@ -1749,11 +1764,12 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
#endif
|
||||
|
||||
// call entry method
|
||||
// initialize mod
|
||||
Context.HeuristicModsRunningCode.Push(metadata);
|
||||
{
|
||||
// call entry method
|
||||
try
|
||||
{
|
||||
IMod mod = metadata.Mod!;
|
||||
mod.Entry(mod.Helper!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -1764,7 +1780,7 @@ namespace StardewModdingAPI.Framework
|
|||
// get mod API
|
||||
try
|
||||
{
|
||||
object? api = metadata.Mod!.GetApi();
|
||||
object? api = mod.GetApi();
|
||||
if (api != null && !api.GetType().IsPublic)
|
||||
{
|
||||
api = null;
|
||||
|
@ -1779,6 +1795,11 @@ namespace StardewModdingAPI.Framework
|
|||
{
|
||||
this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// validate mod doesn't implement both GetApi() and GetApi(mod)
|
||||
if (metadata.Api != null && mod.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IModInfo) })!.DeclaringType != typeof(Mod))
|
||||
metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IModInfo)}), which isn't allowed. The latter will be ignored.", LogLevel.Error);
|
||||
}
|
||||
Context.HeuristicModsRunningCode.TryPop(out _);
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
|
|||
{
|
||||
if (this.LastValues.Count > 0)
|
||||
{
|
||||
this.AddedImpl.AddRange(this.LastValues);
|
||||
this.RemovedImpl.AddRange(this.LastValues);
|
||||
this.LastValues.Clear();
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -23,7 +23,15 @@ namespace StardewModdingAPI
|
|||
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||
void Entry(IModHelper helper);
|
||||
|
||||
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
|
||||
/// <summary>Get an <a href="https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations">API that other mods can access</a>. This is always called after <see cref="Entry"/>, and is only called once even if multiple mods access it.</summary>
|
||||
/// <remarks>You can implement <see cref="GetApi()"/> to provide one instance to all mods, or <see cref="GetApi(IModInfo)"/> to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them.</remarks>
|
||||
/// <remarks>Returns the API instance, or <c>null</c> if the mod has no API.</remarks>
|
||||
object? GetApi();
|
||||
|
||||
/// <summary>Get an <a href="https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations">API that other mods can access</a>. This is always called after <see cref="Entry"/>, and is called once per mod that accesses the API (even if they access it multiple times).</summary>
|
||||
/// <param name="mod">The mod accessing the API.</param>
|
||||
/// <remarks>Returns the API instance, or <c>null</c> if the mod has no API. Note that <paramref name="mod"/> is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive.</remarks>
|
||||
/// <inheritdoc cref="GetApi()" include="/Remarks" />
|
||||
object? GetApi(IModInfo mod);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,12 @@ namespace StardewModdingAPI
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object? GetApi(IModInfo mod)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Release or reset unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
@ -54,18 +54,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future
|
|||
*/
|
||||
"UseCaseInsensitivePaths": null,
|
||||
|
||||
/**
|
||||
* Whether to use the experimental Pintail API proxying library, instead of the original
|
||||
* proxying built into SMAPI itself.
|
||||
*/
|
||||
"UsePintail": true,
|
||||
|
||||
/**
|
||||
* Whether to use raw image data when possible, instead of initializing an XNA Texture2D
|
||||
* instance through the GPU.
|
||||
*/
|
||||
"UseRawImageLoading": true,
|
||||
|
||||
/**
|
||||
* Whether to add a section to the 'mod issues' list for mods which directly use potentially
|
||||
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as
|
||||
|
@ -138,6 +126,14 @@ copy all the settings, or you may cause bugs due to overridden changes in future
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to prevent mods from enabling Harmony's debug mode, which impacts performance and
|
||||
* creates a file on your desktop. Debug mode should never be enabled by a released mod.
|
||||
*
|
||||
* If you actually need debug mode to test your own mod, set this to false.
|
||||
*/
|
||||
"SuppressHarmonyDebugMode": true,
|
||||
|
||||
/**
|
||||
* The mod IDs SMAPI should ignore when performing update checks or validating update keys.
|
||||
*/
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
|
||||
<PackageReference Include="MonoMod.Common" Version="22.3.5.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
|
||||
<PackageReference Include="Pintail" Version="2.2.1" />
|
||||
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
||||
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
||||
|
|
|
@ -54,12 +54,12 @@ namespace StardewModdingAPI.Utilities
|
|||
}
|
||||
|
||||
// parse buttons
|
||||
string[] rawButtons = input.Split('+');
|
||||
string[] rawButtons = input.Split('+', StringSplitOptions.TrimEntries);
|
||||
SButton[] buttons = new SButton[rawButtons.Length];
|
||||
List<string> rawErrors = new List<string>();
|
||||
for (int i = 0; i < buttons.Length; i++)
|
||||
{
|
||||
string rawButton = rawButtons[i].Trim();
|
||||
string rawButton = rawButtons[i];
|
||||
if (string.IsNullOrWhiteSpace(rawButton))
|
||||
rawErrors.Add("Invalid empty button value");
|
||||
else if (!Enum.TryParse(rawButton, ignoreCase: true, out SButton button))
|
||||
|
|
Loading…
Reference in New Issue