Merge remote-tracking branch 'pathoschild/stable' into develop

# Conflicts:
#	.gitignore
#	build/common.targets
#	src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj
#	src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj
#	src/SMAPI.Tests/SMAPI.Tests.csproj
#	src/SMAPI.Toolkit/ModToolkit.cs
#	src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
#	src/SMAPI.sln
#	src/SMAPI/Constants.cs
#	src/SMAPI/Framework/ContentManagers/ModContentManager.cs
#	src/SMAPI/Framework/Input/GamePadStateBuilder.cs
#	src/SMAPI/Framework/Logging/LogManager.cs
#	src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
#	src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
#	src/SMAPI/Framework/Models/SConfig.cs
#	src/SMAPI/Framework/Patching/GamePatcher.cs
#	src/SMAPI/Framework/Reflection/Reflector.cs
#	src/SMAPI/Framework/SCore.cs
#	src/SMAPI/Framework/SGame.cs
#	src/SMAPI/Framework/SMultiplayer.cs
#	src/SMAPI/Framework/StateTracking/LocationTracker.cs
#	src/SMAPI/Metadata/CoreAssetPropagator.cs
#	src/SMAPI/Metadata/InstructionMetadata.cs
#	src/SMAPI/SMAPI.csproj
This commit is contained in:
zhiyang7 2023-01-16 17:28:55 +08:00
commit 3e43d69745
589 changed files with 27032 additions and 10401 deletions

View File

@ -22,6 +22,9 @@ insert_final_newline = false
[README.txt] [README.txt]
end_of_line=crlf end_of_line=crlf
[*.{command,sh}]
end_of_line=lf
########## ##########
## C# formatting ## C# formatting
## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference ## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference

5
.gitattributes vendored
View File

@ -1,3 +1,6 @@
# normalize line endings # normalize line endings
* text=auto * text=auto
README.txt text=crlf README.txt text eol=crlf
*.command text eol=lf
*.sh text eol=lf

5
.gitignore vendored
View File

@ -11,6 +11,7 @@
[Oo]bj/ [Oo]bj/
# Visual Studio cache/options # Visual Studio cache/options
.config/
.vs/ .vs/
# ReSharper # ReSharper
@ -32,6 +33,10 @@ appsettings.Development.json
# Azure generated files # Azure generated files
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
src/SMAPI.Web/Properties/ServiceDependencies/* - Web Deploy/
# macOS
.DS_Store
# Loader Game Asserts # Loader Game Asserts
src/Loader/Assets/Content/ src/Loader/Assets/Content/

Binary file not shown.

3693
build/0Harmony.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,44 @@
<!--
This MSBuild file sets the common configuration and build scripts used by all the projects in this
repo. It imports the other MSBuild files as needed.
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="find-game-folder.targets" />
<!--set properties -->
<PropertyGroup> <PropertyGroup>
<Version>3.7.6</Version> <!--set general build properties -->
<Version>3.18.2</Version>
<Product>SMAPI</Product> <Product>SMAPI</Product>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> <AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
<DefineConstants Condition="$(OS) == 'Windows_NT' AND '$(BUILD_FOR_MOBILE)' == ''">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants> <DefineConstants Condition="$(OS) == 'Windows_NT' AND '$(BUILD_FOR_MOBILE)' == ''">$(DefineConstants);SMAPI_DEPRECATED;SMAPI_FOR_WINDOWS</DefineConstants>
<DebugSymbols>true</DebugSymbols>
<!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported-->
<Nullable Condition="'$(TargetFramework)' != 'netstandard2.0'">enable</Nullable>
<NoWarn Condition="'$(TargetFramework)' == 'netstandard2.0'">$(NoWarn);CS8632</NoWarn>
<!--set platform-->
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
<CopyToGameFolder>true</CopyToGameFolder>
<!-- allow mods to be compiled as AnyCPU for compatibility with older platforms -->
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<!--
suppress warnings that don't apply, so it's easier to spot actual issues.
warning | builds | summary | rationale
┄┄┄┄┄┄┄ | ┄┄┄┄┄┄┄┄┄┄ | ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ | ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
CS0436 | all | local type conflicts with imported type | SMAPI needs to use certain low-level code during very early compatibility checks, before it's safe to load any other DLLs.
CS0612 | deprecated | member is obsolete | internal references to deprecated code when deprecated code is enabled.
CS0618 | deprecated | member is obsolete (with message) | internal references to deprecated code when deprecated code is enabled.
CA1416 | all | platform code available on all platforms | Compiler doesn't recognize the #if constants used by SMAPI.
CS0809 | all | obsolete overload for non-obsolete member | This is deliberate to signal to mods that certain APIs are only implemented for the game and shouldn't be called by mods.
NU1701 | all | NuGet package targets older .NET version | All such packages are carefully tested to make sure they do work.
-->
<NoWarn Condition="$(DefineConstants.Contains(SMAPI_DEPRECATED))">$(NoWarn);CS0612;CS0618</NoWarn>
<NoWarn>$(NoWarn);CS0436;CA1416;CS0809;NU1701</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="$(COMPILE_WITH_PLUGIN) == 'True'"> <ItemGroup Condition="$(COMPILE_WITH_PLUGIN) == 'True'">
@ -19,52 +48,20 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors --> <!--find game folder-->
<Import Project="find-game-folder.targets" />
<Target Name="ValidateInstallPath" AfterTargets="BeforeBuild"> <Target Name="ValidateInstallPath" AfterTargets="BeforeBuild">
<!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors -->
<Error Condition="!Exists('$(GamePath)')" Text="Failed to find the game install path automatically. You can specify where to find it; see https://smapi.io/package/custom-game-path." /> <Error Condition="!Exists('$(GamePath)')" Text="Failed to find the game install path automatically. You can specify where to find it; see https://smapi.io/package/custom-game-path." />
</Target> </Target>
<!-- copy files into game directory and enable debugging -->
<Target Name="CopySmapiFiles" AfterTargets="AfterBuild" Condition="!$(DefineConstants.Contains('SMAPI_FOR_MOBILE'))">
<CallTarget Targets="CopySMAPI;CopyDefaultMods" />
</Target>
<Target Name="CopySMAPI" Condition="'$(MSBuildProjectName)' == 'SMAPI'">
<ItemGroup>
<TranslationFiles Include="$(TargetDir)\i18n\*.json" />
</ItemGroup>
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\SMAPI.config.json" DestinationFiles="$(GamePath)\smapi-internal\config.json" />
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
<Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
</Target>
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" />
<Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
</Target>
<Target Name="CopyToolkit" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit.CoreInterfaces' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
<!-- common build settings --> <!-- common build settings -->
<PropertyGroup> <PropertyGroup>
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<!--deploy local files-->
<Import Project="deploy-local-smapi.targets" Condition="'$(CopyToGameFolder)' == 'true'" />
<!-- launch SMAPI through Visual Studio --> <!-- launch SMAPI through Visual Studio -->
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'SMAPI'"> <PropertyGroup Condition="'$(MSBuildProjectName)' == 'SMAPI'">
@ -73,7 +70,6 @@
<StartWorkingDirectory>$(GamePath)</StartWorkingDirectory> <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory>
</PropertyGroup> </PropertyGroup>
<!-- Somehow this makes Visual Studio for Mac recognise the previous section. Nobody knows why. --> <!-- Somehow this makes Visual Studio for macOS recognise the previous section. Nobody knows why. -->
<PropertyGroup Condition="'$(RunConfiguration)' == 'Default'" /> <PropertyGroup Condition="'$(RunConfiguration)' == 'Default'" />
</Project> </Project>

View File

@ -0,0 +1,81 @@
<!--
This MSBuild file copies SMAPI and the bundled mods into the local Stardew Valley folder on build
to simplify testing. This just avoids needing to run the SMAPI installer each time.
This assumes `find-game-folder.targets` has already been imported and validated.
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CopySmapiFiles" AfterTargets="AfterBuild">
<CallTarget Targets="CopySMAPI;CopyDefaultMods" />
</Target>
<Target Name="CopySMAPI" Condition="'$(MSBuildProjectName)' == 'SMAPI'">
<!-- SMAPI -->
<ItemGroup>
<TranslationFiles Include="$(TargetDir)\i18n\*.json" />
</ItemGroup>
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName)" DestinationFolder="$(GamePath)" Condition="$(OS) != 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\SMAPI.config.json" DestinationFiles="$(GamePath)\smapi-internal\config.json" />
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Pintail.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
<!-- Harmony + dependencies -->
<Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\0Harmony.xml" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.Mdb.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.Pdb.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<!-- FluentHttpClient + dependencies -->
<Copy SourceFiles="$(TargetDir)\Pathoschild.Http.Client.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\System.Net.Http.Formatting.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<!-- .NET dependencies -->
<Copy SourceFiles="$(TargetDir)\System.Management.dll" DestinationFolder="$(GamePath)\smapi-internal" Condition="$(OS) == 'Windows_NT'" />
<!-- Legacy .NET dependencies (remove in SMAPI 4.0.0) -->
<Copy SourceFiles="$(TargetDir)\System.Configuration.ConfigurationManager.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\System.Runtime.Caching.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\System.Security.Permissions.dll" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
<!-- .NET metadata files -->
<Target Name="CopyNetMetadata" Condition="'$(MSBuildProjectName)' == 'SMAPI.Installer'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\assets\runtimeconfig.json" DestinationFiles="$(GamePath)\StardewModdingAPI.runtimeconfig.json" />
<Copy SourceFiles="$(TargetDir)\assets\windows-exe-config.xml" DestinationFiles="$(GamePath)\StardewModdingAPI.exe.config" Condition="$(OS) == 'Windows_NT'" />
<Copy SourceFiles="$(GamePath)\Stardew Valley.deps.json" DestinationFiles="$(GamePath)\StardewModdingAPI.deps.json" Condition="!Exists('$(GamePath)\StardewModdingAPI.deps.json')" />
</Target>
<!-- bundled mods -->
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.ErrorHandler' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'">
<ItemGroup>
<TranslationFiles Include="$(TargetDir)\i18n\*.json" />
</ItemGroup>
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" />
<Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)\i18n" />
</Target>
<!-- toolkit -->
<Target Name="CopyToolkit" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit.CoreInterfaces'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
</Project>

View File

@ -1,3 +1,9 @@
<!--
This MSBuild file detects the Stardew Valley install path if possible, and sets the 'GamePath'
property.
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- import developer's custom path (if any) --> <!-- import developer's custom path (if any) -->
<Import Condition="$(OS) != 'Windows_NT' AND Exists('$(HOME)\stardewvalley.targets')" Project="$(HOME)\stardewvalley.targets" /> <Import Condition="$(OS) != 'Windows_NT' AND Exists('$(HOME)\stardewvalley.targets')" Project="$(HOME)\stardewvalley.targets" />
@ -11,23 +17,15 @@
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/GOG Games/Stardew Valley/game</GamePath> <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/GOG Games/Stardew Valley/game</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.steam/steam/steamapps/common/Stardew Valley</GamePath> <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.steam/steam/steamapps/common/Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.local/share/Steam/steamapps/common/Stardew Valley</GamePath> <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.local/share/Steam/steamapps/common/Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.var/app/com.valvesoftware.Steam/data/Steam/steamapps/common/Stardew Valley</GamePath>
<!-- Mac (may be 'Unix' or 'OSX') --> <!-- macOS (may be 'Unix' or 'OSX') -->
<GamePath Condition="!Exists('$(GamePath)')">/Applications/Stardew Valley.app/Contents/MacOS</GamePath> <GamePath Condition="!Exists('$(GamePath)')">/Applications/Stardew Valley.app/Contents/MacOS</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS</GamePath> <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS</GamePath>
</PropertyGroup> </PropertyGroup>
</When> </When>
<When Condition="$(OS) == 'Windows_NT'"> <When Condition="$(OS) == 'Windows_NT'">
<PropertyGroup> <PropertyGroup>
<!-- default paths -->
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Galaxy\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath>
<!-- registry paths --> <!-- registry paths -->
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath> <GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath> <GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath>
@ -35,13 +33,34 @@
<!-- derive from Steam library path --> <!-- derive from Steam library path -->
<_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32))</_SteamLibraryPath> <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32))</_SteamLibraryPath>
<GamePath Condition="!Exists('$(GamePath)') AND '$(_SteamLibraryPath)' != ''">$(_SteamLibraryPath)\steamapps\common\Stardew Valley</GamePath> <GamePath Condition="!Exists('$(GamePath)') AND '$(_SteamLibraryPath)' != ''">$(_SteamLibraryPath)\steamapps\common\Stardew Valley</GamePath>
<!-- GOG paths -->
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Galaxy\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Games\Stardew Valley</GamePath>
<!-- Xbox app paths -->
<!--
The Xbox app saves the install path to the registry, but we can't use it here since it
saves the internal readonly path (like C:\Program Files\WindowsApps\Mutable\<package ID>)
instead of the mods-enabled path (like C:\Program Files\ModifiableWindowsApps\Stardew Valley).
Fortunately we can cheat a bit: players can customize the install drive, but they can't
change the install path on the drive.
-->
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">D:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">E:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">F:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">G:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">H:\Program Files\ModifiableWindowsApps\Stardew Valley</GamePath>
<!-- Steam paths -->
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath>
</PropertyGroup> </PropertyGroup>
</When> </When>
</Choose> </Choose>
<!-- set game metadata -->
<PropertyGroup>
<GameExecutableName>Stardew Valley</GameExecutableName>
<GameExecutableName Condition="$(OS) != 'Windows_NT'">StardewValley</GameExecutableName>
</PropertyGroup>
</Project> </Project>

View File

@ -1,141 +0,0 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
This build task is run from the installer project after all projects have been compiled, and
creates the build package in the bin\Packages folder.
-->
<Target Name="PrepareInstaller" AfterTargets="AfterBuild">
<PropertyGroup>
<PlatformName>windows</PlatformName>
<PlatformName Condition="$(OS) != 'Windows_NT'">unix</PlatformName>
<BuildRootPath>$(SolutionDir)</BuildRootPath>
<OutRootPath>$(SolutionDir)\..\bin</OutRootPath>
<SmapiBin>$(BuildRootPath)\SMAPI\bin\$(Configuration)</SmapiBin>
<ToolkitBin>$(BuildRootPath)\SMAPI.Toolkit\bin\$(Configuration)\net4.5</ToolkitBin>
<ConsoleCommandsBin>$(BuildRootPath)\SMAPI.Mods.ConsoleCommands\bin\$(Configuration)</ConsoleCommandsBin>
<SaveBackupBin>$(BuildRootPath)\SMAPI.Mods.SaveBackup\bin\$(Configuration)</SaveBackupBin>
<PackagePath>$(OutRootPath)\SMAPI installer</PackagePath>
<PackageDevPath>$(OutRootPath)\SMAPI installer for developers</PackageDevPath>
</PropertyGroup>
<ItemGroup>
<TranslationFiles Include="$(SmapiBin)\i18n\*.json" />
</ItemGroup>
<!-- reset package directory -->
<RemoveDir Directories="$(PackagePath)" />
<RemoveDir Directories="$(PackageDevPath)" />
<!-- copy installer files -->
<Copy SourceFiles="$(TargetDir)\assets\unix-install.sh" DestinationFiles="$(PackagePath)\install on Linux.sh" />
<Copy SourceFiles="$(TargetDir)\assets\unix-install.sh" DestinationFiles="$(PackagePath)\install on Mac.command" />
<Copy SourceFiles="$(TargetDir)\assets\windows-install.bat" DestinationFiles="$(PackagePath)\install on Windows.bat" />
<Copy SourceFiles="$(TargetDir)\assets\README.txt" DestinationFiles="$(PackagePath)\README.txt" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackagePath)\internal\$(PlatformName)-install.exe" />
<Copy Condition="$(PlatformName) == 'windows'" SourceFiles="$(TargetDir)\assets\windows-exe-config.xml" DestinationFiles="$(PackagePath)\internal\$(PlatformName)-install.exe.config" />
<!--copy bundle files-->
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.exe" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.pdb" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.xml" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\steam_appid.txt" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\0Harmony.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(SmapiBin)\Mono.Cecil.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(SmapiBin)\Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(SmapiBin)\TMXTile.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(SmapiBin)\SMAPI.config.json" DestinationFiles="$(PackagePath)\bundle\smapi-internal\config.json" />
<Copy SourceFiles="$(SmapiBin)\SMAPI.metadata.json" DestinationFiles="$(PackagePath)\bundle\smapi-internal\metadata.json" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(PackagePath)\bundle\smapi-internal\i18n" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\unix-launcher.sh" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(SmapiBin)\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(SmapiBin)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(PlatformName) == 'windows'" SourceFiles="$(TargetDir)\assets\windows-exe-config.xml" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI.exe.config" />
<!--copy bundled mods-->
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.dll" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
<Copy SourceFiles="$(ConsoleCommandsBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.dll" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<Copy SourceFiles="$(SaveBackupBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<!-- fix errors on Linux/Mac (sample: https://smapi.io/log/mMdFUpgB) -->
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<!-- fix Linux/Mac permissions -->
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)\install on Linux.sh&quot;" />
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)\install on Mac.command&quot;" />
<!-- finalise 'for developers' installer -->
<ItemGroup>
<PackageFiles Include="$(PackagePath)\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PackageFiles)" DestinationFolder="$(PackageDevPath)\%(RecursiveDir)" />
<ZipDirectory FromDirPath="$(PackageDevPath)\bundle" ToFilePath="$(PackageDevPath)\internal\$(PlatformName)-install.dat" />
<RemoveDir Directories="$(PackageDevPath)\bundle" />
<!-- finalise normal installer -->
<ReplaceFileText FilePath="$(PackagePath)\bundle\smapi-internal\config.json" Search="&quot;DeveloperMode&quot;: true" Replace="&quot;DeveloperMode&quot;: false" />
<ZipDirectory FromDirPath="$(PackagePath)\bundle" ToFilePath="$(PackagePath)\internal\$(PlatformName)-install.dat" />
<RemoveDir Directories="$(PackagePath)\bundle" />
</Target>
<!-- Create a zip file with the contents of a given folder path. Derived from https://stackoverflow.com/a/38127938/262123. -->
<UsingTask TaskName="ZipDirectory" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
<ParameterGroup>
<FromDirPath ParameterType="System.String" Required="true" />
<ToFilePath ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.IO.Compression.FileSystem" />
<Using Namespace="System.IO.Compression" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try
{
ZipFile.CreateFromDirectory(FromDirPath, ToFilePath);
return true;
}
catch(Exception ex)
{
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
<!-- Replace text in a file based on a regex pattern. Derived from https://stackoverflow.com/a/22571621/262123. -->
<UsingTask TaskName="ReplaceFileText" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<FilePath ParameterType="System.String" Required="true" />
<Search ParameterType="System.String" Required="true" />
<Replace ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Text.RegularExpressions" />
<Code Type="Fragment" Language="cs">
<![CDATA[
File.WriteAllText(
FilePath,
Regex.Replace(File.ReadAllText(FilePath), Search, Replace)
);
]]>
</Code>
</Task>
</UsingTask>
</Project>

View File

@ -0,0 +1,213 @@
#!/bin/bash
#
#
# This is the Bash equivalent of ../windows/prepare-install-package.ps1.
# When making changes, both scripts should be updated.
#
#
##########
## Fetch values
##########
# paths
gamePath="/home/pathoschild/Stardew Valley"
bundleModNames=("ConsoleCommands" "ErrorHandler" "SaveBackup")
# build configuration
buildConfig="Release"
folders=("linux" "macOS" "windows")
declare -A runtimes=(["linux"]="linux-x64" ["macOS"]="osx-x64" ["windows"]="win-x64")
declare -A msBuildPlatformNames=(["linux"]="Unix" ["macOS"]="OSX" ["windows"]="Windows_NT")
# version number
version="$1"
if [ $# -eq 0 ]; then
echo "SMAPI release version (like '4.0.0'):"
read version
fi
##########
## Move to SMAPI root
##########
cd "`dirname "$0"`/../.."
##########
## Clear old build files
##########
echo "Clearing old builds..."
echo "-------------------------------------------------"
for path in bin */**/bin */**/obj; do
echo "$path"
rm -rf $path
done
echo ""
##########
## Compile files
##########
. ${0%/*}/set-smapi-version.sh "$version"
for folder in ${folders[@]}; do
runtime=${runtimes[$folder]}
msbuildPlatformName=${msBuildPlatformNames[$folder]}
echo "Compiling SMAPI for $folder..."
echo "-------------------------------------------------"
dotnet publish src/SMAPI --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained true
echo ""
echo ""
echo "Compiling installer for $folder..."
echo "-------------------------------------------------"
dotnet publish src/SMAPI.Installer --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" -p:PublishTrimmed=True -p:TrimMode=Link --self-contained true
echo ""
echo ""
for modName in ${bundleModNames[@]}; do
echo "Compiling $modName for $folder..."
echo "-------------------------------------------------"
dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false"
echo ""
echo ""
done
done
##########
## Prepare install package
##########
echo "Preparing install package..."
echo "-------------------------------------------------"
# init paths
installAssets="src/SMAPI.Installer/assets"
packagePath="bin/SMAPI installer"
packageDevPath="bin/SMAPI installer for developers"
# init structure
for folder in ${folders[@]}; do
mkdir "$packagePath/internal/$folder/bundle/smapi-internal" --parents
done
# copy base installer files
for name in "install on Linux.sh" "install on macOS.command" "install on Windows.bat" "README.txt"; do
cp "$installAssets/$name" "$packagePath"
done
# copy per-platform files
for folder in ${folders[@]}; do
runtime=${runtimes[$folder]}
# get paths
smapiBin="src/SMAPI/bin/$buildConfig/$runtime/publish"
internalPath="$packagePath/internal/$folder"
bundlePath="$internalPath/bundle"
# installer files
cp -r "src/SMAPI.Installer/bin/$buildConfig/$runtime/publish"/* "$internalPath"
rm -rf "$internalPath/assets"
# runtime config for SMAPI
# This is identical to the one generated by the build, except that the min runtime version is
# set to 5.0.0 (instead of whatever version it was built with) and rollForward is set to latestMinor instead of
# minor.
cp "$installAssets/runtimeconfig.json" "$bundlePath/StardewModdingAPI.runtimeconfig.json"
# installer DLL config
if [ $folder == "windows" ]; then
cp "$installAssets/windows-exe-config.xml" "$packagePath/internal/windows/install.exe.config"
fi
# bundle root files
for name in "StardewModdingAPI" "StardewModdingAPI.dll" "StardewModdingAPI.pdb" "StardewModdingAPI.xml" "steam_appid.txt"; do
if [ $name == "StardewModdingAPI" ] && [ $folder == "windows" ]; then
name="$name.exe"
fi
cp "$smapiBin/$name" "$bundlePath"
done
# bundle i18n
cp -r "$smapiBin/i18n" "$bundlePath/smapi-internal"
# bundle smapi-internal
for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pathoschild.Http.Client.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.pdb" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.pdb" "SMAPI.Toolkit.CoreInterfaces.xml" "System.Net.Http.Formatting.dll"; do
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
done
cp "$smapiBin/SMAPI.config.json" "$bundlePath/smapi-internal/config.json"
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
if [ $folder == "linux" ] || [ $folder == "macOS" ]; then
cp "$installAssets/unix-launcher.sh" "$bundlePath"
else
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
fi
# copy .NET dependencies
if [ $folder == "windows" ]; then
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
fi
# copy legacy .NET dependencies (remove in SMAPI 4.0.0)
cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal"
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal"
# copy bundled mods
for modName in ${bundleModNames[@]}; do
fromPath="src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
targetPath="$bundlePath/Mods/$modName"
mkdir "$targetPath" --parents
cp "$fromPath/$modName.dll" "$targetPath"
cp "$fromPath/$modName.pdb" "$targetPath"
cp "$fromPath/manifest.json" "$targetPath"
if [ -d "$fromPath/i18n" ]; then
cp -r "$fromPath/i18n" "$targetPath"
fi
done
done
# mark scripts executable
for path in "install on Linux.sh" "install on macOS.command" "bundle/unix-launcher.sh"; do
if [ -f "$packagePath/$path" ]; then
chmod 755 "$packagePath/$path"
fi
done
# split into main + for-dev folders
cp -r "$packagePath" "$packageDevPath"
for folder in ${folders[@]}; do
# disable developer mode in main package
sed --in-place --expression="s/\"DeveloperMode\": true/\"DeveloperMode\": false/" "$packagePath/internal/$folder/bundle/smapi-internal/config.json"
# convert bundle folder into final 'install.dat' files
for path in "$packagePath/internal/$folder" "$packageDevPath/internal/$folder"; do
pushd "$path/bundle" > /dev/null
zip "install.dat" * --recurse-paths --quiet
popd > /dev/null
mv "$path/bundle/install.dat" "$path/install.dat"
rm -rf "$path/bundle"
done
done
##########
## Create release zips
##########
# rename folders
mv "$packagePath" "bin/SMAPI $version installer"
mv "$packageDevPath" "bin/SMAPI $version installer for developers"
# package files
pushd bin > /dev/null
zip -9 "SMAPI $version installer.zip" "SMAPI $version installer" --recurse-paths --quiet
zip -9 "SMAPI $version installer for developers.zip" "SMAPI $version installer for developers" --recurse-paths --quiet
popd > /dev/null
echo ""
echo "Done! Package created in $(pwd)/bin"

26
build/unix/set-smapi-version.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
#
#
# This is the Bash equivalent of ../windows/set-smapi-version.ps1.
# When making changes, both scripts should be updated.
#
#
# get version number
version="$1"
if [ $# -eq 0 ]; then
echo "SMAPI release version (like '4.0.0'):"
read version
fi
# move to SMAPI root
cd "`dirname "$0"`/../.."
# apply changes
sed "s/<Version>.+<\/Version>/<Version>$version<\/Version>/" "build/common.targets" --in-place --regexp-extended
sed "s/RawApiVersion = \".+?\";/RawApiVersion = \"$version\";/" "src/SMAPI/Constants.cs" --in-place --regexp-extended
for modName in "ConsoleCommands" "ErrorHandler" "SaveBackup"; do
sed "s/\"(Version|MinimumApiVersion)\": \".+?\"/\"\1\": \"$version\"/g" "src/SMAPI.Mods.$modName/manifest.json" --in-place --regexp-extended
done

View File

@ -0,0 +1,67 @@
#!/bin/bash
##########
## Read config
##########
# get SMAPI version
version="$1"
if [ $# -eq 0 ]; then
echo "SMAPI release version (like '4.0.0'):"
read version
fi
# get Windows bin path
windowsBinPath="$2"
if [ $# -le 1 ]; then
echo "Windows compiled bin path:"
read windowsBinPath
fi
# installer internal folders
buildFolders=("linux" "macOS" "windows")
##########
## Finalize release package
##########
for folderName in "SMAPI $version installer" "SMAPI $version installer for developers"; do
# move files to Linux filesystem
echo "Preparing $folderName.zip..."
echo "-------------------------------------------------"
echo "copying '$windowsBinPath/$folderName' to Linux filesystem..."
cp -r "$windowsBinPath/$folderName" .
# fix permissions
echo "fixing permissions..."
find "$folderName" -type d -exec chmod 755 {} \;
find "$folderName" -type f -exec chmod 644 {} \;
find "$folderName" -name "*.sh" -exec chmod 755 {} \;
find "$folderName" -name "*.command" -exec chmod 755 {} \;
find "$folderName" -name "SMAPI.Installer" -exec chmod 755 {} \;
find "$folderName" -name "StardewModdingAPI" -exec chmod 755 {} \;
# convert bundle folder into final 'install.dat' files
for build in ${buildFolders[@]}; do
echo "packaging $folderName/internal/$build/install.dat..."
pushd "$folderName/internal/$build/bundle" > /dev/null
zip "install.dat" * --recurse-paths --quiet
mv install.dat ../
popd > /dev/null
rm -rf "$folderName/internal/$build/bundle"
done
# zip installer
echo "packaging installer..."
zip -9 "$folderName.zip" "$folderName" --recurse-paths --quiet
# move zip back to Windows bin path
echo "moving release zip to $windowsBinPath/$folderName.zip..."
mv "$folderName.zip" "$windowsBinPath"
rm -rf "$folderName"
echo ""
echo ""
done
echo "Done!"

View File

@ -0,0 +1,11 @@
function In-Place-Regex {
param (
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Search,
[Parameter(Mandatory)][string]$Replace
)
$content = (Get-Content "$Path" -Encoding UTF8)
$content = ($content -replace "$Search", "$Replace")
[System.IO.File]::WriteAllLines((Get-Item "$Path").FullName, $content)
}

View File

@ -0,0 +1,242 @@
#
#
# This is the PowerShell equivalent of ../unix/prepare-install-package.sh, *except* that it doesn't
# set Linux permissions, create the install.dat files, or create the final zip (unless you specify
# --windows-only). Due to limitations in PowerShell, the final changes are handled by the
# windows/finalize-install-package.sh file in WSL.
#
# When making changes, make sure to update ../unix/prepare-install-package.ps1 too.
#
#
. "$PSScriptRoot/lib/in-place-regex.ps1"
##########
## Fetch values
##########
# paths
$gamePath = "C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"
$bundleModNames = "ConsoleCommands", "ErrorHandler", "SaveBackup"
# build configuration
$buildConfig = "Release"
$folders = "linux", "macOS", "windows"
$runtimes = @{ linux = "linux-x64"; macOS = "osx-x64"; windows = "win-x64" }
$msBuildPlatformNames = @{ linux = "Unix"; macOS = "OSX"; windows = "Windows_NT" }
# version number
$version = $args[0]
if (!$version) {
$version = Read-Host "SMAPI release version (like '4.0.0')"
}
# Windows-only build
$windowsOnly = $false
foreach ($arg in $args) {
if ($arg -eq "--windows-only") {
$windowsOnly = $true
$folders = "windows"
$runtimes = @{ windows = "win-x64" }
$msBuildPlatformNames = @{ windows = "Windows_NT" }
}
}
##########
## Move to SMAPI root
##########
cd "$PSScriptRoot/../.."
##########
## Clear old build files
##########
echo "Clearing old builds..."
echo "-------------------------------------------------"
foreach ($path in (dir -Recurse -Include ('bin', 'obj'))) {
echo "$path"
rm -Recurse -Force "$path"
}
echo ""
##########
## Compile files
##########
. "$PSScriptRoot/set-smapi-version.ps1" "$version"
foreach ($folder in $folders) {
$runtime = $runtimes[$folder]
$msbuildPlatformName = $msBuildPlatformNames[$folder]
echo "Compiling SMAPI for $folder..."
echo "-------------------------------------------------"
dotnet publish src/SMAPI --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained true
echo ""
echo ""
echo "Compiling installer for $folder..."
echo "-------------------------------------------------"
dotnet publish src/SMAPI.Installer --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" -p:PublishTrimmed=True -p:TrimMode=Link --self-contained true
echo ""
echo ""
foreach ($modName in $bundleModNames) {
echo "Compiling $modName for $folder..."
echo "-------------------------------------------------"
dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false"
echo ""
echo ""
}
}
##########
## Prepare install package
##########
echo "Preparing install package..."
echo "----------------------------"
# init paths
$installAssets = "src/SMAPI.Installer/assets"
$packagePath = "bin/SMAPI installer"
$packageDevPath = "bin/SMAPI installer for developers"
# init structure
foreach ($folder in $folders) {
mkdir "$packagePath/internal/$folder/bundle/smapi-internal" > $null
}
# copy base installer files
foreach ($name in @("install on Linux.sh", "install on macOS.command", "install on Windows.bat", "README.txt")) {
if ($windowsOnly -and ($name -eq "install on Linux.sh" -or $name -eq "install on macOS.command")) {
continue;
}
cp "$installAssets/$name" "$packagePath"
}
# copy per-platform files
foreach ($folder in $folders) {
$runtime = $runtimes[$folder]
# get paths
$smapiBin = "src/SMAPI/bin/$buildConfig/$runtime/publish"
$internalPath = "$packagePath/internal/$folder"
$bundlePath = "$internalPath/bundle"
# installer files
cp "src/SMAPI.Installer/bin/$buildConfig/$runtime/publish/*" "$internalPath" -Recurse
rm -Recurse -Force "$internalPath/assets"
# runtime config for SMAPI
# This is identical to the one generated by the build, except that the min runtime version is
# set to 5.0.0 (instead of whatever version it was built with) and rollForward is set to latestMinor instead of
# minor.
cp "$installAssets/runtimeconfig.json" "$bundlePath/StardewModdingAPI.runtimeconfig.json"
# installer DLL config
if ($folder -eq "windows") {
cp "$installAssets/windows-exe-config.xml" "$packagePath/internal/windows/install.exe.config"
}
# bundle root files
foreach ($name in @("StardewModdingAPI", "StardewModdingAPI.dll", "StardewModdingAPI.pdb", "StardewModdingAPI.xml", "steam_appid.txt")) {
if ($name -eq "StardewModdingAPI" -and $folder -eq "windows") {
$name = "$name.exe"
}
cp "$smapiBin/$name" "$bundlePath"
}
# bundle i18n
cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal"
# bundle smapi-internal
foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) {
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") {
cp "$installAssets/unix-launcher.sh" "$bundlePath"
}
else {
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
}
# copy .NET dependencies
if ($folder -eq "windows") {
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
}
# copy legacy .NET dependencies (remove in SMAPI 4.0.0)
cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal"
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal"
# copy bundled mods
foreach ($modName in $bundleModNames) {
$fromPath = "src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
$targetPath = "$bundlePath/Mods/$modName"
mkdir "$targetPath" > $null
cp "$fromPath/$modName.dll" "$targetPath"
cp "$fromPath/$modName.pdb" "$targetPath"
cp "$fromPath/manifest.json" "$targetPath"
if (Test-Path "$fromPath/i18n" -PathType Container) {
cp -Recurse "$fromPath/i18n" "$targetPath"
}
}
}
# DISABLED: will be handled by Linux script
# mark scripts executable
#ForEach ($path in @("install on Linux.sh", "install on macOS.command", "bundle/unix-launcher.sh")) {
# if (Test-Path "$packagePath/$path" -PathType Leaf) {
# chmod 755 "$packagePath/$path"
# }
#}
# split into main + for-dev folders
cp -Recurse "$packagePath" "$packageDevPath"
foreach ($folder in $folders) {
# disable developer mode in main package
In-Place-Regex -Path "$packagePath/internal/$folder/bundle/smapi-internal/config.json" -Search "`"DeveloperMode`": true" -Replace "`"DeveloperMode`": false"
# convert bundle folder into final 'install.dat' files
if ($windowsOnly)
{
foreach ($path in @("$packagePath/internal/$folder", "$packageDevPath/internal/$folder"))
{
Compress-Archive -Path "$path/bundle/*" -CompressionLevel Optimal -DestinationPath "$path/install.zip"
mv "$path/install.zip" "$path/install.dat"
rm -Recurse -Force "$path/bundle"
}
}
}
###########
### Create release zips
###########
# rename folders
mv "$packagePath" "bin/SMAPI $version installer"
mv "$packageDevPath" "bin/SMAPI $version installer for developers"
# package files
if ($windowsOnly)
{
Compress-Archive -Path "bin/SMAPI $version installer" -DestinationPath "bin/SMAPI $version installer.zip" -CompressionLevel Optimal
Compress-Archive -Path "bin/SMAPI $version installer for developers" -DestinationPath "bin/SMAPI $version installer for developers.zip" -CompressionLevel Optimal
}
echo ""
echo "Done! See docs/technical/smapi.md to create the release zips."

View File

@ -0,0 +1,25 @@
#
#
# This is the PowerShell equivalent of ../unix/set-smapi-version.sh.
# When making changes, both scripts should be updated.
#
#
. "$PSScriptRoot\lib\in-place-regex.ps1"
# get version number
$version=$args[0]
if (!$version) {
$version = Read-Host "SMAPI release version (like '4.0.0')"
}
# move to SMAPI root
cd "$PSScriptRoot/../.."
# apply changes
In-Place-Regex -Path "build/common.targets" -Search "<Version>.+</Version>" -Replace "<Version>$version</Version>"
In-Place-Regex -Path "src/SMAPI/Constants.cs" -Search "RawApiVersion = `".+?`";" -Replace "RawApiVersion = `"$version`";"
ForEach ($modName in "ConsoleCommands","ErrorHandler","SaveBackup") {
In-Place-Regex -Path "src/SMAPI.Mods.$modName/manifest.json" -Search "`"(Version|MinimumApiVersion)`": `".+?`"" -Replace "`"`$1`": `"$version`""
}

View File

@ -11,9 +11,9 @@ doesn't change any of your game files. It serves seven main purposes:
couldn't._ couldn't._
3. **Rewrite mods for compatibility.** 3. **Rewrite mods for compatibility.**
_SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows _SMAPI rewrites mods' compiled code before loading them so they work on Linux/macOS/Windows
without the mods needing to handle differences between the Linux/Mac and Windows versions of the without the mods needing to handle differences between the Linux/macOS and Windows versions of
game. In some cases it also rewrites code broken by a game update so the mod doesn't break._ the game. In some cases it also rewrites code broken by a game update so the mod doesn't break._
5. **Intercept errors and automatically fix saves.** 5. **Intercept errors and automatically fix saves.**
_SMAPI intercepts errors, shows the error info in the SMAPI console, and in most cases _SMAPI intercepts errors, shows the error info in the SMAPI console, and in most cases
@ -56,17 +56,24 @@ SMAPI rarely shows text in-game, so it only has a few translations. Contribution
[Modding:Translations](https://stardewvalleywiki.com/Modding:Translations) on the wiki for help [Modding:Translations](https://stardewvalleywiki.com/Modding:Translations) on the wiki for help
contributing translations. contributing translations.
locale | status locale | status
---------- | :---------------- ----------- | :----------------
default | ✓ [fully translated](../src/SMAPI/i18n/default.json) default | ✓ [fully translated](../src/SMAPI/i18n/default.json)
Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json) Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json)
French | ✓ [fully translated](../src/SMAPI/i18n/fr.json) French | ✓ [fully translated](../src/SMAPI/i18n/fr.json)
German | ✓ [fully translated](../src/SMAPI/i18n/de.json) German | ✓ [fully translated](../src/SMAPI/i18n/de.json)
Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json) Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json)
Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json) Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json)
Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json) Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json)
Korean | ✓ [fully translated](../src/SMAPI/i18n/ko.json) Korean | ✓ [fully translated](../src/SMAPI/i18n/ko.json)
Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json) [Polish] | ✓ [fully translated](../src/SMAPI/i18n/pl.json)
Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json) Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json)
Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json) Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json)
Turkish | ✓ [fully translated](../src/SMAPI/i18n/tr.json) Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json)
[Thai] | ✓ [fully translated](../src/SMAPI/i18n/th.json)
Turkish | ✓ [fully translated](../src/SMAPI/i18n/tr.json)
[Ukrainian] | ✓ [fully translated](../src/SMAPI/i18n/uk.json)
[Polish]: https://www.nexusmods.com/stardewvalley/mods/3616
[Thai]: https://www.nexusmods.com/stardewvalley/mods/7052
[Ukrainian]: https://www.nexusmods.com/stardewvalley/mods/8427

View File

@ -19,7 +19,7 @@ Released 13 September 2019 for Stardew Valley 1.3.36.
* Added log parser instructions for Android. * Added log parser instructions for Android.
* Fixed log parser failing in some cases due to time format localization. * Fixed log parser failing in some cases due to time format localization.
* For modders: * For mod authors:
* `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. The change will only take effect when you recompile the mod. * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. The change will only take effect when you recompile the mod.
* Fixed 'location list changed' verbose log not correctly listing changes. * Fixed 'location list changed' verbose log not correctly listing changes.
* Fixed mods able to directly load (and in some cases edit) a different mod's local assets using internal asset key forwarding. * Fixed mods able to directly load (and in some cases edit) a different mod's local assets using internal asset key forwarding.
@ -30,7 +30,7 @@ Released 13 September 2019 for Stardew Valley 1.3.36.
Released 23 April 2019 for Stardew Valley 1.3.36. Released 23 April 2019 for Stardew Valley 1.3.36.
* For players: * For players:
* Fixed error when a custom map references certain vanilla tilesheets on Linux/Mac. * Fixed error when a custom map references certain vanilla tilesheets on Linux/macOS.
* Fixed compatibility with some Linux distros. * Fixed compatibility with some Linux distros.
## 2.11.1 ## 2.11.1
@ -42,7 +42,7 @@ Released 17 March 2019 for Stardew Valley 1.3.36.
* Updated mod compatibility list. * Updated mod compatibility list.
* Fixed `world_clear` console command removing chests edited to have a debris name. * Fixed `world_clear` console command removing chests edited to have a debris name.
* For modders: * For mod authors:
* Added support for suppressing false-positive warnings in rare cases. * Added support for suppressing false-positive warnings in rare cases.
* For the web UI: * For the web UI:
@ -55,7 +55,7 @@ Released 01 March 2019 for Stardew Valley 1.3.36.
* For players: * For players:
* Updated for Stardew Valley 1.3.36. * Updated for Stardew Valley 1.3.36.
* For modders: * For mod authors:
* Bumped all deprecation levels to _pending removal_. * Bumped all deprecation levels to _pending removal_.
* For the web UI: * For the web UI:
@ -68,7 +68,7 @@ Released 09 January 2019 for Stardew Valley 1.3.3233.
* For players: * For players:
* SMAPI now keeps the first save backup created for the day, instead of the last one. * SMAPI now keeps the first save backup created for the day, instead of the last one.
* Fixed save backup for some Linux/Mac players. (When compression isn't available, SMAPI will now create uncompressed backups instead.) * Fixed save backup for some Linux/macOS players. (When compression isn't available, SMAPI will now create uncompressed backups instead.)
* Fixed some common dependencies not linking to the mod page in 'missing mod' errors. * Fixed some common dependencies not linking to the mod page in 'missing mod' errors.
* Fixed 'unknown mod' deprecation warnings showing a stack trace when developers mode not enabled. * Fixed 'unknown mod' deprecation warnings showing a stack trace when developers mode not enabled.
* Fixed 'unknown mod' deprecation warnings when they occur in the Mod constructor. * Fixed 'unknown mod' deprecation warnings when they occur in the Mod constructor.
@ -80,7 +80,7 @@ Released 09 January 2019 for Stardew Valley 1.3.3233.
* Added beta status filter to compatibility list. * Added beta status filter to compatibility list.
* Fixed broken ModDrop links in the compatibility list. * Fixed broken ModDrop links in the compatibility list.
* For modders: * For mod authors:
* Asset changes are now propagated into the parsed save being loaded if applicable. * Asset changes are now propagated into the parsed save being loaded if applicable.
* Added locale to context trace logs. * Added locale to context trace logs.
* Fixed error loading custom map tilesheets in some cases. * Fixed error loading custom map tilesheets in some cases.
@ -90,7 +90,7 @@ Released 09 January 2019 for Stardew Valley 1.3.3233.
* Fixed 'unknown mod' deprecation warnings showing the wrong stack trace. * Fixed 'unknown mod' deprecation warnings showing the wrong stack trace.
* Fixed `e.Cursor` in input events showing wrong grab tile when player using a controller moves without moving the viewpoint. * Fixed `e.Cursor` in input events showing wrong grab tile when player using a controller moves without moving the viewpoint.
* Fixed incorrect 'bypassed safety checks' warning for mods using the new `Specialized.LoadStageChanged` event in 2.10. * Fixed incorrect 'bypassed safety checks' warning for mods using the new `Specialized.LoadStageChanged` event in 2.10.
* Deprecated `EntryDll` values whose capitalization don't match the actual file. (This works on Windows, but causes errors for Linux/Mac players.) * Deprecated `EntryDll` values whose capitalization don't match the actual file. (This works on Windows, but causes errors for Linux/macOS players.)
## 2.10.1 ## 2.10.1
Released 30 December 2018 for Stardew Valley 1.3.3233. Released 30 December 2018 for Stardew Valley 1.3.3233.
@ -106,7 +106,7 @@ Released 29 December 2018 for Stardew Valley 1.3.3233.
* Minor performance improvements. * Minor performance improvements.
* Tweaked installer to reduce antivirus false positives. * Tweaked installer to reduce antivirus false positives.
* For modders: * For mod authors:
* Added [events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events): `GameLoop.OneSecondUpdateTicking`, `GameLoop.OneSecondUpdateTicked`, and `Specialized.LoadStageChanged`. * Added [events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events): `GameLoop.OneSecondUpdateTicking`, `GameLoop.OneSecondUpdateTicked`, and `Specialized.LoadStageChanged`.
* Added `e.IsCurrentLocation` event arg to `World` events. * Added `e.IsCurrentLocation` event arg to `World` events.
* You can now use `helper.Data.Read/WriteSaveData` as soon as the save is loaded (instead of once the world is initialized). * You can now use `helper.Data.Read/WriteSaveData` as soon as the save is loaded (instead of once the world is initialized).
@ -133,7 +133,7 @@ Released 16 December 2018 for Stardew Valley 1.3.32.
* Fixed game launch errors logged as `SMAPI` instead of `game`. * Fixed game launch errors logged as `SMAPI` instead of `game`.
* Fixed Windows installer adding unneeded Unix launcher to game folder. * Fixed Windows installer adding unneeded Unix launcher to game folder.
* For modders: * For mod authors:
* Moved content pack methods into a new [content pack API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content_Packs). * Moved content pack methods into a new [content pack API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content_Packs).
* Fixed invalid NPC data propagated when a mod changes NPC dispositions. * Fixed invalid NPC data propagated when a mod changes NPC dispositions.
* Fixed `Display.RenderedWorld` event broken in SMAPI 2.9.1. * Fixed `Display.RenderedWorld` event broken in SMAPI 2.9.1.
@ -162,7 +162,7 @@ Released 07 December 2018 for Stardew Valley 1.3.32.
* Fixed empty "mods with warnings" list in some cases due to hidden warnings. * Fixed empty "mods with warnings" list in some cases due to hidden warnings.
* Fixed Console Commands' handling of tool upgrade levels for item commands. * Fixed Console Commands' handling of tool upgrade levels for item commands.
* For modders: * For mod authors:
* Added ModDrop update keys (see [docs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks)). * Added ModDrop update keys (see [docs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks)).
* Added `IsLocalPlayer` to new player events. * Added `IsLocalPlayer` to new player events.
* Added `helper.CreateTemporaryContentPack` to replace the deprecated `CreateTransitionalContentPack`. * Added `helper.CreateTemporaryContentPack` to replace the deprecated `CreateTransitionalContentPack`.
@ -183,7 +183,7 @@ Released 07 December 2018 for Stardew Valley 1.3.32.
## 2.8.2 ## 2.8.2
Released 19 November 2018 for Stardew Valley 1.3.32. Released 19 November 2018 for Stardew Valley 1.3.32.
* Fixed game crash in MacOS with SMAPI 2.8. * Fixed game crash in macOS with SMAPI 2.8.
## 2.8.1 ## 2.8.1
Released 19 November 2018 for Stardew Valley 1.3.32. Released 19 November 2018 for Stardew Valley 1.3.32.
@ -205,7 +205,7 @@ Released 19 November 2018 for Stardew Valley 1.3.32.
* SMAPI now recommends a compatible SMAPI version if you have an older game version. * SMAPI now recommends a compatible SMAPI version if you have an older game version.
* Improved various error messages to be more clear and intuitive. * Improved various error messages to be more clear and intuitive.
* Improved compatibility with various Linux shells (thanks to lqdev!), and prefer xterm when available. * Improved compatibility with various Linux shells (thanks to lqdev!), and prefer xterm when available.
* Fixed transparency issues on Linux/Mac for some mod images. * Fixed transparency issues on Linux/macOS for some mod images.
* Fixed error when a mod manifest is corrupted. * Fixed error when a mod manifest is corrupted.
* Fixed error when a mod adds an unnamed location. * Fixed error when a mod adds an unnamed location.
* Fixed friendly error no longer shown when SMAPI isn't run from the game folder. * Fixed friendly error no longer shown when SMAPI isn't run from the game folder.
@ -223,9 +223,9 @@ Released 19 November 2018 for Stardew Valley 1.3.32.
* The log parser now has a separate filter for game messages. * The log parser now has a separate filter for game messages.
* The log parser now shows content pack authors (thanks to danvolchek!). * The log parser now shows content pack authors (thanks to danvolchek!).
* Tweaked log parser UI (thanks to danvolchek!). * Tweaked log parser UI (thanks to danvolchek!).
* Fixed log parser instructions for Mac. * Fixed log parser instructions for macOS.
* For modders: * For mod authors:
* Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data) to store mod data in the save file or app data. * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data) to store mod data in the save file or app data.
* Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info. * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info.
* Added [verbose logging](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Logging#Verbose_logging) feature. * Added [verbose logging](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Logging#Verbose_logging) feature.
@ -267,7 +267,7 @@ Released 14 August 2018 for Stardew Valley 1.3.28.
* Improved how mod issues are listed in the console and log. * Improved how mod issues are listed in the console and log.
* Revamped installer. It now... * Revamped installer. It now...
* uses a new format that should be more intuitive; * uses a new format that should be more intuitive;
* lets players on Linux/Mac choose the console color scheme (SMAPI will auto-detect it on Windows); * lets players on Linux/macOS choose the console color scheme (SMAPI will auto-detect it on Windows);
* and validates requirements earlier. * and validates requirements earlier.
* Fixed custom festival maps always using spring tilesheets. * Fixed custom festival maps always using spring tilesheets.
* Fixed `player_add` command not recognising return scepter. * Fixed `player_add` command not recognising return scepter.
@ -275,7 +275,7 @@ Released 14 August 2018 for Stardew Valley 1.3.28.
* Fixed some SMAPI logs not deleted when starting a new session. * Fixed some SMAPI logs not deleted when starting a new session.
* Updated compatibility list. * Updated compatibility list.
* For modders: * For mod authors:
* Added support for `.json` data files in the content API (including Content Patcher). * Added support for `.json` data files in the content API (including Content Patcher).
* Added propagation for asset changes through the content API for... * Added propagation for asset changes through the content API for...
* child sprites; * child sprites;
@ -314,8 +314,8 @@ Released 01 August 2018 for Stardew Valley 1.3.27.
* Removed the `player_setlevel` and `player_setspeed` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those. * Removed the `player_setlevel` and `player_setspeed` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those.
* Fixed `SEHException` errors for some players. * Fixed `SEHException` errors for some players.
* Fixed performance issues for some players. * Fixed performance issues for some players.
* Fixed default color scheme on Mac or in PowerShell (configurable via `StardewModdingAPI.config.json`). * Fixed default color scheme on macOS or in PowerShell (configurable via `StardewModdingAPI.config.json`).
* Fixed installer error on Linux/Mac in some cases. * Fixed installer error on Linux/macOS in some cases.
* Fixed installer not finding some game paths or showing duplicate paths. * Fixed installer not finding some game paths or showing duplicate paths.
* Fixed installer not removing some SMAPI files. * Fixed installer not removing some SMAPI files.
* Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!) * Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!)
@ -336,7 +336,7 @@ Released 01 August 2018 for Stardew Valley 1.3.27.
* Fixed log parser mangling crossplatform paths in some cases. * Fixed log parser mangling crossplatform paths in some cases.
* Fixed `smapi.io/install` not linking to a useful page. * Fixed `smapi.io/install` not linking to a useful page.
* For modders: * For mod authors:
* Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input. * Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input.
* Added code analysis in the NuGet package to flag common issues as warnings. * Added code analysis in the NuGet package to flag common issues as warnings.
* Replaced `LocationEvents` to support multiplayer: * Replaced `LocationEvents` to support multiplayer:
@ -345,7 +345,7 @@ Released 01 August 2018 for Stardew Valley 1.3.27.
* each event now provides a list of added/removed values; * each event now provides a list of added/removed values;
* added buildings-changed event. * added buildings-changed event.
* Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags.
* Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, macOS, or Windows.
* Added `semanticVersion.IsPrerelease()` method. * Added `semanticVersion.IsPrerelease()` method.
* Added support for launching multiple instances transparently. This removes the former `--log-path` command-line argument. * Added support for launching multiple instances transparently. This removes the former `--log-path` command-line argument.
* Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map. * Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map.
@ -376,7 +376,7 @@ Released 01 August 2018 for Stardew Valley 1.3.27.
* Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.) * Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.)
* For SMAPI developers: * For SMAPI developers:
* Added more consistent crossplatform handling, including MacOS detection. * Added more consistent crossplatform handling, including macOS detection.
* Added beta update channel. * Added beta update channel.
* Added optional mod metadata to the web API (including Nexus info, wiki metadata, etc). * Added optional mod metadata to the web API (including Nexus info, wiki metadata, etc).
* Added early prototype of SMAPI 3.0 events via `helper.Events`. * Added early prototype of SMAPI 3.0 events via `helper.Events`.
@ -411,14 +411,14 @@ Released 26 March 2018 for Stardew Valley 1.2.301.2.33.
* For players: * For players:
* Fixed some textures not updated when a mod changes them. * Fixed some textures not updated when a mod changes them.
* Fixed visual bug on Linux/Mac when mods overlay textures. * Fixed visual bug on Linux/macOS when mods overlay textures.
* Fixed error when mods remove an asset editor/loader. * Fixed error when mods remove an asset editor/loader.
* Fixed minimum game version incorrectly increased in SMAPI 2.5.3. * Fixed minimum game version incorrectly increased in SMAPI 2.5.3.
* For the [log parser](https://smapi.io/log): * For the [log parser](https://smapi.io/log):
* Fixed error when log text contains certain tokens. * Fixed error when log text contains certain tokens.
* For modders: * For mod authors:
* Updated to Json.NET 11.0.2. * Updated to Json.NET 11.0.2.
* For SMAPI developers: * For SMAPI developers:
@ -448,7 +448,7 @@ Released 13 March 2018 for Stardew Valley ~~1.2.30~~1.2.33.
## 2.5.2 ## 2.5.2
Released 25 February 2018 for Stardew Valley 1.2.301.2.33. Released 25 February 2018 for Stardew Valley 1.2.301.2.33.
* For modders: * For mod authors:
* Fixed issue where replacing an asset through `asset.AsImage()` or `asset.AsDictionary()` didn't take effect. * Fixed issue where replacing an asset through `asset.AsImage()` or `asset.AsDictionary()` didn't take effect.
* For the [log parser](https://smapi.io/log): * For the [log parser](https://smapi.io/log):
@ -467,12 +467,12 @@ Released 24 February 2018 for Stardew Valley 1.2.301.2.33.
* **Added support for [content packs](https://stardewvalleywiki.com/Modding:Content_packs)**. * **Added support for [content packs](https://stardewvalleywiki.com/Modding:Content_packs)**.
<small>_Content packs are collections of files for a SMAPI mod to load. These can be installed directly under `Mods` like a normal SMAPI mod, get automatic update and compatibility checks, and provide convenient APIs to the mods that read them._</small> <small>_Content packs are collections of files for a SMAPI mod to load. These can be installed directly under `Mods` like a normal SMAPI mod, get automatic update and compatibility checks, and provide convenient APIs to the mods that read them._</small>
* Added mod detection for unhandled errors (so most errors now mention which mod caused them). * Added mod detection for unhandled errors (so most errors now mention which mod caused them).
* Added install scripts for Linux/Mac (no more manual terminal commands!). * Added install scripts for Linux/macOS (no more manual terminal commands!).
* Added the missing mod's name and URL to dependency errors. * Added the missing mod's name and URL to dependency errors.
* Fixed uninstall script not reporting when done on Linux/Mac. * Fixed uninstall script not reporting when done on Linux/macOS.
* Updated compatibility list and enabled update checks for more mods. * Updated compatibility list and enabled update checks for more mods.
* For modders: * For mod authors:
* Added support for content packs and new APIs to read them. * Added support for content packs and new APIs to read them.
* Added support for `ISemanticVersion` in JSON models. * Added support for `ISemanticVersion` in JSON models.
* Added `SpecializedEvents.UnvalidatedUpdateTick` event for specialized use cases. * Added `SpecializedEvents.UnvalidatedUpdateTick` event for specialized use cases.
@ -506,7 +506,7 @@ Released 24 January 2018 for Stardew Valley 1.2.301.2.33.
* For the [log parser](https://smapi.io/log): * For the [log parser](https://smapi.io/log):
* Fixed error parsing logs with zero installed mods. * Fixed error parsing logs with zero installed mods.
* For modders: * For mod authors:
* Added `SaveEvents.BeforeCreate` and `AfterCreate` events. * Added `SaveEvents.BeforeCreate` and `AfterCreate` events.
* Added `SButton` `IsActionButton()` and `IsUseToolButton()` extensions. * Added `SButton` `IsActionButton()` and `IsUseToolButton()` extensions.
* Improved JSON parse errors to provide more useful info for troubleshooting. * Improved JSON parse errors to provide more useful info for troubleshooting.
@ -524,10 +524,10 @@ Released 26 December 2017 for Stardew Valley 1.2.301.2.33.
* For players: * For players:
* Added a user-friendly [download page](https://smapi.io). * Added a user-friendly [download page](https://smapi.io).
* Improved cryptic libgdiplus errors on Mac when Mono isn't installed. * Improved cryptic libgdiplus errors on macOS when Mono isn't installed.
* Fixed mod UIs hidden when menu backgrounds are enabled. * Fixed mod UIs hidden when menu backgrounds are enabled.
* For modders: * For mod authors:
* **Added mod-provided APIs** to allow simple integrations between mods, even without direct assembly references. * **Added mod-provided APIs** to allow simple integrations between mods, even without direct assembly references.
* Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialized). * Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialized).
* Added `IsSuppressed` to input events so mods can optionally avoid handling keys another mod has already handled. * Added `IsSuppressed` to input events so mods can optionally avoid handling keys another mod has already handled.
@ -545,9 +545,9 @@ Released 26 December 2017 for Stardew Valley 1.2.301.2.33.
Released 02 December 2017 for Stardew Valley 1.2.301.2.33. Released 02 December 2017 for Stardew Valley 1.2.301.2.33.
* For players: * For players:
* Fixed error when a mod loads custom assets on Linux/Mac. * Fixed error when a mod loads custom assets on Linux/macOS.
* Fixed error when checking for updates on Linux/Mac due to API HTTPS redirect. * Fixed error when checking for updates on Linux/macOS due to API HTTPS redirect.
* Fixed error when Mac adds an `mcs` symlink to the installer package. * Fixed error when macOS adds an `mcs` symlink to the installer package.
* Fixed `player_add` command not handling tool upgrade levels. * Fixed `player_add` command not handling tool upgrade levels.
* Improved error when a mod has an invalid `EntryDLL` filename format. * Improved error when a mod has an invalid `EntryDLL` filename format.
* Updated compatibility list. * Updated compatibility list.
@ -557,7 +557,7 @@ Released 02 December 2017 for Stardew Valley 1.2.301.2.33.
* Fixed error when uploading very large logs. * Fixed error when uploading very large logs.
* Slightly improved the UI. * Slightly improved the UI.
* For modders: * For mod authors:
* Added `helper.Content.NormalizeAssetName` method. * Added `helper.Content.NormalizeAssetName` method.
* Added `SDate.DaysSinceStart` property. * Added `SDate.DaysSinceStart` property.
* Fixed input events' `e.SuppressButton(button)` method ignoring specified button. * Fixed input events' `e.SuppressButton(button)` method ignoring specified button.
@ -575,7 +575,7 @@ Released 01 November 2017 for Stardew Valley 1.2.301.2.33.
* Fixed compatibility check for players with Stardew Valley 1.08. * Fixed compatibility check for players with Stardew Valley 1.08.
* Fixed `player_setlevel` command not setting XP too. * Fixed `player_setlevel` command not setting XP too.
* For modders: * For mod authors:
* The reflection API now works with public code to simplify mod integrations. * The reflection API now works with public code to simplify mod integrations.
* The content API now lets you invalidated multiple assets at once. * The content API now lets you invalidated multiple assets at once.
* The `InputEvents` have been improved: * The `InputEvents` have been improved:
@ -600,7 +600,7 @@ Released 14 October 2017 for Stardew Valley 1.2.301.2.33.
* **Mod update checks** * **Mod update checks**
SMAPI now checks if your mods have updates available, and will alert you in the console with a convenient link to the SMAPI now checks if your mods have updates available, and will alert you in the console with a convenient link to the
mod page. This works with mods from the Chucklefish mod site, GitHub, or Nexus Mods. SMAPI 2.0 launches with mod page. This works with mods from the Chucklefish mod site, GitHub, or Nexus Mods. SMAPI 2.0 launches with
update-check support for over 250 existing mods, and more will be added as modders enable the feature. update-check support for over 250 existing mods, and more will be added as mod authors enable the feature.
* **Mod stability warnings** * **Mod stability warnings**
SMAPI now detects when a mod contains code which can destabilise your game or corrupt your save, and shows a warning SMAPI now detects when a mod contains code which can destabilise your game or corrupt your save, and shows a warning
@ -610,7 +610,7 @@ Released 14 October 2017 for Stardew Valley 1.2.301.2.33.
The console is now simpler and easier to read, some commands have been streamlined, and the colors now adjust to fit The console is now simpler and easier to read, some commands have been streamlined, and the colors now adjust to fit
your terminal background color. your terminal background color.
* **New features for modders** * **New features for mod authors**
SMAPI 2.0 adds several features to enable new kinds of mods (see SMAPI 2.0 adds several features to enable new kinds of mods (see
[API documentation](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs)). [API documentation](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs)).
@ -651,7 +651,7 @@ For players:
* The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color. * The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color.
* Renamed installer folder to avoid confusion. * Renamed installer folder to avoid confusion.
* Updated compatibility list. * Updated compatibility list.
* Fixed update check errors on Linux/Mac. * Fixed update check errors on Linux/macOS.
* Fixed collection-changed errors during startup for some players. * Fixed collection-changed errors during startup for some players.
For mod developers: For mod developers:
@ -685,10 +685,10 @@ For SMAPI developers:
Released 09 September 2017 for Stardew Valley 1.2.301.2.33. Released 09 September 2017 for Stardew Valley 1.2.301.2.33.
For players: For players:
* Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader. * Fixed errors when loading some custom maps on Linux/macOS or using XNB Loader.
* Fixed errors in rare cases when a mod calculates an in-game date. * Fixed errors in rare cases when a mod calculates an in-game date.
For modders: For mod authors:
* Added UTC timestamp to log file. * Added UTC timestamp to log file.
For SMAPI developers: For SMAPI developers:
@ -726,7 +726,7 @@ For players:
* Fixed controller mod input broken in 1.15. * Fixed controller mod input broken in 1.15.
* Fixed TrainerMod packaging unneeded files. * Fixed TrainerMod packaging unneeded files.
For modders: For mod authors:
* Fixed mod registry lookups by unique ID not being case-insensitive. * Fixed mod registry lookups by unique ID not being case-insensitive.
## 1.15 ## 1.15
@ -744,7 +744,7 @@ For players:
* Fixed invalid `ObjectInformation.xnb` causing a flood of warnings; SMAPI now shows one error instead. * Fixed invalid `ObjectInformation.xnb` causing a flood of warnings; SMAPI now shows one error instead.
* Updated mod compatibility list. * Updated mod compatibility list.
For modders: For mod authors:
* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Utilities#Dates)). * Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Utilities#Dates)).
* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)). * Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)).
* Added more useful logging when loading mods. * Added more useful logging when loading mods.
@ -772,12 +772,12 @@ For players:
* you have Stardew Valley 1.11 or earlier (which aren't compatible); * you have Stardew Valley 1.11 or earlier (which aren't compatible);
* you run `install.exe` from within the downloaded zip file. * you run `install.exe` from within the downloaded zip file.
* Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event.
* Fixed `libgdiplus.dylib` errors for some players on Mac. * Fixed `libgdiplus.dylib` errors for some players on macOS.
* Fixed rare crash when window loses focus for a few players. * Fixed rare crash when window loses focus for a few players.
* Bumped minimum game version to 1.2.30. * Bumped minimum game version to 1.2.30.
* Updated mod compatibility list. * Updated mod compatibility list.
For modders: For mod authors:
* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)). * You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)).
* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation)). * You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation)).
* You can now load unpacked `.tbin` files from your mod folder through the content API. * You can now load unpacked `.tbin` files from your mod folder through the content API.
@ -788,7 +788,7 @@ For modders:
* Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`.
* Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. * Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension.
* Fixed `debug` command output not printed to console. * Fixed `debug` command output not printed to console.
* Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most modders think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead. * Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most mod authors think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead.
## 1.13.1 ## 1.13.1
Released 19 May 2017 for Stardew Valley 1.2.261.2.29. Released 19 May 2017 for Stardew Valley 1.2.261.2.29.
@ -805,8 +805,8 @@ For players:
* SMAPI now recovers automatically from errors in the game loop when possible. * SMAPI now recovers automatically from errors in the game loop when possible.
* SMAPI now remembers if your game crashed and offers help next time you launch it. * SMAPI now remembers if your game crashed and offers help next time you launch it.
* Fixed installer sometimes finding redundant game paths. * Fixed installer sometimes finding redundant game paths.
* Fixed save events not being raised after the first day on Linux/Mac. * Fixed save events not being raised after the first day on Linux/macOS.
* Fixed error on Linux/Mac when a mod loads a PNG immediately after the save is loaded. * Fixed error on Linux/macOS when a mod loads a PNG immediately after the save is loaded.
* Updated mod compatibility list for Stardew Valley 1.2. * Updated mod compatibility list for Stardew Valley 1.2.
For mod developers: For mod developers:
@ -826,15 +826,15 @@ Released 03 May 2017 for Stardew Valley 1.2.261.2.29.
For players: For players:
* The installer now lets you choose the install path if you have multiple copies of the game, instead of using the first path found. * The installer now lets you choose the install path if you have multiple copies of the game, instead of using the first path found.
* Fixed mod draw errors breaking the game. * Fixed mod draw errors breaking the game.
* Fixed mods on Linux/Mac no longer working after the game saves. * Fixed mods on Linux/macOS no longer working after the game saves.
* Fixed `libgdiplus.dylib` errors on Mac when mods read PNG files. * Fixed `libgdiplus.dylib` errors on macOS when mods read PNG files.
* Adopted pufferchick. * Adopted pufferchick.
For mod developers: For mod developers:
* Unknown mod manifest fields are now stored in `IManifest::ExtraFields`. * Unknown mod manifest fields are now stored in `IManifest::ExtraFields`.
* The content API now defaults to `ContentSource.ModFolder`. * The content API now defaults to `ContentSource.ModFolder`.
* Fixed content API error when loading a PNG during early game init (e.g. in mod's `Entry`). * Fixed content API error when loading a PNG during early game init (e.g. in mod's `Entry`).
* Fixed content API error when loading an XNB from the mod folder on Mac. * Fixed content API error when loading an XNB from the mod folder on macOS.
## 1.11 ## 1.11
Released 30 April 2017 for Stardew Valley 1.2.26. Released 30 April 2017 for Stardew Valley 1.2.26.
@ -888,7 +888,7 @@ For players:
* Fixed the game-needs-an-update error not pausing before exit. * Fixed the game-needs-an-update error not pausing before exit.
* Fixed installer errors for some players when deleting files. * Fixed installer errors for some players when deleting files.
* Fixed installer not ignoring potential game folders that don't contain a Stardew Valley exe. * Fixed installer not ignoring potential game folders that don't contain a Stardew Valley exe.
* Fixed installer not recognising Linux/Mac paths starting with `~/` or containing an escaped space. * Fixed installer not recognising Linux/macOS paths starting with `~/` or containing an escaped space.
* Fixed TrainerMod letting you add invalid items which may crash the game. * Fixed TrainerMod letting you add invalid items which may crash the game.
* Fixed TrainerMod's `world_downminelevel` command not working. * Fixed TrainerMod's `world_downminelevel` command not working.
* Fixed rare issue where mod dependencies would override SMAPI dependencies and cause unpredictable bugs. * Fixed rare issue where mod dependencies would override SMAPI dependencies and cause unpredictable bugs.
@ -912,7 +912,7 @@ For mod developers:
* Removed the experimental `IConfigFile`. * Removed the experimental `IConfigFile`.
For SMAPI developers: For SMAPI developers:
* Added support for debugging SMAPI on Linux/Mac if supported by the editor. * Added support for debugging SMAPI on Linux/macOS if supported by the editor.
## 1.8 ## 1.8
Released 04 February 2017 for Stardew Valley 1.11.11. Released 04 February 2017 for Stardew Valley 1.11.11.
@ -1004,7 +1004,7 @@ For players:
* Improved installer wording to reduce confusion. * Improved installer wording to reduce confusion.
* Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory.
* Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition.
* Fixed the SMAPI console not being shown on Linux and Mac. * Fixed the SMAPI console not being shown on Linux and macOS.
For developers: For developers:
* Added a reflection API (via `helper.Reflection`) that simplifies robust access to the game's private fields and methods. * Added a reflection API (via `helper.Reflection`) that simplifies robust access to the game's private fields and methods.
@ -1016,7 +1016,7 @@ For developers:
Released 04 December 2016 for Stardew Valley 1.11.11. Released 04 December 2016 for Stardew Valley 1.11.11.
For players: For players:
* You can now run most mods on any platform (e.g. run Windows mods on Linux/Mac). * You can now run most mods on any platform (e.g. run Windows mods on Linux/macOS).
* Fixed the normal uninstaller not removing files added by the 'SMAPI for developers' installer. * Fixed the normal uninstaller not removing files added by the 'SMAPI for developers' installer.
## 1.2 ## 1.2
@ -1063,7 +1063,7 @@ For developers:
Released 11 November 2016 for Stardew Valley 1.11.11. Released 11 November 2016 for Stardew Valley 1.11.11.
For players: For players:
* Added support for Linux and Mac. * Added support for Linux and macOS.
* Added installer to automate adding & removing SMAPI. * Added installer to automate adding & removing SMAPI.
* Added background update check on launch. * Added background update check on launch.
* Fixed missing `steam_appid.txt` file. * Fixed missing `steam_appid.txt` file.

View File

@ -2,11 +2,759 @@
# Release notes # Release notes
<!-- <!--
## Future release ## 4.0.0
* For modders: * The installer no longer supports updating from SMAPI 2.11.3 or earlier (released in 2019).
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). _If needed, you can update to SMAPI 3.16.0 first and then install the latest version._
--> -->
## 3.18.2
Released 09 January 2023 for Stardew Valley 1.5.6 or later.
* For players:
* Fixed empty save backups for some macOS players.
* Fixed `player_add` console command not handling custom slingshots correctly (thanks too DaLion!).
* For mod authors:
* Added `DelegatingModHooks` utility for mods which need to override SMAPI's mod hooks directly.
* Updated to Newtonsoft.Json 13.0.2 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.2)) and Pintail 2.2.2 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#222)).
## 3.18.1
Released 01 December 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Fixed mod texture edits sometimes cut off (thanks to atravita!).
* For the web UI:
* The log parser no longer warns about missing Error Handler on Android, where it doesn't exist yet (thanks to AnotherPillow!).
## 3.18.0
Released 12 November 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/74565278).
* For players:
* You can now override the mod load order in `smapi-internal/config.json` (thanks to Shockah!).
* You can now disable console input in `smapi-internal/config.json`, which may reduce CPU usage on some Linux systems.
* Fixed map edits not always applied for farmhands in multiplayer (thanks to SinZ163!).
* Internal changes to prepare for the upcoming Stardew Valley 1.6 and SMAPI 4.0.
* For mod authors:
* Optimized asset name comparisons (thanks to atravita!).
* Raised all deprecation messages to the 'pending removal' level.
* **This is the last major update before SMAPI 4.0.0, which will drop all deprecated APIs.** If you haven't [fixed deprecation warnings in your mod code](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) (if any), you should do it soon. SMAPI 4.0.0 will release alongside the upcoming Stardew Valley 1.6.
* For the web UI:
* The log parser now detects split-screen mode and shows which screen logged each message.
## 3.17.2
Released 21 October 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Fixed installer crash if Steam's library data is invalid or in an old format; it'll now be ignored instead.
* For mod authors:
* Fixed image patches sometimes applied one pixel higher than expected after 3.17.0 (thanks to atravita!).
## 3.17.1
Released 10 October 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Fixed installer error on Windows if the Steam library folder exists but doesn't contain Steam's `.vdf` library data file.
## 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.
* For players:
* Fixed `NoSuitableGraphicsDeviceException` launch error for some players with compatible GPUs since 3.16.0.
## 3.16.1
Released 29 August 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Updated PyTK compatibility mode for the latest PyTK version.
* Fixed broken mods sometimes incorrectly listed as duplicate.
## 3.16.0
Released 22 August 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/70797008).
* For players:
* Added error message if mod files are detected directly under `Mods` (instead of each mod having its own subfolder).
* SMAPI now sets a success/error code when the game exits.
_This is used by your OS (like Windows) to decide whether to keep the console window open when the game ends._
* Fixed SMAPI on Windows applying different DPI awareness settings than the game (thanks to spacechase0!).
* Fixed Linux/macOS installer's color scheme question partly unreadable if the terminal background is dark.
* Fixed error message when a mod loads an invalid PNG file (thanks to atravita!).
* Fixed error message when a mod is duplicated, but one of the copies is also missing the DLL file. This now shows the duplicate-mod message instead of the missing-DLL message.
* Fixed macOS launcher using Terminal regardless of the system's default terminal (thanks to ishan!).
* Fixed best practices in Linux/macOS launcher scripts (thanks to ishan!).
* Improved translations. Thanks to KediDili (updated Turkish)!
* For mod authors:
* While loading your mod, SMAPI now searches for indirect dependencies in your mod's folder (thanks to TehPers)! This mainly enables F# mods.
* **Raised deprecation message levels.**
_Deprecation warnings are now player-visible in the SMAPI console as faded `DEBUG` messages._
* Updated to Pintail 2.2.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#221)).
* Switched SMAPI's `.pdb` files to the newer 'portable' format. This has no effect on mods.
* For the web UI:
* Added log parser warning about performance of PyTK 1.23.0 or earlier.
* Converted images to SVG (thanks to ishan!).
* Updated log parser for the new update alert format in SMAPI 3.15.1.
* Updated the JSON validator/schema for Content Patcher 1.28.0.
* Fixed log parsing for invalid content packs.
* Fixed log parsing if a mod logged a null character.
## 3.15.1
Released 06 July 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Added current version to update alerts (thanks to ishan!).
* Fixed lag for some players since Stardew Valley 1.5.5.
* Fixed `smapi-internal/config.user.json` overrides not applied after SMAPI 3.14.0.
* Fixed PyTK not rescaling images correctly in some cases.
_When PyTK 1.23.0 or earlier is installed, this will disable the main performance improvements in SMAPI 3.15.0._
* Updated compatibility list.
* For mod authors:
* The [FluentHttpClient package](https://github.com/Pathoschild/FluentHttpClient#readme) is now loaded by SMAPI.
* Fixed `TRACE` logs not tracking reloaded map tilesheets as a propagated asset.
* For the web UI:
* Added log parser suggested fix for missing/outdated Error Handler, and improved visual styles.
* Updated the JSON validator/schema for Content Patcher 1.27.0.
* Fixed the mod count in the log parser metadata.
## 3.15.0
Released 17 June 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/67877219).
* For players:
* Optimized mod image file loading.
* Minor optimizations (thanks to Michael Kuklinski / Ameisen!).
* Updated compatibility list.
* For mod authors:
* Added an [`IRawTextureData` asset type](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Raw_texture_data), to avoid creating full `Texture2D` instances in many cases.
* In `smapi-internal/config.json`, you can now enable verbose logging for specific mods (instead of all or nothing).
* Updated dependencies:
* Harmony 2.2.1 (see changes in [2.2.0](https://github.com/pardeike/Harmony/releases/tag/v2.2.0.0) and [2.2.1](https://github.com/pardeike/Harmony/releases/tag/v2.2.1.0));
* Newtonsoft.Json 13.0.1 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.1));
* Pintail 2.2.0 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#220)).
* Removed transitional `UsePintail` option added in 3.14.0 (now always enabled).
* Fixed `onBehalfOf` arguments in the new content API being case-sensitive.
* Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players.
## 3.14.7
Released 01 June 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Optimized reflection cache to reduce frame skips for some players.
* For mod authors:
* Removed `runtimeconfig.json` setting which impacted hot reload support.
## 3.14.6
Released 27 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Fixed error in split-screen mode when a mod provides a localized asset in one screen but not another.
* Minor optimizations.
## 3.14.5
Released 22 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Improved performance when mods change some asset types (including NPC portraits/sprites).
* Fixed _could not find file_ error if a mod provides a localized version of a normally unlocalized asset and then stops providing it.
* Fixed CurseForge update checks for the new CurseForge API.
## 3.14.4
Released 15 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Improved performance for mods using deprecated APIs.
* For mod authors:
* Removed warning for mods which use `dynamic`.
_This no longer causes errors on Linux/macOS after Stardew Valley 1.5.5._
## 3.14.3
Released 12 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Reduced in-game performance impact.
* For mod authors:
* Refactored how event handling works under the hood, particularly the new content API. This should have no effect on mod usage.
* Verbose mode now logs the in-game time.
* Fixed error when loading a `.xnb` file through the old content API without the file extension.
* Fixed asset propagation for player sprites not fully updating recolor masks in some cases.
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.26.0.
## 3.14.2
Released 08 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Enabled case-insensitive file paths by default for Android and Linux players.
_This was temporarily disabled in SMAPI 3.14.1, and will remain disabled by default on macOS and Windows since their filesystems are already case-insensitive._
* Various performance improvements.
* For mod authors:
* Dynamic content packs created via `helper.ContentPacks.CreateTemporary` or `CreateFake` are now listed in the log file.
* Fixed assets loaded through a fake content pack not working correctly since 3.14.0.
## 3.14.1
Released 06 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Improved performance for mods still using the previous content API.
* Disabled case-insensitive file paths (introduced in 3.14.0) by default.
_You can enable them by editing `smapi-internal/config.json` if needed. They'll be re-enabled in an upcoming version after they're reworked a bit._
* Removed experimental 'aggressive memory optimizations' option.
_This was disabled by default and is no longer needed in most cases. Memory usage will be better reduced by reworked asset propagation in the upcoming SMAPI 4.0.0._
* Fixed 'content file was not found' error when the game tries to load unlocalized text from a localizable mod data asset in 3.14.0.
* Fixed error reading empty JSON files. These are now treated as if they didn't exist (matching pre-3.14.0 behavior).
* Updated compatibility list.
## 3.14.0
Released 01 May 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/65265507).
### For players
This is a big update, but existing mods should all work fine. If the latest version of a mod breaks in SMAPI 3.14, please report it [on the SMAPI mod page](https://www.nexusmods.com/stardewvalley/mods/2400?tab=posts).
* Improvements:
* SMAPI now ignores dot-prefixed files when searching for mod folders (thanks to Nuztalgia!).
* On Linux, SMAPI now fixes many case-sensitive mod path issues automatically.
* On Linux/macOS, added `--use-current-shell` [command-line argument](technical/smapi.md#command-line-arguments) to avoid opening a separate terminal window.
* Improved performance in some cases.
* Improved translations. Thanks to ChulkyBow (updated Ukrainian)!
* Dropped update checks for the unofficial 64-bit patcher (obsolete since SMAPI 3.12.6).
* Fixes:
* Fixed some movie theater textures not translated when loaded through SMAPI (specifically assets with the `_international` suffix).
* Fixed the warning text when a mod causes an asset load conflict with itself.
* Fixed `--no-terminal` [command-line argument](technical/smapi.md#command-line-arguments) on Linux/macOS still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!).
* Fixed `player_add` console command not handling journal scraps and secret notes correctly.
* Fixed `set_farm_type` console command not updating warps.
* For the web UI:
* Improved log parser UI (thanks to KhloeLeclair!):
* Added pagination for big logs.
* Added search box to filter the log.
* Added option to show/hide content packs in the mod list.
* Added jump links in the sidebar.
* The filter options now stick to the top of the screen when scrolling.
* Rewrote rendering to improve performance.
### For mod authors
This is a big release that includes the new features planned for SMAPI 4.0.0.
For C# mod authors: your mods should still work fine in SMAPI 3.14.0. However you should review the [migration to SMAPI 4.0](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) guide and update your mods when possible. Deprecated code will be removed when SMAPI 4.0.0 releases later this year (no sooner than August 2022), and break any mods which haven't updated by that time. You can update affected mods now, there's no need to wait for 4.0.0.
For content pack authors: SMAPI 3.14.0 and 4.0.0 don't affect content packs. They should work fine as long as
the C# mod that loads them is updated.
* Major changes:
* Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.
_These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has changed, and avoid data corruption issues in some edge cases._
* Added [nullable reference type annotations](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Nullable_reference_type_annotations) for all APIs.
* Added [`helper.GameContent` and `helper.ModContent`](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Content_loading_API), which will replace `helper.Content` in SMAPI 4.0.0.
* Improved [mod-provided API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) proxying (thanks to Shockah!).
_This adds support for custom interfaces in return values or input arguments, custom enums if their values match, generic methods, and more. This is an internal change, you don't need to do anything different in your mod code._
* Mod files loaded through SMAPI APIs (including `helper.Content.Load`) are now case-insensitive, even on Linux.
* Enabled deprecation notices for all deprecated APIs. These will only be shown in `TRACE` logs for at least a month after SMAPI 3.14.0 releases.
* Other improvements:
* Added `IAssetDataForImage.ExtendMap` to resize maps in asset editors.
* Added `IContentPack.ModContent` property to manage content pack assets.
* Added `Constants.ContentPath` to get the full path to the game's `Content` folder.
* Added `IAssetName` fields to the info received by `IAssetEditor`, `IAssetLoader`, and content event methods.
_This adds methods for working with asset names, parsed locales, etc._
* Added `helper.Content.ParseAssetName` to get an `IAssetName` for an arbitrary asset key.
* Added [command-line arguments](technical/smapi.md#command-line-arguments) to toggle developer mode (thanks to Tondorian!).
* If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated by `helper.Content.InvalidateCache`.
* The `ISemanticVersion` comparison methods (`CompareTo`, `IsBetween`, `IsNewerThan`, and `IsOlderThan`) now allow null values. A null version is always considered older than any non-null version per [best practices](https://docs.microsoft.com/en-us/dotnet/api/system.icomparable-1.compareto#remarks).
* Deprecation notices now show a shorter stack trace in most cases, so it's clearer where the deprecated code is in the mod.
* Fixes:
* Fixed the `SDate` constructor being case-sensitive.
* Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`).
* Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead.
* Fixed null handling in various edge cases.
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.25.0.
* Added `data-*` attributes to the log parser page for external tools.
* Fixed JSON validator showing incorrect error for update keys without a subkey.
### For SMAPI contributors
* You no longer need a Nexus API key to launch the `SMAPI.Web` project locally.
## 3.13.4
Released 16 January 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Fixed Linux/macOS launch error in 3.13.3.
## 3.13.3
Released 16 January 2022 for Stardew Valley 1.5.6 or later.
* For players:
* **SMAPI now needs Stardew Valley 1.5.6 or later.**
* Added automatic fix for custom maps which are missing a required tilesheet.
* Added automatic save recovery when a custom farm type isn't available anymore.
* Added the game's new build number to the SMAPI console + log.
* The installer now detects Xbox app game folders.
* Reduced mod loading time a bit.
* Fixed macOS launch issue when using some terminals (thanks to bruce2409!).
* Fixed Linux/macOS terminal ignoring backspaces in Stardew Valley 1.5.5+.
* Fixed extra newlines in the SMAPI console.
* Fixed outdated instructions in Steam error message.
* Fixed uninstaller not removing `StardewModdingAPI.deps.json` file.
* Simplified [running without a terminal on Linux/macOS](https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting#SMAPI_doesn.27t_recognize_controller_.28Steam_only.29) when needed.
* Updated compatibility list.
* Improved translations. Thanks to ChulkyBow (added Ukrainian)!
* For the web UI:
* Added log instructions for Xbox app on Windows.
* Added log download option.
* Redesigned log instruction UI.
* Fixed log parser not correctly handling multiple mods having the exact same name.
* Fixed JSON validator not recognizing manifest [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys).
## 3.13.2
Released 05 December 2021 for Stardew Valley 1.5.5 or later.
* For players:
* You no longer need .NET 5 to install or use SMAPI.
* The installer now detects when the game folder contains an incompatible legacy game version.
* Updated for the latest Stardew Valley 1.5.5 hotfix.
* Updated compatibility list.
* For the web UI:
* Fixed the JSON validator marking `.fnt` files invalid in Content Patcher files.
* For SMAPI maintainers:
* Added [release package scripts](technical/smapi.md) to streamline preparing SMAPI releases.
## 3.13.1
Released 30 November 2021 for Stardew Valley 1.5.5 or later.
* For players:
* Improved .NET 5 validation in Windows installer to better explain how to get the right version.
* Fixed installer failing on Windows when run from the game folder.
## 3.13.0
Released 30 November 2021 for Stardew Valley 1.5.5 or later. See [release highlights](https://www.patreon.com/posts/59348226).
* For players:
* Updated for Stardew Valley 1.5.5.
* Added `set_farm_type` [console command](https://stardewvalleywiki.com/Modding:Console_commands#Console_commands) to change the current farm type.
* Fixed installer window closing immediately if the installer crashed.
* Updated compatibility list.
* For mod authors:
* Migrated to 64-bit MonoGame and .NET 5 on all platforms (see [migration guide for mod authors](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5)).
* Added support for [map overlays via `asset.AsMap().PatchMap`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content#Edit_a_map).
* Added support for loading BmFont `.fnt` files for [custom languages](https://stardewvalleywiki.com/Modding:Custom_languages) through the [content API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content).
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.24.0.
**Update note for players with older systems:**
The game now has two branches: the _main branch_ which you'll get by default, and an optional
[_compatibility branch_ for older systems](https://www.stardewvalley.net/compatibility/). The two
branches have identical content, but use [different technologies](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5#Game_compatibility_branch).
Unfortunately **SMAPI only supports the main branch of the game**. There are formidable difficulties
across all mods in supporting all three variations, 32-bit imposes significant restrictions on what
mods can do, and the [Steam hardware stats](https://store.steampowered.com/hwsurvey) show that 99.69%
of players now have 64-bit.
## 3.12.8
Released 18 October 2021 for Stardew Valley 1.5.4.
* For players:
* Fixed mod edits to the farmhouse shifting the player down one tile in some cases.
* Fixed map tile rotations/flips not working for farmhands in split-screen mode.
* Improved translations. Thanks to ellipszist (added Thai) and Zangorr (added Polish)!
_These are custom languages which require Stardew Valley 1.5.5 and the [Polish](https://www.nexusmods.com/stardewvalley/mods/3616) or [Thai](https://www.nexusmods.com/stardewvalley/mods/7052) mod._
* For mod authors:
* SMAPI now intercepts dictionary duplicate-key errors and adds the key to the error message to simplify troubleshooting. (Due to Harmony limitations, this only works for the dictionary types used by the game.)
* Fixed barn/coop exit warps being reset when you edit their interior map.
* For the web UI:
* Added support for unified [mod data overrides](https://stardewvalleywiki.com/Modding:Mod_compatibility#Mod_data_overrides) defined on the wiki.
* The mod compatibility list now shows separate beta stats when 'show advanced info' is enabled.
## 3.12.7
Released 18 September 2021 for Stardew Valley 1.5.4.
* For players:
* Added more progress updates in the log during startup.
* Simplified asset load error message.
* Simplified exception logs.
* Fixed crash loading mods with corrupted translation files.
* For mod authors:
* Added asset propagation for `LooseSprites\Giftbox`.
* Improved SMAPI's crossplatform read/writing of `Color`, `Point`, `Rectangle`, and `Vector2` in JSON to support nullable fields too.
* For the web UI:
* The mod compatibility list now shows the beta status by default (if any).
* Fixed JSON validator line numbers sometimes incorrect.
## 3.12.6
Released 03 September 2021 for Stardew Valley 1.5.4.
* For players:
* Added friendly error when using SMAPI 3.2._x_ with Stardew Valley 1.5.5 or later.
* Improved mod compatibility in 64-bit mode (thanks to spacechase0!).
* Reduced load time when scanning/rewriting many mods for compatibility.
* **Dropped support for unofficial 64-bit mode**. You can now use the [official 64-bit Stardew Valley 1.5.5 beta](https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows) instead.
* Updated compatibility list.
* For mod authors:
* Added `PathUtilities.NormalizeAssetName` and `PathUtilities.PreferredAssetSeparator` to prepare for the upcoming Stardew Valley 1.5.5.
* **SMAPI no longer propagates changes to `Data/Bundles`.**
_You can still load/edit the asset like usual, but if bundles have already been loaded for a save, SMAPI will no longer dynamically update the in-game bundles to reflect the changes. Unfortunately this caused bundle corruption when playing in non-English._
* Fixed content packs created via `helper.ContentPacks.CreateFake` or `CreateTemporary` not initializing translations correctly.
* For console commands:
* Added `hurry_all` command which immediately warps all NPCs to their scheduled positions.
**Update note for mod authors:**
Stardew Valley 1.5.5 will change how asset names are formatted. If you use `PathUtilities.NormalizePath`
to format asset names, you should switch to `PathUtilities.NormalizeAssetName` now so your code will
continue working in the next game update.
## 3.12.5
Released 26 August 2021 for Stardew Valley 1.5.4.
* Fixed some mods in unofficial 64-bit mode no longer loading after SMAPI 3.12.3.
## 3.12.4
Released 25 August 2021 for Stardew Valley 1.5.4.
* For players:
* Fixed error loading some mods in SMAPI 3.12.3.
## 3.12.3
Released 25 August 2021 for Stardew Valley 1.5.4.
* For players:
* Added friendly error in 64-bit mode when a mod is 32-bit only.
* Fixed console encoding issues on Linux/macOS.
* Fixed some installer errors not showing info header.
* For mod authors:
* Added `helper.Translation.GetInAllLocales` to get a translation in every available locale.
* Fixed Visual Studio debugger crash when any mods are rewritten for compatibility (thanks to spacechase0!).
* Fixed `helper.Data.WriteJsonFile` not deleting the file if the model is null, unlike the other `Write*` methods.
* Fixed error-handling for `StackOverflowException` thrown on Linux/macOS.
* Internal changes to prepare for Stardew Valley 1.5.5.
* For the web API:
* Fixed update checks not shown for prerelease mod versions when you have a SMAPI beta.
* Fixed update checks shown for prerelease mod versions if you have a working non-prerelease version.
## 3.12.2
Released 05 August 2021 for Stardew Valley 1.5.4.
* For players:
* Fixed error creating a new save or joining a multiplayer world in 3.12.1.
* For mod authors:
* Reverted the `Constants.Save*` fix in SMAPI 3.12.1.
_The change caused a number of other issues, and is only needed for rare cases where the save folder was invalid. This may be revisited in a future version instead._
* Fixed `NullReferenceException` in SMAPI's error-handling when trying to handle an invalid `ReflectionTypeLoadException`.
## 3.12.1
Released 03 August 2021 for Stardew Valley 1.5.4.
* For players:
* The software conflict message is now shown as a warning to simplify troubleshooting.
* Fixed error loading older Harmony mods for some Windows players using unofficial 64-bit Stardew Valley.
* Updated compatibility list.
* For mod authors:
* Fixed `Constants.Save*` fields incorrect if the save's folder name and ID don't match.
## 3.12.0
Released 01 August 2021 for Stardew Valley 1.5.4. See [release highlights](https://www.patreon.com/posts/54388616).
* For players:
* Added save recovery when content mods leave null objects in the save (in _Error Handler_).
* Added error if the wrong SMAPI bitness is installed (e.g. 32-bit SMAPI with 64-bit game).
* Added error if some SMAPI files aren't updated correctly.
* Added `removable` option to the `world_clear` console command (in _Console Commands_, thanks to bladeoflight16!).
* Fixed handling of Unicode characters in console commands.
* Fixed intermittent error if a mod gets mod-provided APIs asynchronously.
* Fixed crash when creating a farm name containing characters that aren't allowed in a folder path.
* For mod authors:
* **Updated Harmony 1.2.0.1 to 2.1.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).**
* SMAPI now intercepts `KeyNotFoundException` errors and adds the key to the error message to simplify troubleshooting. (Due to Harmony limitations, this only works for the dictionary types used by the game.)
* Fixed error loading `.xnb` files from the local mod folder.
* Fixed reloading a map not correctly reapplying interior doors.
## 3.11.0
Released 09 July 2021 for Stardew Valley 1.5.4. See [release highlights](https://www.patreon.com/posts/53514295).
* For players:
* Updated for Stardew Valley 1.4.5 multiplayer hotfix on Linux/macOS.
* Fixed installer error on Windows when running as administrator (thanks to LostLogic!).
* Fixed installer error on some Windows systems (thanks to eddyballs!).
* Fixed error if SMAPI fails to dispose on game exit.
* Fixed `player_add` and `list_items` console commands not including some shirts _(in Console Commands)_.
* For mod authors:
* Added `World.FurnitureListChanged` event (thanks to DiscipleOfEris!).
* Added asset propagation for building/house paint masks.
* Added log message for troubleshooting if Windows software which often causes issues is installed (currently MSI Afterburner and RivaTuner).
* Improved validation for the manifest `Dependencies` field.
* Fixed validation for mods with invalid version `0.0.0`.
* Fixed _loaded with custom settings_ trace log added when using default settings.
* Fixed `Constants.SaveFolderName` and `Constants.CurrentSavePath` not set correctly in rare cases.
* For the web UI and JSON validator:
* Updated the JSON validator/schema for Content Patcher 1.23.0.
* Fixed [JSON schema](technical/web.md#using-a-schema-file-directly) in Visual Studio Code warning about comments and trailing commas.
* Fixed JSON schema for `i18n` files requiring the wrong value for the `$schema` field.
## 3.10.1
Released 03 May 2021 for Stardew Valley 1.5.4.
* For players:
* Fixed installer leaving an unneeded `StardewModdingAPI-x64.exe` file in 32-bit game folders.
## 3.10
Released 03 May 2021 for Stardew Valley 1.5.4. See [release highlights](https://www.patreon.com/posts/50764911).
* For players:
* Added full support for the [unofficial 64-bit Stardew Valley patch](https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows), which removes memory limits. The installer detects which version of SMAPI you need, and SMAPI shows update alerts for Stardew64Installer if applicable.
* Added smarter grouping for skipped mods, so it's easier to see root dependencies to update first.
* Added crash recovery when the game can't update a map's seasonal tilesheets _(in Error Handler)_. SMAPI will log an error and keep the previous tilesheets in that case.
* Added installer option to enter a custom game path even if it detected a game folder.
* `*.ico` files are now ignored when scanning for mods.
* Fixed error for non-English players after returning to title, reloading, and entering town with a completed movie theater.
* Fixed `world_clear` console command not removing resource clumps outside the farm and secret woods.
* Fixed error running SMAPI in a strict sandbox on Linux (thanks to kuesji!).
* Fixed `StardewModdingAPI.bin.osx` on macOS overwritten with an identical file on launch which would reset file permissions (thanks to 007wayne!).
* Fixed inconsistent spelling/style for 'macOS'.
* For modders:
* Added support for [ignoring local map tilesheet files when loading a map](https://stardewvalleywiki.com/Modding:Maps#Local_copy_of_a_vanilla_tilesheet).
* Added asset propagation for `Data\Concessions`.
* Added SMAPI version and bitness to the console title before startup to simplify troubleshooting.
* If a map loads a tilesheet path with no file extension, SMAPI now automatically links it to a `.png` version in the map folder if possible.
* Improved error-handling during asset propagation.
* Fixed `Context.IsMainPlayer` returning true for a farmhand in split-screen mode before the screen is initialized.
* Fixed error when editing bundle data while a split-screen player is joining.
* Fixed update subkeys not working in file descriptions for Nexus mods marked as adult content.
## 3.9.5
Released 21 March 2021 for Stardew Valley 1.5.4.
* For players:
* Added console command to reset community center bundles _(in Console Commands)_.
* Disabled aggressive memory optimization by default.
_The option was added in SMAPI 3.9.2 to reduce errors for some players, but it can cause multiplayer crashes with some mods. If you often see `OutOfMemoryException` errors, you can edit `smapi-internal/config.json` to re-enable it. We're experimenting with making Stardew Valley 64-bit to address memory issues more systematically._
* Fixed bundles corrupted in non-English saves created after SMAPI 3.9.2.
_If you have an affected save, you can load your save and then enter the `regenerate_bundles confirm` command in the SMAPI console to fix it._
* Internal changes to prepare for unofficial 64-bit.
* For mod authors:
* Improved asset propagation:
* Added for interior door sprites.
* SMAPI now updates the NPC pathfinding cache when map warps are changed through the content API.
* Reduced performance impact of invalidating cached assets before a save is loaded.
* Fixed asset changes not reapplied in the edge case where you're playing in non-English, and the changes are only applied after the save is loaded, and the player returns to title and reloads a save, and the game reloads the target asset before the save is loaded.
* Added a second `KeybindList` constructor to simplify single-key default bindings.
* Added a `Constants.GameFramework` field which indicates whether the game is using XNA Framework or MonoGame.
_Note: mods don't need to handle the difference in most cases, but some players may use MonoGame on Windows in upcoming versions. Mods which check `Constants.TargetPlatform` should review usages as needed._
## 3.9.4
Released 07 March 2021 for Stardew Valley 1.5.4.
* For players:
* Fixed installer error if the `Mods` folder doesn't exist in 3.9.3.
## 3.9.3
Released 07 March 2021 for Stardew Valley 1.5.4.
* For players:
* Added descriptive error if possible when a `PathTooLongException` crashes SMAPI or the installer.
* The installer window now tries to stay open if it crashed, so you can read the error and ask for help.
* Fixed console showing _found 1 mod with warnings_ with no mods listed in some cases.
* For mod authors:
* Added three stages to the specialised [`LoadStageChanged` event](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Specialised): `CreatedInitialLocations`, `SaveAddedLocations`, and `ReturningToTitle`.
* Fixed `RewriteMods` option ignored when rewriting for OS compatibility.
* Fixed edge case when playing as a farmhand in non-English where translatable assets loaded via `IAssetLoader` weren't reapplied immediately when the server disconnects.
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.21.
## 3.9.2
Released 21 February 2021 for Stardew Valley 1.5.4.
* For players:
* Added more aggressive memory optimization to reduce `OutOfMemoryException` errors with some mods.
* Improved error when `Stardew Valley.exe` exists but can't be loaded.
* Fixed error running `install on Windows.bat` in very rare cases.
* Fixed `world_settime` command not always updating outdoor ambient lighting _(in Console Commands)_.
* For mod authors:
* Added early detection of disposed textures so the error details are more relevant _(in Error Handler)_.
* Added error details when an event command fails _(in Error Handler)_.
* Fixed asset propagation for `TileSheets/ChairTiles` not changing existing map seats.
* Fixed edge case when playing in non-English where translatable assets loaded via `IAssetLoader` would no longer be applied after returning to the title screen unless manually invalidated from the cache.
* For the web UI:
* Updated compatibility list for the new wiki.
* Updated the JSON validator/schema for Content Patcher 1.20.
* Fixed mod compatibility list error if a mod has no name.
* For SMAPI developers:
* Fixed SMAPI toolkit defaulting the mod type incorrectly if a mod's `manifest.json` has neither `EntryDll` nor `ContentPackFor`. This only affects external tools, since SMAPI itself validates those fields separately.
## 3.9.1
Released 25 January 2021 for Stardew Valley 1.5.4.
* For players:
* Fixed _tile contains an invalid TileSheet reference_ crash after mods change certain maps.
* Fixed _patched game code_ issue shown for the bundled Error Handler mod.
## 3.9
Released 22 January 2021 for Stardew Valley 1.5.4. See [release highlights](https://www.patreon.com/posts/46553874).
* For players:
* Updated for Stardew Valley 1.5.4.
* Improved game detection in the installer:
* The installer now prefers paths registered by Steam or GOG Galaxy.
* The installer now detects default manual GOG installs.
* Added clearer error text for empty mod folders created by Vortex.
* Fixed the game's map changes not always reapplied correctly after mods change certain maps, which caused issues like the community center resetting to its non-repaired texture.
* Fixed compatibility for very old content packs which still load maps from `.xnb` files. These were broken by map loading changes in Stardew Valley 1.5, but SMAPI now corrects them automatically.
* Fixed some broken mods incorrectly listed as XNB mods under 'skipped mods'.
* For mod authors:
* Added new input APIs:
* Added an [API for multi-key bindings](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList).
* Added a new [`Input.ButtonsChanged` event](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged).
* Added a `buttonState.IsDown()` extension.
* Added a `helper.Input.SuppressActiveKeybinds` method to suppress the active buttons in a keybind list.
* Improved multiplayer APIs:
* `PerScreen<T>` now lets you get/set the value for any screen, get all active values, or clear all values.
* Peer data from the multiplayer API/events now includes `IsSplitScreen` and `ScreenID` fields.
* Fixed network messages through the multiplayer API being sent to players who don't have SMAPI installed in some cases.
* Improved asset propagation:
* Updated map propagation for the changes in Stardew Valley 1.5.4.
* Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names).
* Fixed quarry bridge not fixed if the mountain map was reloaded.
* Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This prevents older mods from loading, but bypasses a Visual Studio crash when debugging.
* Game errors shown in the chatbox are now logged.
* Moved vanilla error-handling into a new Error Handler mod. This simplifies the core SMAPI logic, and lets users disable it if needed.
* For the Console Commands mod:
* Removed the `inf` option for `player_sethealth`, `player_setmoney`, and `player_setstamina`. You can use mods like [CJB Cheats Menu](https://www.nexusmods.com/stardewvalley/mods/4) instead for that.
* For the Error Handler mod:
* Added a detailed message for the _Input string was not in a correct format_ error when the game fails to parse an item text description.
* For the web UI:
* Fixed JSON validator incorrectly marking some manifest update keys as invalid.
## 3.8.4
Released 15 January 2021 for Stardew Valley 1.5.3 or later.
* For players:
* Updated for Stardew Valley 1.5.3.
* Fixed issue where title screen music didn't stop after loading a save.
* For mod authors:
* Fixed `SemanticVersion` comparisons returning wrong value in rare cases.
## 3.8.3
Released 08 January 2021 for Stardew Valley 1.5.2 or later.
* For players:
* Updated for Stardew Valley 1.5.2.
* Reduced memory usage.
* You can now enter console commands for a specific screen in split-screen mode by adding `screen=ID` to the command.
* Typing `help` in the SMAPI console is now more helpful.
* For mod authors:
* Simplified tilesheet order warning added in SMAPI 3.8.2.
* For the Console Commands mod:
* Removed experimental `performance` command. Unfortunately this impacted SMAPI's memory usage and performance, and the data was often misinterpreted. This may be replaced with more automatic performance alerts in a future version.
## 3.8.2
Released 03 January 2021 for Stardew Valley 1.5.1 or later.
* For players:
* SMAPI now blocks farm map replacements that would crash the game in Stardew Valley 1.5.
* On Linux, the SMAPI installer now auto-detects Flatpak Steam paths.
* Updated compatibility list.
* Fixed errors when multiple players join in split-screen mode.
* Fixed 'skipped mods' section repeating mods in some cases.
* Fixed out-of-date error text.
* For mod authors:
* Added warning when a map replacement changes the order/IDs of the original tilesheets, which may cause errors and crashes. Doing so for a farm map is blocked outright since that causes a consistent crash in Stardew Valley 1.5.
* Message data from the `ModMessageReceived` event now uses the same serializer settings as the rest of SMAPI. That mainly adds support for sending crossplatform `Color`, `Point`, `Vector2`, `Rectangle`, and `SemanticVersion` fields through network messages.
* When a mod is blocked by SMAPI's compatibility override list, the `TRACE` messages while loading it now say so and indicate why.
* Fixed how the input API handles UI scaling. This mainly affects `ICursorPosition` values returned by the API; see [the wiki docs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#ICursorPosition) for how to account for UI scaling.
## 3.8.1
Released 26 December 2020 for Stardew Valley 1.5.1 or later.
* For players:
* Fixed broken community center bundles for non-English saves created in Stardew Valley 1.5. Affected saves will be fixed automatically on load.
* For mod authors:
* World events are now raised for volcano dungeon levels.
* Added `apply_save_fix` command to reapply a save migration in exceptional cases. This should be used very carefully. Type `help apply_save_fix` for details.
* **Deprecation notice:** the `Helper.ConsoleCommands.Trigger` method is now deprecated and should no longer be used. See [integration APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations) for better mod integration options. It will eventually be removed in SMAPI 4.0.
For the web UI:
* Fixed edge cases in SMAPI log parsing.
## 3.8
Released 21 December 2020 for Stardew Valley 1.5 or later. See [release highlights](https://www.patreon.com/posts/45294737).
* For players:
* Updated for Stardew Valley 1.5, including split-screen support.
* You can now run the installer from a subfolder of your game folder to auto-detect it. That simplifies installation if you have multiple copies of the game or it can't otherwise auto-detect the game path.
* Clarified error when the SMAPI installer is in the `Mods` folder.
* For mod authors:
* Added `PerScreen<T>` utility and new `Context` fields to simplify split-screen support in mods.
* Added screen ID to log when playing in split-screen mode.
* For the Console Commands mod:
* Added `furniture` option to `world_clear`.
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.19.
## 3.7.6 ## 3.7.6
Released 21 November 2020 for Stardew Valley 1.4.1 or later. Released 21 November 2020 for Stardew Valley 1.4.1 or later.
@ -14,7 +762,7 @@ Released 21 November 2020 for Stardew Valley 1.4.1 or later.
* Fixed error when heuristically rewriting an outdated mod in rare cases. * Fixed error when heuristically rewriting an outdated mod in rare cases.
* Fixed rare 'collection was modified' error when using `harmony summary` console command. * Fixed rare 'collection was modified' error when using `harmony summary` console command.
* For modders: * For mod authors:
* Updated TMXTile to 1.5.8 to fix exported `.tmx` files losing tile index properties. * Updated TMXTile to 1.5.8 to fix exported `.tmx` files losing tile index properties.
* For the Console Commands mod: * For the Console Commands mod:
@ -24,7 +772,7 @@ Released 21 November 2020 for Stardew Valley 1.4.1 or later.
## 3.7.5 ## 3.7.5
Released 16 October 2020 for Stardew Valley 1.4.1 or later. Released 16 October 2020 for Stardew Valley 1.4.1 or later.
* For modders: * For mod authors:
* Fixed changes to the town map asset not reapplying the game's community center, JojaMart, and Pam house changes. * Fixed changes to the town map asset not reapplying the game's community center, JojaMart, and Pam house changes.
## 3.7.4 ## 3.7.4
@ -34,7 +782,7 @@ Released 03 October 2020 for Stardew Valley 1.4.1 or later.
* Improved performance on some older computers (thanks to millerscout!). * Improved performance on some older computers (thanks to millerscout!).
* Fixed update alerts for Chucklefish forum mods broken by a recent site change. * Fixed update alerts for Chucklefish forum mods broken by a recent site change.
* For modders: * For mod authors:
* Updated dependencies (including Mono.Cecil 0.11.2 → 0.11.3 and Platonymous.TMXTile 1.3.8 → 1.5.6). * Updated dependencies (including Mono.Cecil 0.11.2 → 0.11.3 and Platonymous.TMXTile 1.3.8 → 1.5.6).
* Fixed asset propagation for `Data\MoviesReactions`. * Fixed asset propagation for `Data\MoviesReactions`.
* Fixed error in content pack path handling when you pass a null path. * Fixed error in content pack path handling when you pass a null path.
@ -49,11 +797,11 @@ Released 03 October 2020 for Stardew Valley 1.4.1 or later.
Released 16 September 2020 for Stardew Valley 1.4.1 or later. Released 16 September 2020 for Stardew Valley 1.4.1 or later.
* For players: * For players:
* Fixed errors on Linux/Mac due to content packs with incorrect filename case. * Fixed errors on Linux/macOS due to content packs with incorrect filename case.
* Fixed map rendering crash due to conflict between SMAPI and PyTK. * Fixed map rendering crash due to conflict between SMAPI and PyTK.
* Fixed error in heuristically-rewritten mods in rare cases (thanks to collaboration with ZaneYork!). * Fixed error in heuristically-rewritten mods in rare cases (thanks to collaboration with ZaneYork!).
* For modders: * For mod authors:
* File paths accessed through `IContentPack` are now case-insensitive (even on Linux). * File paths accessed through `IContentPack` are now case-insensitive (even on Linux).
* For the web UI: * For the web UI:
@ -65,7 +813,7 @@ Released 08 September 2020 for Stardew Valley 1.4.1 or later.
* For players: * For players:
* Fixed mod recipe changes not always applied in 3.7. * Fixed mod recipe changes not always applied in 3.7.
* For modders: * For mod authors:
* Renamed `PathUtilities.NormalizePathSeparators` to `NormalizePath`, and added normalization for more cases. * Renamed `PathUtilities.NormalizePathSeparators` to `NormalizePath`, and added normalization for more cases.
## 3.7.1 ## 3.7.1
@ -90,10 +838,10 @@ Released 07 September 2020 for Stardew Valley 1.4.1 or later. See [release highl
* Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled.
* Internal changes to prepare for upcoming game updates. * Internal changes to prepare for upcoming game updates.
* For modders: * For mod authors:
* Added `PathUtilities` to simplify working with file/asset names. * Added `PathUtilities` to simplify working with file/asset names.
* You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc).
* Fixed asset propagation not updating title menu buttons immediately on Linux/Mac. * Fixed asset propagation not updating title menu buttons immediately on Linux/macOS.
* For the web UI: * For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.16 and 1.17. * Updated the JSON validator/schema for Content Patcher 1.16 and 1.17.
@ -120,7 +868,7 @@ Released 02 August 2020 for Stardew Valley 1.4.1 or later.
* Fixed spawned Floor TV not functional as a TV (thanks to Platonymous!). * Fixed spawned Floor TV not functional as a TV (thanks to Platonymous!).
* Fixed spawned sturgeon roe having incorrect color. * Fixed spawned sturgeon roe having incorrect color.
* For modders: * For mod authors:
* Updated internal dependencies. * Updated internal dependencies.
* SMAPI now ignores more file types when scanning for mod folders (`.doc`, `.docx`, `.rar`, and `.zip`). * SMAPI now ignores more file types when scanning for mod folders (`.doc`, `.docx`, `.rar`, and `.zip`).
* Added current GPU to trace logs to simplify troubleshooting. * Added current GPU to trace logs to simplify troubleshooting.
@ -138,7 +886,7 @@ Released 20 June 2020 for Stardew Valley 1.4.1 or later. See [release highlights
* Added experimental option to reduce startup time when loading mod DLLs (thanks to ZaneYork!). Enable `RewriteInParallel` in the `smapi-internal/config.json` to try it. * Added experimental option to reduce startup time when loading mod DLLs (thanks to ZaneYork!). Enable `RewriteInParallel` in the `smapi-internal/config.json` to try it.
* Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!). * Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!).
* Mod load warnings are now listed alphabetically. * Mod load warnings are now listed alphabetically.
* MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * macOS files starting with `._` are now ignored and can no longer cause skipped mods.
* Simplified paranoid warning logs and reduced their log level. * Simplified paranoid warning logs and reduced their log level.
* Fixed black maps on Android for mods which use `.tmx` files. * Fixed black maps on Android for mods which use `.tmx` files.
* Fixed `BadImageFormatException` error detection. * Fixed `BadImageFormatException` error detection.
@ -153,7 +901,7 @@ Released 20 June 2020 for Stardew Valley 1.4.1 or later. See [release highlights
* Updated ModDrop URLs. * Updated ModDrop URLs.
* Internal changes to improve performance and reliability. * Internal changes to improve performance and reliability.
* For modders: * For mod authors:
* Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!).
* Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys).
* Added [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs. * Added [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs.
@ -188,13 +936,13 @@ Released 27 April 2020 for Stardew Valley 1.4.1 or later. See [release highlight
* Updated the JSON validator/schema for Content Patcher 1.13. * Updated the JSON validator/schema for Content Patcher 1.13.
* Fixed rare intermittent "CGI application encountered an error" errors. * Fixed rare intermittent "CGI application encountered an error" errors.
* For modders: * For mod authors:
* Added map patching to the content API (via `asset.AsMap()`). * Added map patching to the content API (via `asset.AsMap()`).
* Added support for using patch helpers with arbitrary data (via `helper.Content.GetPatchHelper`). * Added support for using patch helpers with arbitrary data (via `helper.Content.GetPatchHelper`).
* Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!).
* Added `SDate` translations taken from the Lookup Anything mod.¹ * Added `SDate` translations taken from the Lookup Anything mod.¹
* Fixed asset propagation for certain maps loaded through temporary content managers. This notably fixes unreliable patches to the farmhouse and town maps. * Fixed asset propagation for certain maps loaded through temporary content managers. This notably fixes unreliable patches to the farmhouse and town maps.
* Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. * Fixed asset propagation on Linux/macOS for monster sprites, NPC dialogue, and NPC schedules.
* Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses.
¹ Date format translations were taken from the Lookup Anything mod; thanks to translators FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Some translations for Korean, Hungarian, and Turkish were derived from the game translations. ¹ Date format translations were taken from the Lookup Anything mod; thanks to translators FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Some translations for Korean, Hungarian, and Turkish were derived from the game translations.
@ -202,7 +950,7 @@ Released 27 April 2020 for Stardew Valley 1.4.1 or later. See [release highlight
## 3.4.1 ## 3.4.1
Released 24 March 2020 for Stardew Valley 1.4.1 or later. Released 24 March 2020 for Stardew Valley 1.4.1 or later.
* For modders: * For mod authors:
* Asset changes now propagate to NPCs in an event (e.g. wedding sprites). * Asset changes now propagate to NPCs in an event (e.g. wedding sprites).
* Fixed mouse input suppression not working in SMAPI 3.4. * Fixed mouse input suppression not working in SMAPI 3.4.
@ -210,12 +958,12 @@ Released 24 March 2020 for Stardew Valley 1.4.1 or later.
Released 22 March 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/35161371). Released 22 March 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/35161371).
* For players: * For players:
* Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows). * Fixed semi-transparency issues on Linux/macOS in recent versions of Mono (e.g. pink shadows).
* Fixed `player_add` command error if you have broken XNB mods. * Fixed `player_add` command error if you have broken XNB mods.
* Removed invalid-location check now handled by the game. * Removed invalid-location check now handled by the game.
* Updated translations. Thanks to Annosz (added Hungarian)! * Updated translations. Thanks to Annosz (added Hungarian)!
* For modders: * For mod authors:
* Added support for flipped and rotated map tiles (thanks to collaboration with Platonymous!). * Added support for flipped and rotated map tiles (thanks to collaboration with Platonymous!).
* Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!).
* Added `this.Monitor.LogOnce` method. * Added `this.Monitor.LogOnce` method.
@ -252,14 +1000,14 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later. See [release highli
* Updated translations. Thanks to xCarloC (added Italian)! * Updated translations. Thanks to xCarloC (added Italian)!
* For the Save Backup mod: * For the Save Backup mod:
* Fixed warning on MacOS when you have no saves yet. * Fixed warning on macOS when you have no saves yet.
* Reduced log messages. * Reduced log messages.
* For the web UI: * For the web UI:
* Updated the JSON validator and Content Patcher schema for `.tmx` support. * Updated the JSON validator and Content Patcher schema for `.tmx` support.
* The mod compatibility page now has a sticky table header. * The mod compatibility page now has a sticky table header.
* For modders: * For mod authors:
* Added support for [message sending](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Message_sending) to mods on the current computer (in addition to remote computers). * Added support for [message sending](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Message_sending) to mods on the current computer (in addition to remote computers).
* Added `ExtendImage` method to content API when editing files to resize textures. * Added `ExtendImage` method to content API when editing files to resize textures.
* Added `helper.Input.GetState` to get the low-level state of a button. * Added `helper.Input.GetState` to get the low-level state of a button.
@ -294,7 +1042,7 @@ Released 01 February 2020 for Stardew Valley 1.4.1 or later. See [release highli
* Fixed extra files under `Saves` (e.g. manual backups) not being ignored. * Fixed extra files under `Saves` (e.g. manual backups) not being ignored.
* Fixed Android issue where game files were backed up. * Fixed Android issue where game files were backed up.
* For modders: * For mod authors:
* Added support for `.tmx` map files. (Thanks to [Platonymous for the underlying library](https://github.com/Platonymous/TMXTile)!) * Added support for `.tmx` map files. (Thanks to [Platonymous for the underlying library](https://github.com/Platonymous/TMXTile)!)
* Added special handling for `Vector2` values in `.json` files, so they work consistently crossplatform. * Added special handling for `Vector2` values in `.json` files, so they work consistently crossplatform.
* Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on.
@ -343,7 +1091,7 @@ Released 05 January 2019 for Stardew Valley 1.4.1 or later. See [release highlig
* Fixed log parser not correctly handling content packs with no author (thanks to danvolchek!). * Fixed log parser not correctly handling content packs with no author (thanks to danvolchek!).
* Fixed main sidebar link pointing to wiki instead of home page. * Fixed main sidebar link pointing to wiki instead of home page.
* For modders: * For mod authors:
* Added `World.ChestInventoryChanged` event (thanks to collaboration with wartech0!). * Added `World.ChestInventoryChanged` event (thanks to collaboration with wartech0!).
* Added asset propagation for... * Added asset propagation for...
* grass textures; * grass textures;
@ -372,7 +1120,7 @@ Released 02 December 2019 for Stardew Valley 1.4 or later.
* If a log can't be uploaded to Pastebin (e.g. due to rate limits), it's now uploaded to Amazon S3 instead. Logs uploaded to S3 expire after one month. * If a log can't be uploaded to Pastebin (e.g. due to rate limits), it's now uploaded to Amazon S3 instead. Logs uploaded to S3 expire after one month.
* Fixed JSON validator not letting you drag & drop a file. * Fixed JSON validator not letting you drag & drop a file.
* For modders: * For mod authors:
* `SemanticVersion` now supports [semver 2.0](https://semver.org/) build metadata. * `SemanticVersion` now supports [semver 2.0](https://semver.org/) build metadata.
## 3.0 ## 3.0
@ -402,7 +1150,7 @@ For players:
* **Fixed many bugs and edge cases.** * **Fixed many bugs and edge cases.**
For modders: For mod authors:
* **New event system.** * **New event system.**
SMAPI 3.0 removes the deprecated static events in favor of the new `helper.Events` API. The event SMAPI 3.0 removes the deprecated static events in favor of the new `helper.Events` API. The event
engine is rewritten to make events more efficient, add events that weren't possible before, make engine is rewritten to make events more efficient, add events that weren't possible before, make
@ -486,7 +1234,7 @@ For modders:
* Added instructions for Android. * Added instructions for Android.
* The page now detects your OS and preselects the right instructions (thanks to danvolchek!). * The page now detects your OS and preselects the right instructions (thanks to danvolchek!).
### For modders ### For mod authors
* Breaking changes: * Breaking changes:
* Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialized when `Entry` is called; use the `GameLaunched` event if you need to run code when the game is initialized. * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialized when `Entry` is called; use the `GameLaunched` event if you need to run code when the game is initialized.
* Removed all deprecated APIs. * Removed all deprecated APIs.

View File

@ -1,7 +1,7 @@
&larr; [SMAPI](../README.md) &larr; [SMAPI](../README.md)
The **mod build package** is an open-source NuGet package which automates the MSBuild configuration The **mod build package** is an open-source NuGet package which automates the MSBuild configuration
for SMAPI mods and related tools. The package is fully compatible with Linux, Mac, and Windows. for SMAPI mods and related tools. The package is fully compatible with Linux, macOS, and Windows.
## Contents ## Contents
* [Use](#use) * [Use](#use)
@ -29,20 +29,19 @@ change how these work):
* **Detect game path:** * **Detect game path:**
The package automatically finds your game folder by scanning the default install paths and The package automatically finds your game folder by scanning the default install paths and
Windows registry. It adds two MSBuild properties for use in your `.csproj` file if needed: Windows registry. It adds two MSBuild properties for use in your `.csproj` file if needed:
`$(GamePath)` and `$(GameExecutableName)`. `$(GamePath)` and `$(GameModsPath)`.
* **Add assembly references:** * **Add assembly references:**
The package adds assembly references to SMAPI, Stardew Valley, xTile, and the game framework The package adds assembly references to MonoGame, SMAPI, Stardew Valley, and xTile. It
(MonoGame on Linux/Mac, XNA Framework on Windows). It automatically adjusts depending on which OS automatically adjusts depending on which OS you're compiling it on. If you use
you're compiling it on. If you use [Harmony](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Harmony), [Harmony](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Harmony), it can optionally add
it can optionally add a reference to that too. a reference to that too.
* **Copy files into the `Mods` folder:** * **Copy files into the `Mods` folder:**
The package automatically copies your mod's DLL and PDB files, `manifest.json`, [`i18n` The package automatically copies your mod's DLL and PDB files, `manifest.json`, [`i18n`
files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder (if files](https://stardewvalleywiki.com/Modding:Translations) (if any), and the `assets` folder (if
any), and [build output](https://stackoverflow.com/a/10828462/262123) into your game's `Mods` any) into the `Mods` folder when you rebuild the code, with a subfolder matching the mod's project
folder when you rebuild the code, with a subfolder matching the mod's project name. That lets you name. That lets you try the mod in-game right after building it.
try the mod in-game right after building it.
* **Create release zip:** * **Create release zip:**
The package adds a zip file in your project's `bin` folder when you rebuild the code, in the The package adds a zip file in your project's `bin` folder when you rebuild the code, in the
@ -55,7 +54,7 @@ change how these work):
breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019) breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019)
in your code while the game is running, or [make simple changes to the mod code without needing to in your code while the game is running, or [make simple changes to the mod code without needing to
restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019). restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019).
This is disabled on Linux/Mac due to limitations with the Mono wrapper. This is disabled on Linux/macOS due to limitations with the Mono wrapper.
* **Preconfigure common settings:** * **Preconfigure common settings:**
The package automatically enables `.pdb` files (so error logs show line numbers to simplify The package automatically enables `.pdb` files (so error logs show line numbers to simplify
@ -82,7 +81,7 @@ There are two places you can put them:
1. Open the home folder on your computer (see instructions for 1. Open the home folder on your computer (see instructions for
[Linux](https://superuser.com/questions/409218/where-is-my-users-home-folder-in-ubuntu), [Linux](https://superuser.com/questions/409218/where-is-my-users-home-folder-in-ubuntu),
[MacOS](https://www.cnet.com/how-to/how-to-find-your-macs-home-folder-and-add-it-to-finder/), [macOS](https://www.cnet.com/how-to/how-to-find-your-macs-home-folder-and-add-it-to-finder/),
or [Windows](https://www.computerhope.com/issues/ch000109.htm)). or [Windows](https://www.computerhope.com/issues/ch000109.htm)).
2. Create a `stardewvalley.targets` file with this content: 2. Create a `stardewvalley.targets` file with this content:
```xml ```xml
@ -129,14 +128,6 @@ The absolute path to the folder containing the game's installed mods (defaults t
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>GameExecutableName</code></td>
<td>
The filename for the game's executable (i.e. `StardewValley.exe` on Linux/Mac or
`Stardew Valley.exe` on Windows). This is auto-detected, and you should almost never change this.
</td>
</tr>
</table> </table>
</li> </li>
@ -197,11 +188,63 @@ The folder path where the release zip is created (defaults to the project's `bin
<th>effect</th> <th>effect</th>
</tr> </tr>
<tr> <tr>
<td><code>CopyModReferencesToBuildOutput</code></td> <td><code>BundleExtraAssemblies</code></td>
<td> <td>
Whether to copy game and framework DLLs into the mod folder (default `false`). This is useful for **Most mods should not change this option.**
unit test projects, but not needed for mods that'll be run through SMAPI.
By default (when this is _not_ enabled), only the mod files [normally considered part of the
mod](#Features) will be added to the release `.zip` and copied into the `Mods` folder (i.e.
"deployed"). That includes the assembly files (`*.dll`, `*.pdb`, and `*.xml`) for your mod project,
but any other DLLs won't be deployed.
Enabling this option will add _all_ dependencies to the build output, then deploy _some_ of them
depending on the comma-separated value(s) you set:
<table>
<tr>
<th>option</th>
<th>result</th>
</tr>
<tr>
<td><code>ThirdParty</code></td>
<td>
Assembly files which don't match any other category.
</td>
</tr>
<tr>
<td><code>System</code></td>
<td>
Assembly files whose names start with `Microsoft.*` or `System.*`.
</td>
</tr>
<tr>
<td><code>Game</code></td>
<td>
Assembly files which are part of MonoGame, SMAPI, or Stardew Valley.
</td>
</tr>
<tr>
<td><code>All</code></td>
<td>
Equivalent to `System, Game, ThirdParty`.
</td>
</tr>
</table>
Most mods should omit the option. Some mods may need `ThirdParty` if they bundle third-party DLLs
with their mod. The other options are mainly useful for unit tests.
When enabling this option, you should **manually review which files get deployed** and use the
`IgnoreModFilePaths` or `IgnoreModFilePatterns` options to exclude files as needed.
</td> </td>
</tr> </tr>
@ -213,6 +256,20 @@ Whether to configure the project so you can launch or debug the game through the
Visual Studio (default `true`). There's usually no reason to change this, unless it's a unit test Visual Studio (default `true`). There's usually no reason to change this, unless it's a unit test
project. project.
</td>
</tr>
<tr>
<td><code>IgnoreModFilePaths</code></td>
<td>
A comma-delimited list of literal file paths to ignore, relative to the mod's `bin` folder. Paths
are case-sensitive, but path delimiters are normalized automatically. For example, this ignores a
set of tilesheets:
```xml
<IgnoreModFilePaths>assets/paths.png, assets/springobjects.png</IgnoreModFilePaths>
```
</td> </td>
</tr> </tr>
<tr> <tr>
@ -291,6 +348,15 @@ Warning text:
Your code accesses a field which is obsolete or no longer works. Use the suggested field instead. Your code accesses a field which is obsolete or no longer works. Use the suggested field instead.
### Wrong processor architecture
Warning text:
> The target platform should be set to 'Any CPU' for compatibility with both 32-bit and 64-bit
> versions of Stardew Valley (currently set to '{{current platform}}').
Mods can be used in either 32-bit or 64-bit mode. Your project's target platform isn't set to the
default 'Any CPU', so it won't work in both. You can fix it by [setting the target platform to
'Any CPU'](https://docs.microsoft.com/en-ca/visualstudio/ide/how-to-configure-projects-to-target-platforms).
## FAQs ## FAQs
### How do I set the game path?<span id="custom-game-path"></span> ### How do I set the game path?<span id="custom-game-path"></span>
The package detects where your game is installed automatically, so you usually don't need to set it The package detects where your game is installed automatically, so you usually don't need to set it
@ -306,22 +372,21 @@ To do that:
<GamePath>PATH_HERE</GamePath> <GamePath>PATH_HERE</GamePath>
</PropertyGroup> </PropertyGroup>
``` ```
3. Replace `PATH_HERE` with your game's folder path. 3. Replace `PATH_HERE` with your game's folder path (don't add quotes).
The configuration will check your custom path first, then fall back to the default paths (so it'll The configuration will check your custom path first, then fall back to the default paths (so it'll
still compile on a different computer). still compile on a different computer).
### How do I change which files are included in the mod deploy/zip? ### How do I change which files are included in the mod deploy/zip?
For custom files, you can [add/remove them in the build output](https://stackoverflow.com/a/10828462/262123). * For normal files, you can [add/remove them in the build output](https://stackoverflow.com/a/10828462/262123).
(If your project references another mod, make sure the reference is [_not_ marked 'copy * For assembly files (`*.dll`, `*.exe`, `*.pdb`, or `*.xml`), see the
local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) [`BundleExtraAssemblies` option](#configure).
* To exclude a file which the package copies by default, see the [`IgnoreModFilePaths` or
To exclude a file the package copies by default, see `IgnoreModFilePatterns` under `IgnoreModFilePatterns` options](#configure).
[_configure_](#configure).
### Can I use the package for non-mod projects? ### Can I use the package for non-mod projects?
You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable Yep, this works in unit tests and framework projects too. Just disable the mod-related package
the mod-related package features (see [_configure_](#configure)): features (see [_configure_](#configure)):
```xml ```xml
<EnableGameDebugging>false</EnableGameDebugging> <EnableGameDebugging>false</EnableGameDebugging>
@ -329,9 +394,9 @@ the mod-related package features (see [_configure_](#configure)):
<EnableModZip>false</EnableModZip> <EnableModZip>false</EnableModZip>
``` ```
If you need to copy the referenced DLLs into your build output, add this too: To copy referenced DLLs into your build output for unit tests, add this too:
```xml ```xml
<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput> <BundleExtraAssemblies>All</BundleExtraAssemblies>
``` ```
## For SMAPI developers ## For SMAPI developers
@ -347,11 +412,63 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
when you compile it. when you compile it.
## Release notes ## Release notes
## 3.2.2 ### 4.1.0
Released 08 January 2023.
* Added `manifest.json` format validation on build (thanks to tylergibbs2!).
* Fixed game DLLs not excluded from the release zip when they're referenced explicitly but `BundleExtraAssemblies` isn't set.
### 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.
* Added detection for Xbox app game folders.
* Fixed "_conflicts between different versions of Microsoft.Win32.Registry_" warnings in recent SMAPI versions.
* Internal refactoring.
### 4.0.0
Released 30 November 2021.
* Updated for Stardew Valley 1.5.5 and SMAPI 3.13.0. (Older versions are no longer supported.)
* Added `IgnoreModFilePaths` option to ignore literal paths.
* Added `BundleExtraAssemblies` option to copy bundled DLLs into the mod zip/folder.
* Removed the `GameExecutableName` and `GameFramework` options (since they now have the same value
on all platforms).
* Removed the `CopyModReferencesToBuildOutput` option (superseded by `BundleExtraAssemblies`).
* Improved analyzer performance by enabling parallel execution.
**Migration guide for mod authors:**
1. See [_migrate to 64-bit_](https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows) and
[_migrate to Stardew Valley 1.5.5_](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5).
2. Possible changes in your `.csproj` or `.targets` files:
* Replace `$(GameExecutableName)` with `Stardew Valley`.
* Replace `$(GameFramework)` with `MonoGame` and remove any XNA Framework-specific logic.
* Replace `<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput>` with
`<BundleExtraAssemblies>Game</BundleExtraAssemblies>`.
* If you need to bundle extra DLLs besides your mod DLL, see the [`BundleExtraAssemblies`
documentation](#configure).
### 3.3.0
Released 30 March 2021.
* Added a build warning when the mod isn't compiled for `Any CPU`.
* Added a `GameFramework` build property set to `MonoGame` or `Xna` based on the platform. This can
be overridden to change which framework it references.
* Added support for building mods against the 64-bit Linux version of the game on Windows.
* The package now suppresses the misleading 'processor architecture mismatch' warnings.
### 3.2.2
Released 23 September 2020. Released 23 September 2020.
* Reworked and streamlined how the package is compiled. * Reworked and streamlined how the package is compiled.
* Added [SMAPI-ModTranslationClassBuilder](https://github.com/Pathoschild/SMAPI-ModTranslationClassBuilder) files to the ignore list. * Added [SMAPI-ModTranslationClassBuilder](https://github.com/Pathoschild/SMAPI-ModTranslationClassBuilder)
files to the ignore list.
### 3.2.1 ### 3.2.1
Released 11 September 2020. Released 11 September 2020.
@ -359,19 +476,19 @@ Released 11 September 2020.
* Added more detailed logging. * Added more detailed logging.
* Fixed _path's format is not supported_ error when using default `Mods` path in 3.2. * Fixed _path's format is not supported_ error when using default `Mods` path in 3.2.
### 3.2 ### 3.2.0
Released 07 September 2020. Released 07 September 2020.
* Added option to change `Mods` folder path. * Added option to change `Mods` folder path.
* Rewrote documentation to make it easier to read. * Rewrote documentation to make it easier to read.
### 3.1 ### 3.1.0
Released 01 February 2020. Released 01 February 2020.
* Added support for semantic versioning 2.0. * Added support for semantic versioning 2.0.
* `0Harmony.dll` is now ignored if the mod references Harmony directly (it's bundled with SMAPI). * `0Harmony.dll` is now ignored if the mod references Harmony directly (it's bundled with SMAPI).
### 3.0 ### 3.0.0
Released 26 November 2019. Released 26 November 2019.
* Updated for SMAPI 3.0 and Stardew Valley 1.4. * Updated for SMAPI 3.0 and Stardew Valley 1.4.
@ -386,14 +503,14 @@ Released 26 November 2019.
* Dropped support for older versions of SMAPI and Visual Studio. * Dropped support for older versions of SMAPI and Visual Studio.
* Migrated package icon to NuGet's new format. * Migrated package icon to NuGet's new format.
### 2.2 ### 2.2.0
Released 28 October 2018. Released 28 October 2018.
* Added support for SMAPI 2.8+ (still compatible with earlier versions). * Added support for SMAPI 2.8+ (still compatible with earlier versions).
* Added default game paths for 32-bit Windows. * Added default game paths for 32-bit Windows.
* Fixed valid manifests marked invalid in some cases. * Fixed valid manifests marked invalid in some cases.
### 2.1 ### 2.1.0
Released 27 July 2018. Released 27 July 2018.
* Added support for Stardew Valley 1.3. * Added support for Stardew Valley 1.3.
@ -413,7 +530,7 @@ Released 11 October 2017.
* Fixed mod deploy failing to create subfolders if they don't already exist. * Fixed mod deploy failing to create subfolders if they don't already exist.
### 2.0 ### 2.0.0
Released 11 October 2017. Released 11 October 2017.
* Added: mods are now copied into the `Mods` folder automatically (configurable). * Added: mods are now copied into the `Mods` folder automatically (configurable).
@ -431,7 +548,7 @@ Released 28 July 2017.
* The manifest/i18n files in the project now take precedence over those in the build output if both * The manifest/i18n files in the project now take precedence over those in the build output if both
are present. are present.
### 1.7 ### 1.7.0
Released 28 July 2017. Released 28 July 2017.
* Added option to create release zips on build. * Added option to create release zips on build.
@ -448,19 +565,19 @@ Released 09 July 2017.
* Improved crossplatform game path detection. * Improved crossplatform game path detection.
### 1.6 ### 1.6.0
Released 05 June 2017. Released 05 June 2017.
* Added support for deploying mod files into `Mods` automatically. * Added support for deploying mod files into `Mods` automatically.
* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. * Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI.
### 1.5 ### 1.5.0
Released 23 January 2017. Released 23 January 2017.
* Added support for setting a custom game path globally. * Added support for setting a custom game path globally.
* Added default GOG path on Mac. * Added default GOG path on macOS.
### 1.4 ### 1.4.0
Released 11 January 2017. Released 11 January 2017.
* Fixed detection of non-default game paths on 32-bit Windows. * Fixed detection of non-default game paths on 32-bit Windows.
@ -468,22 +585,22 @@ Released 11 January 2017.
* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms * Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms
mods automatically). mods automatically).
### 1.3 ### 1.3.0
Released 31 December 2016. Released 31 December 2016.
* Added support for non-default game paths on Windows. * Added support for non-default game paths on Windows.
### 1.2 ### 1.2.0
Released 24 October 2016. Released 24 October 2016.
* Exclude game binaries from mod build output. * Exclude game binaries from mod build output.
### 1.1 ### 1.1.0
Released 21 October 2016. Released 21 October 2016.
* Added support for overriding the target platform. * Added support for overriding the target platform.
### 1.0 ### 1.0.0
Released 21 October 2016. Released 21 October 2016.
* Initial release. * Initial release.

View File

@ -11,11 +11,12 @@ This document is about SMAPI itself; see also [mod build package](mod-package.md
* [Configuration file](#configuration-file) * [Configuration file](#configuration-file)
* [Command-line arguments](#command-line-arguments) * [Command-line arguments](#command-line-arguments)
* [Compile flags](#compile-flags) * [Compile flags](#compile-flags)
* [For SMAPI developers](#for-smapi-developers) * [Compile from source code](#compile-from-source-code)
* [Compiling from source](#compiling-from-source) * [Main project](#main-project)
* [Debugging a local build](#debugging-a-local-build) * [Custom Harmony build](#custom-harmony-build)
* [Preparing a release](#preparing-a-release) * [Prepare a release](#prepare-a-release)
* [Using a custom Harmony build](#using-a-custom-harmony-build) * [On any platform](#on-any-platform)
* [On Windows](#on-windows)
* [Release notes](#release-notes) * [Release notes](#release-notes)
## Customisation ## Customisation
@ -32,24 +33,28 @@ argument | purpose
`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. `--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. `--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, but these are meant for internal use or testing, and might
or testing and may change without warning. On Linux/Mac, see _environment variables_ below. change without warning. **On Linux/macOS**, command-line arguments won't work; see _environment
variables_ below instead.
argument | purpose argument | purpose
-------- | ------- -------- | -------
`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) `--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` | SMAPI won't log anything to the console. On Linux/macOS only, this will also prevent the launch script from trying to open a terminal window. (Messages will still be written to the log file.)
`--use-current-shell` | On Linux/macOS only, the launch script won't try to open a terminal window. All console output will be sent to the shell running the launch script.
`--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. `--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.
### Environment variables ### Environment variables
The above SMAPI arguments don't work on Linux/Mac due to the way the game launcher works. You can The above SMAPI arguments may not work on Linux/macOS due to the way the game launcher works. You
set temporary environment variables instead. For example: can set temporary environment variables instead. For example:
> SMAPI_MODS_PATH="Mods (multiplayer)" /path/to/StardewValley > SMAPI_MODS_PATH="Mods (multiplayer)" /path/to/StardewValley
environment variable | purpose environment variable | purpose
-------------------- | ------- -------------------- | -------
`SMAPI_NO_TERMINAL` | Equivalent to `--no-terminal` above. `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_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.
### Compile flags ### Compile flags
SMAPI uses a small number of conditional compilation constants, which you can set by editing the SMAPI uses a small number of conditional compilation constants, which you can set by editing the
@ -57,54 +62,114 @@ SMAPI uses a small number of conditional compilation constants, which you can se
flag | purpose flag | purpose
---- | ------- ---- | -------
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled for Windows; if not set, the code assumes Linux/macOS. Set automatically in `common.targets`.
`HARMONY_2` | Whether to enable experimental Harmony 2.0 support and rewrite existing Harmony 1._x_ mods for compatibility. Note that you need to replace `build/0Harmony.dll` with a Harmony 2.0 build (or switch to a package reference) to use this flag. `SMAPI_DEPRECATED` | Whether to include deprecated code in the build.
## For SMAPI developers ## Compile from source code
### Compiling from source ### Main project
Using an official SMAPI release is recommended for most users, but you can compile from source Using an official SMAPI release is recommended for most users, but you can compile from source
directly if needed. There are no special steps (just open the project and compile), but SMAPI often directly if needed. Just open the project in an IDE like [Visual
uses the latest C# syntax. You may need the latest version of your IDE to compile it. Studio](https://visualstudio.microsoft.com/vs/community/) or [Rider](https://www.jetbrains.com/rider/),
and build the `SMAPI` project. The project will automatically adjust the build settings for your
current OS and Stardew Valley install path.
SMAPI uses build configuration derived from the [crossplatform mod config](https://smapi.io/package/readme)
to detect your current OS automatically and load the correct references. Compile output will be
placed in a `bin` folder at the root of the Git repository.
### Debugging a local build
Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting
the `SMAPI` project with debugging from Visual Studio (on Mac or Windows) will launch SMAPI with the `SMAPI` project with debugging from Visual Studio or Rider should launch SMAPI with the
the debugger attached, so you can intercept errors and step through the code being executed. That debugger attached, so you can intercept errors and step through the code being executed.
doesn't work in MonoDevelop on Linux, unfortunately.
### Preparing a release ### Custom Harmony build
To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See SMAPI uses [a custom build of Harmony 2.2.2](https://github.com/Pathoschild/Harmony#readme), which
[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) is included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
on the wiki for the first-time setup. folder before compiling.
1. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for ## Prepare a release
bundled mods. Make sure you use a [semantic version](https://semver.org). Recommended format: ### On any platform
**⚠ Ideally we'd have one set of instructions for all platforms. The instructions in this section
will produce a fully functional release for all supported platforms, _except_ that the application
icon for SMAPI on Windows will disappear due to [.NET runtime bug
3828](https://github.com/dotnet/runtime/issues/3828). Until that's fixed, see the _[on
Windows](#on-windows)_ section below to create a build that retains the icon.**
#### First-time setup
1. On Windows only:
1. [Install Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install).
2. Run `sudo apt update` in WSL to update the package list.
3. The rest of the instructions below should be run in WSL.
2. Install the required software:
1. Install the [.NET 5 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu).
_For Ubuntu-based systems, you can run `lsb_release -a` to get the Ubuntu version number._
2. [Install Steam](https://linuxconfig.org/how-to-install-steam-on-ubuntu-20-04-focal-fossa-linux).
3. Launch `steam` and install the game like usual.
4. Download and install your preferred IDE. For the [latest standalone Rider
version](https://www.jetbrains.com/help/rider/Installation_guide.html#prerequisites):
```sh
wget "<download url here>" -O rider-install.tar.gz
sudo tar -xzvf rider-install.tar.gz -C /opt
ln -s "/opt/JetBrains Rider-<version>/bin/rider.sh"
./rider.sh
```
3. Clone the SMAPI repo:
```sh
git clone https://github.com/Pathoschild/SMAPI.git
```
### Launch the game
1. Run these commands to start Steam:
```sh
export TERM=xterm
steam
```
2. Launch the game through the Steam UI.
### Prepare the release
1. Run `build/unix/prepare-install-package.sh VERSION_HERE` to create the release package in the
root `bin` folder.
Make sure you use a [semantic version](https://semver.org). Recommended format:
build type | format | example build type | format | example
:--------- | :----------------------- | :------ :--------- | :----------------------- | :------
dev build | `<version>-alpha.<date>` | `3.0.0-alpha.20171230` dev build | `<version>-alpha.<date>` | `4.0.0-alpha.20251230`
prerelease | `<version>-beta.<date>` | `3.0.0-beta.20171230` prerelease | `<version>-beta.<date>` | `4.0.0-beta.20251230`
release | `<version>` | `3.0.0` release | `<version>` | `4.0.0`
2. In Windows: ### On Windows
1. Rebuild the solution with the _release_ solution configuration. #### First-time setup
2. Copy `bin/SMAPI installer` and `bin/SMAPI installer for developers` to Linux/Mac. 1. Set up Windows Subsystem for Linux (WSL):
1. [Install WSL](https://docs.microsoft.com/en-us/windows/wsl/install).
2. Run `sudo apt update` in WSL to update the package list.
3. The rest of the instructions below should be run in WSL.
2. Install the required software:
1. Install the [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0).
2. Install [Stardew Valley](https://www.stardewvalley.net/).
3. Clone the SMAPI repo:
```sh
git clone https://github.com/Pathoschild/SMAPI.git
```
3. In Linux/Mac: ### Prepare the release
1. Rebuild the solution with the _release_ solution configuration. 1. Run `build/windows/prepare-install-package.ps1 VERSION_HERE` in PowerShell to create the release
2. Add the `windows-install.*` files from Windows to the `bin/SMAPI installer` and package folders in the root `bin` folder.
`bin/SMAPI installer for developers` folders compiled on Linux.
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
4. Zip the two folders.
### Custom Harmony build Make sure you use a [semantic version](https://semver.org). Recommended format:
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 build type | format | example
folder before compiling. :--------- | :----------------------- | :------
dev build | `<version>-alpha.<date>` | `4.0.0-alpha.20251230`
prerelease | `<version>-beta.<date>` | `4.0.0-beta.20251230`
release | `<version>` | `4.0.0`
2. Launch WSL and run this script:
```bash
# edit to match the build created in steps 1
# In WSL, `/mnt/c/example` accesses `C:\example` on the Windows filesystem.
version="4.0.0"
binFolder="/mnt/e/source/_Stardew/SMAPI/bin"
build/windows/finalize-install-package.sh "$version" "$binFolder"
```
Note: to prepare a test Windows-only build, you can pass `--windows-only` in the first step and
skip the second one.
## Release notes ## Release notes
See [release notes](../release-notes.md). See [release notes](../release-notes.md).

View File

@ -367,7 +367,7 @@ accordingly.
Initial setup: Initial setup:
1. Create an Azure Blob storage account for uploaded files. 1. Create an Azure Blob storage account for uploaded files.
2. Create an Azure App Services environment running the latest .NET Core on Linux or Windows. 2. Create an Azure App Services environment running the latest .NET on Linux or Windows.
3. Add these application settings in the new App Services environment: 3. Add these application settings in the new App Services environment:
property name | description property name | description

View File

@ -0,0 +1,65 @@
using System.IO;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.GameScanning;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Installer.Framework
{
/// <summary>The installer context.</summary>
internal class InstallerContext
{
/*********
** Fields
*********/
/// <summary>The underlying toolkit game scanner.</summary>
private readonly GameScanner GameScanner = new();
/*********
** Accessors
*********/
/// <summary>The current OS.</summary>
public Platform Platform { get; }
/// <summary>The human-readable OS name and version.</summary>
public string PlatformName { get; }
/// <summary>Whether the installer is running on Windows.</summary>
public bool IsWindows => this.Platform == Platform.Windows;
/// <summary>Whether the installer is running on a Unix OS (including Linux or macOS).</summary>
public bool IsUnix => !this.IsWindows;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public InstallerContext()
{
this.Platform = EnvironmentUtility.DetectPlatform();
this.PlatformName = EnvironmentUtility.GetFriendlyPlatformName(this.Platform);
}
/// <summary>Get the installer's version number.</summary>
public ISemanticVersion GetInstallerVersion()
{
var raw = this.GetType().Assembly.GetName().Version!;
return new SemanticVersion(raw);
}
/// <summary>Get whether a folder seems to contain the game files.</summary>
/// <param name="dir">The folder to check.</param>
public bool LooksLikeGameFolder(DirectoryInfo dir)
{
return this.GameScanner.LooksLikeGameFolder(dir);
}
/// <summary>Get whether a folder seems to contain the game, and which version it contains if so.</summary>
/// <param name="dir">The folder to check.</param>
public GameFolderType GetGameFolderType(DirectoryInfo dir)
{
return this.GameScanner.GetGameFolderType(dir);
}
}
}

View File

@ -1,4 +1,5 @@
using System.IO; using System.IO;
using StardewModdingAPI.Toolkit.Framework;
namespace StardewModdingAPI.Installer.Framework namespace StardewModdingAPI.Installer.Framework
{ {
@ -44,17 +45,20 @@ namespace StardewModdingAPI.Installer.Framework
/// <summary>The full path to the user's config overrides file.</summary> /// <summary>The full path to the user's config overrides file.</summary>
public string ApiUserConfigPath { get; } public string ApiUserConfigPath { get; }
/// <summary>The full path to the installed game DLL.</summary>
public string GameDllPath { get; }
/// <summary>The full path to the installed SMAPI executable file.</summary> /// <summary>The full path to the installed SMAPI executable file.</summary>
public string ExecutablePath { get; } public string UnixSmapiExecutablePath { get; }
/// <summary>The full path to the vanilla game launcher on Linux/Mac.</summary> /// <summary>The full path to the vanilla game launch script on Linux/macOS.</summary>
public string UnixLauncherPath { get; } public string VanillaLaunchScriptPath { get; }
/// <summary>The full path to the installed SMAPI launcher on Linux/Mac before it's renamed.</summary> /// <summary>The full path to the installed SMAPI launch script on Linux/macOS before it's renamed.</summary>
public string UnixSmapiLauncherPath { get; } public string NewLaunchScriptPath { get; }
/// <summary>The full path to the vanilla game launcher on Linux/Mac after SMAPI is installed.</summary> /// <summary>The full path to the backed up game launch script on Linux/macOS after SMAPI is installed.</summary>
public string UnixBackupLauncherPath { get; } public string BackupLaunchScriptPath { get; }
/********* /*********
@ -63,19 +67,22 @@ namespace StardewModdingAPI.Installer.Framework
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="bundleDir">The directory path containing the files to copy into the game folder.</param> /// <param name="bundleDir">The directory path containing the files to copy into the game folder.</param>
/// <param name="gameDir">The directory path for the installed game.</param> /// <param name="gameDir">The directory path for the installed game.</param>
/// <param name="gameExecutableName">The name of the game's executable file for the current platform.</param> public InstallerPaths(DirectoryInfo bundleDir, DirectoryInfo gameDir)
public InstallerPaths(DirectoryInfo bundleDir, DirectoryInfo gameDir, string gameExecutableName)
{ {
// base paths
this.BundleDir = bundleDir; this.BundleDir = bundleDir;
this.GameDir = gameDir; this.GameDir = gameDir;
this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods")); this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods"));
this.GameDllPath = Path.Combine(gameDir.FullName, Constants.GameDllName);
// launch scripts
this.VanillaLaunchScriptPath = Path.Combine(gameDir.FullName, "StardewValley");
this.NewLaunchScriptPath = Path.Combine(gameDir.FullName, "unix-launcher.sh");
this.BackupLaunchScriptPath = Path.Combine(gameDir.FullName, "StardewValley-original");
this.UnixSmapiExecutablePath = Path.Combine(gameDir.FullName, "StardewModdingAPI");
// internal files
this.BundleApiUserConfigPath = Path.Combine(bundleDir.FullName, "smapi-internal", "config.user.json"); this.BundleApiUserConfigPath = Path.Combine(bundleDir.FullName, "smapi-internal", "config.user.json");
this.ExecutablePath = Path.Combine(gameDir.FullName, gameExecutableName);
this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley");
this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI");
this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original");
this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.json"); this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.json");
this.ApiUserConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.user.json"); this.ApiUserConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.user.json");
} }

View File

@ -1,18 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Win32; using System.Reflection;
using StardewModdingApi.Installer.Enums; using StardewModdingApi.Installer.Enums;
using StardewModdingAPI.Installer.Framework; using StardewModdingAPI.Installer.Framework;
using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Internal.ConsoleWriting;
using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Framework.GameScanning;
using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
#if !SMAPI_FOR_WINDOWS
using System.Diagnostics;
#endif
namespace StardewModdingApi.Installer namespace StardewModdingApi.Installer
{ {
@ -25,35 +26,37 @@ namespace StardewModdingApi.Installer
/// <summary>The absolute path to the directory containing the files to copy into the game folder.</summary> /// <summary>The absolute path to the directory containing the files to copy into the game folder.</summary>
private readonly string BundlePath; private readonly string BundlePath;
/// <summary>The <see cref="Environment.OSVersion"/> value that represents Windows 7.</summary>
private readonly Version Windows7Version = new Version(6, 1);
/// <summary>The mod IDs which the installer should allow as bundled mods.</summary> /// <summary>The mod IDs which the installer should allow as bundled mods.</summary>
private readonly string[] BundledModIDs = new[] private readonly string[] BundledModIDs = {
{
"SMAPI.SaveBackup", "SMAPI.SaveBackup",
"SMAPI.ConsoleCommands" "SMAPI.ConsoleCommands",
"SMAPI.ErrorHandler"
}; };
/// <summary>Get the absolute file or folder paths to remove when uninstalling SMAPI.</summary> /// <summary>Get the absolute file or folder paths to remove when uninstalling SMAPI.</summary>
/// <param name="installDir">The folder for Stardew Valley and SMAPI.</param> /// <param name="installDir">The folder for Stardew Valley and SMAPI.</param>
/// <param name="modsDir">The folder for SMAPI mods.</param> /// <param name="modsDir">The folder for SMAPI mods.</param>
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are valid file names.")]
private IEnumerable<string> GetUninstallPaths(DirectoryInfo installDir, DirectoryInfo modsDir) private IEnumerable<string> GetUninstallPaths(DirectoryInfo installDir, DirectoryInfo modsDir)
{ {
string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); string GetInstallPath(string path) => Path.Combine(installDir.FullName, path);
// current files // current files
yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only
yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI.deps.json");
yield return GetInstallPath("StardewModdingAPI.dll");
yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.exe");
yield return GetInstallPath("StardewModdingAPI.exe.config"); yield return GetInstallPath("StardewModdingAPI.exe.config");
yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/macOS only
yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only
yield return GetInstallPath("StardewModdingAPI.runtimeconfig.json");
yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("StardewModdingAPI.xml");
yield return GetInstallPath("smapi-internal"); yield return GetInstallPath("smapi-internal");
yield return GetInstallPath("steam_appid.txt"); yield return GetInstallPath("steam_appid.txt");
#if SMAPI_DEPRECATED
// obsolete // obsolete
yield return GetInstallPath("libgdiplus.dylib"); // before 3.13 (macOS only)
yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *2.0 (renamed to ConsoleCommands) yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *2.0 (renamed to ConsoleCommands)
yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.31.8 yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.31.8
@ -73,15 +76,14 @@ namespace StardewModdingApi.Installer
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8
yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 yield return GetInstallPath("StardewModdingAPI-x64.exe"); // before 3.13
yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8
yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8
if (modsDir.Exists) if (modsDir.Exists)
{ {
foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories())
yield return Path.Combine(modDir.FullName, ".cache"); // 1.41.7 yield return Path.Combine(modDir.FullName, ".cache"); // 1.41.7
} }
#endif
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files
} }
@ -109,13 +111,13 @@ namespace StardewModdingApi.Installer
/// 2. Ask the user whether to install or uninstall. /// 2. Ask the user whether to install or uninstall.
/// ///
/// Uninstall logic: /// Uninstall logic:
/// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup. /// 1. On Linux/macOS: if a backup of the launcher exists, delete the launcher and restore the backup.
/// 2. Delete all files and folders in the game directory matching one of the values returned by <see cref="GetUninstallPaths"/>. /// 2. Delete all files and folders in the game directory matching one of the values returned by <see cref="GetUninstallPaths"/>.
/// ///
/// Install flow: /// Install flow:
/// 1. Run the uninstall flow. /// 1. Run the uninstall flow.
/// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory. /// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory.
/// 3. On Linux/Mac: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) /// 3. On Linux/macOS: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.)
/// 4. Create the 'Mods' directory. /// 4. Create the 'Mods' directory.
/// 5. Copy the bundled mods into the 'Mods' directory (deleting any existing versions). /// 5. Copy the bundled mods into the 'Mods' directory (deleting any existing versions).
/// 6. Move any mods from app data into game's mods directory. /// 6. Move any mods from app data into game's mods directory.
@ -128,54 +130,30 @@ namespace StardewModdingApi.Installer
/**** /****
** Get basic info & set window title ** Get basic info & set window title
****/ ****/
ModToolkit toolkit = new ModToolkit(); ModToolkit toolkit = new();
Platform platform = EnvironmentUtility.DetectPlatform(); var context = new InstallerContext();
Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.Title = $"SMAPI {context.GetInstallerVersion()} installer on {context.Platform} {context.PlatformName}";
Console.WriteLine(); Console.WriteLine();
/**** /****
** Check if correct installer ** Check if correct installer
****/ ****/
#if SMAPI_FOR_WINDOWS #if SMAPI_FOR_WINDOWS
if (platform == Platform.Linux || platform == Platform.Mac) if (context.IsUnix)
{ {
this.PrintError($"This is the installer for Windows. Run the 'install on {platform}.{(platform == Platform.Linux ? "sh" : "command")}' file instead."); this.PrintError($"This is the installer for Windows. Run the 'install on {context.Platform}.{(context.Platform == Platform.Mac ? "command" : "sh")}' file instead.");
Console.ReadLine(); Console.ReadLine();
return; return;
} }
#else #else
if (platform == Platform.Windows) if (context.IsWindows)
{ {
this.PrintError($"This is the installer for Linux/Mac. Run the 'install on Windows.exe' file instead."); this.PrintError($"This is the installer for Linux/macOS. Run the 'install on Windows.exe' file instead.");
Console.ReadLine(); Console.ReadLine();
return; return;
} }
#endif #endif
/****
** Check Windows dependencies
****/
if (platform == Platform.Windows)
{
// .NET Framework 4.5+
if (!this.HasNetFramework45(platform))
{
this.PrintError(Environment.OSVersion.Version >= this.Windows7Version
? "Please install the latest version of .NET Framework before installing SMAPI." // Windows 7+
: "Please install .NET Framework 4.5 before installing SMAPI." // Windows Vista or earlier
);
this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details.");
Console.ReadLine();
return;
}
if (!this.HasXna(platform))
{
this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup.");
Console.ReadLine();
return;
}
}
/**** /****
** read command-line arguments ** read command-line arguments
****/ ****/
@ -190,7 +168,7 @@ namespace StardewModdingApi.Installer
} }
// get game path from CLI // get game path from CLI
string gamePathArg = null; string? gamePathArg = null;
{ {
int pathIndex = Array.LastIndexOf(args, "--game-path") + 1; int pathIndex = Array.LastIndexOf(args, "--game-path") + 1;
if (pathIndex >= 1 && args.Length >= pathIndex) if (pathIndex >= 1 && args.Length >= pathIndex)
@ -199,10 +177,10 @@ namespace StardewModdingApi.Installer
/********* /*********
** Step 2: choose a theme (can't auto-detect on Linux/Mac) ** Step 2: choose a theme (can't auto-detect on Linux/macOS)
*********/ *********/
MonitorColorScheme scheme = MonitorColorScheme.AutoDetect; MonitorColorScheme scheme = MonitorColorScheme.AutoDetect;
if (platform == Platform.Linux || platform == Platform.Mac) if (context.IsUnix)
{ {
/**** /****
** print header ** print header
@ -215,8 +193,8 @@ namespace StardewModdingApi.Installer
** show theme selector ** show theme selector
****/ ****/
// get theme writers // get theme writers
var lightBackgroundWriter = new ColorfulConsoleWriter(platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground)); ColorfulConsoleWriter lightBackgroundWriter = new(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground));
var darkBackgroundWriter = new ColorfulConsoleWriter(platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground)); ColorfulConsoleWriter darkBackgroundWriter = new(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground));
// print question // print question
this.PrintPlain("Which text looks more readable?"); this.PrintPlain("Which text looks more readable?");
@ -228,7 +206,7 @@ namespace StardewModdingApi.Installer
Console.WriteLine(); Console.WriteLine();
// handle choice // handle choice
string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }); string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }, printLine: Console.WriteLine);
switch (choice) switch (choice)
{ {
case "1": case "1":
@ -263,8 +241,7 @@ namespace StardewModdingApi.Installer
** collect details ** collect details
****/ ****/
// get game path // get game path
this.PrintInfo("Where is your game folder?"); DirectoryInfo? installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg);
DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, toolkit, gamePathArg);
if (installDir == null) if (installDir == null)
{ {
this.PrintError("Failed finding your game path."); this.PrintError("Failed finding your game path.");
@ -273,21 +250,22 @@ namespace StardewModdingApi.Installer
} }
// get folders // get folders
DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath); DirectoryInfo bundleDir = new(this.BundlePath);
paths = new InstallerPaths(bundleDir, installDir, EnvironmentUtility.GetExecutableName(platform)); paths = new InstallerPaths(bundleDir, installDir);
} }
Console.Clear();
/********* /*********
** Step 4: validate assumptions ** Step 4: validate assumptions
*********/ *********/
if (!File.Exists(paths.ExecutablePath)) // executable exists
if (!File.Exists(paths.GameDllPath))
{ {
this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
Console.ReadLine(); Console.ReadLine();
return; return;
} }
Console.Clear();
/********* /*********
@ -359,11 +337,11 @@ namespace StardewModdingApi.Installer
** Always uninstall old files ** Always uninstall old files
****/ ****/
// restore game launcher // restore game launcher
if (platform.IsMono() && File.Exists(paths.UnixBackupLauncherPath)) if (context.IsUnix && File.Exists(paths.BackupLaunchScriptPath))
{ {
this.PrintDebug("Removing SMAPI launcher..."); this.PrintDebug("Removing SMAPI launcher...");
this.InteractivelyDelete(paths.UnixLauncherPath); this.InteractivelyDelete(paths.VanillaLaunchScriptPath);
File.Move(paths.UnixBackupLauncherPath, paths.UnixLauncherPath); File.Move(paths.BackupLaunchScriptPath, paths.VanillaLaunchScriptPath);
} }
// remove old files // remove old files
@ -380,8 +358,8 @@ namespace StardewModdingApi.Installer
// move global save data folder (changed in 3.2) // move global save data folder (changed in 3.2)
{ {
string dataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); string dataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
DirectoryInfo oldDir = new DirectoryInfo(Path.Combine(dataPath, "Saves", ".smapi")); DirectoryInfo oldDir = new(Path.Combine(dataPath, "Saves", ".smapi"));
DirectoryInfo newDir = new DirectoryInfo(Path.Combine(dataPath, ".smapi")); DirectoryInfo newDir = new(Path.Combine(dataPath, ".smapi"));
if (oldDir.Exists) if (oldDir.Exists)
{ {
@ -406,38 +384,46 @@ namespace StardewModdingApi.Installer
} }
// replace mod launcher (if possible) // replace mod launcher (if possible)
if (platform.IsMono()) if (context.IsUnix)
{ {
this.PrintDebug("Safely replacing game launcher..."); this.PrintDebug("Safely replacing game launcher...");
// back up & remove current launcher // back up & remove current launcher
if (File.Exists(paths.UnixLauncherPath)) if (File.Exists(paths.VanillaLaunchScriptPath))
{ {
if (!File.Exists(paths.UnixBackupLauncherPath)) if (!File.Exists(paths.BackupLaunchScriptPath))
File.Move(paths.UnixLauncherPath, paths.UnixBackupLauncherPath); File.Move(paths.VanillaLaunchScriptPath, paths.BackupLaunchScriptPath);
else else
this.InteractivelyDelete(paths.UnixLauncherPath); this.InteractivelyDelete(paths.VanillaLaunchScriptPath);
} }
// add new launcher // add new launcher
File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath); File.Move(paths.NewLaunchScriptPath, paths.VanillaLaunchScriptPath);
// mark file executable // mark files executable
// (MSBuild doesn't keep permission flags for files zipped in a build task.) // (MSBuild doesn't keep permission flags for files zipped in a build task.)
// (Note: exclude from Windows build because antivirus apps can flag the process start code as suspicious.) foreach (string path in new[] { paths.VanillaLaunchScriptPath, paths.UnixSmapiExecutablePath })
#if !SMAPI_FOR_WINDOWS
new Process
{ {
StartInfo = new ProcessStartInfo new Process
{ {
FileName = "chmod", StartInfo = new ProcessStartInfo
Arguments = $"755 \"{paths.UnixLauncherPath}\"", {
CreateNoWindow = true FileName = "chmod",
} Arguments = $"755 \"{path}\"",
}.Start(); CreateNoWindow = true
#endif }
}.Start();
}
} }
// copy the game's deps.json file
// (This is needed to resolve native DLLs like libSkiaSharp.)
File.Copy(
sourceFileName: Path.Combine(paths.GamePath, "Stardew Valley.deps.json"),
destFileName: Path.Combine(paths.GamePath, "StardewModdingAPI.deps.json"),
overwrite: true
);
// create mods directory (if needed) // create mods directory (if needed)
if (!paths.ModsDir.Exists) if (!paths.ModsDir.Exists)
{ {
@ -446,13 +432,13 @@ namespace StardewModdingApi.Installer
} }
// add or replace bundled mods // add or replace bundled mods
DirectoryInfo bundledModsDir = new DirectoryInfo(Path.Combine(paths.BundlePath, "Mods")); DirectoryInfo bundledModsDir = new(Path.Combine(paths.BundlePath, "Mods"));
if (bundledModsDir.Exists && bundledModsDir.EnumerateDirectories().Any()) if (bundledModsDir.Exists && bundledModsDir.EnumerateDirectories().Any())
{ {
this.PrintDebug("Adding bundled mods..."); this.PrintDebug("Adding bundled mods...");
ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray(); ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath, useCaseInsensitiveFilePaths: true).ToArray();
foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName)) foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName, useCaseInsensitiveFilePaths: true))
{ {
// validate source mod // validate source mod
if (sourceMod.Manifest == null) if (sourceMod.Manifest == null)
@ -467,8 +453,9 @@ namespace StardewModdingApi.Installer
} }
// find target folder // find target folder
ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true); // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract -- avoid error if the Mods folder has invalid mods, since they're not validated yet
DirectoryInfo defaultTargetFolder = new DirectoryInfo(Path.Combine(paths.ModsPath, sourceMod.Directory.Name)); ModFolder? targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true);
DirectoryInfo defaultTargetFolder = new(Path.Combine(paths.ModsPath, sourceMod.Directory.Name));
DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder; DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder;
this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName
? $" adding {sourceMod.Manifest.Name}..." ? $" adding {sourceMod.Manifest.Name}..."
@ -493,8 +480,10 @@ namespace StardewModdingApi.Installer
File.WriteAllText(paths.ApiConfigPath, text); File.WriteAllText(paths.ApiConfigPath, text);
} }
#if SMAPI_DEPRECATED
// remove obsolete appdata mods // remove obsolete appdata mods
this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir); this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir);
#endif
} }
} }
Console.WriteLine(); Console.WriteLine();
@ -504,7 +493,7 @@ namespace StardewModdingApi.Installer
/********* /*********
** Step 7: final instructions ** Step 7: final instructions
*********/ *********/
if (platform == Platform.Windows) if (context.IsWindows)
{ {
if (action == ScriptAction.Install) if (action == ScriptAction.Install)
{ {
@ -531,16 +520,6 @@ namespace StardewModdingApi.Installer
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>Get the display text for an assembly version.</summary>
/// <param name="version">The assembly version.</param>
private string GetDisplayVersion(Version version)
{
string str = $"{version.Major}.{version.Minor}";
if (version.Build != 0)
str += $".{version.Build}";
return str;
}
/// <summary>Get the display text for a color scheme.</summary> /// <summary>Get the display text for a color scheme.</summary>
/// <param name="scheme">The color scheme.</param> /// <param name="scheme">The color scheme.</param>
private string GetDisplayText(MonitorColorScheme scheme) private string GetDisplayText(MonitorColorScheme scheme)
@ -560,58 +539,44 @@ namespace StardewModdingApi.Installer
/// <summary>Print a message without formatting.</summary> /// <summary>Print a message without formatting.</summary>
/// <param name="text">The text to print.</param> /// <param name="text">The text to print.</param>
private void PrintPlain(string text) => Console.WriteLine(text); private void PrintPlain(string text)
{
Console.WriteLine(text);
}
/// <summary>Print a debug message.</summary> /// <summary>Print a debug message.</summary>
/// <param name="text">The text to print.</param> /// <param name="text">The text to print.</param>
private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug); private void PrintDebug(string text)
{
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
}
/// <summary>Print a debug message.</summary> /// <summary>Print a debug message.</summary>
/// <param name="text">The text to print.</param> /// <param name="text">The text to print.</param>
private void PrintInfo(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info); private void PrintInfo(string text)
{
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info);
}
/// <summary>Print a warning message.</summary> /// <summary>Print a warning message.</summary>
/// <param name="text">The text to print.</param> /// <param name="text">The text to print.</param>
private void PrintWarning(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn); private void PrintWarning(string text)
{
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn);
}
/// <summary>Print a warning message.</summary> /// <summary>Print a warning message.</summary>
/// <param name="text">The text to print.</param> /// <param name="text">The text to print.</param>
private void PrintError(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error); private void PrintError(string text)
{
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error);
}
/// <summary>Print a success message.</summary> /// <summary>Print a success message.</summary>
/// <param name="text">The text to print.</param> /// <param name="text">The text to print.</param>
private void PrintSuccess(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success); private void PrintSuccess(string text)
/// <summary>Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows.</summary>
/// <param name="platform">The current platform.</param>
/// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
private bool HasNetFramework45(Platform platform)
{ {
switch (platform) this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success);
{
case Platform.Windows:
using (RegistryKey versionKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"))
return versionKey?.GetValue("Release") != null; // .NET Framework 4.5+
default:
throw new NotSupportedException("The installed .NET Framework version can only be checked on Windows.");
}
}
/// <summary>Get whether the current system has XNA Framework installed. This only applies on Windows.</summary>
/// <param name="platform">The current platform.</param>
/// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
private bool HasXna(Platform platform)
{
switch (platform)
{
case Platform.Windows:
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\XNA\Framework"))
return key != null; // XNA Framework 4.0+
default:
throw new NotSupportedException("The installed XNA Framework version can only be checked on Windows.");
}
} }
/// <summary>Interactively delete a file or folder path, and block until deletion completes.</summary> /// <summary>Interactively delete a file or folder path, and block until deletion completes.</summary>
@ -622,7 +587,7 @@ namespace StardewModdingApi.Installer
{ {
try try
{ {
FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : new FileInfo(path));
break; break;
} }
catch (Exception ex) catch (Exception ex)
@ -638,7 +603,7 @@ namespace StardewModdingApi.Installer
/// <param name="source">The file or folder to copy.</param> /// <param name="source">The file or folder to copy.</param>
/// <param name="targetFolder">The folder to copy into.</param> /// <param name="targetFolder">The folder to copy into.</param>
/// <param name="filter">A filter which matches directories and files to copy, or <c>null</c> to match all.</param> /// <param name="filter">A filter which matches directories and files to copy, or <c>null</c> to match all.</param>
private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter = null) private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool>? filter = null)
{ {
if (filter != null && !filter(source)) if (filter != null && !filter(source))
return; return;
@ -653,8 +618,8 @@ namespace StardewModdingApi.Installer
break; break;
case DirectoryInfo sourceDir: case DirectoryInfo sourceDir:
DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)); DirectoryInfo targetSubfolder = new(Path.Combine(targetFolder.FullName, sourceDir.Name));
foreach (var entry in sourceDir.EnumerateFileSystemInfos()) foreach (FileSystemInfo entry in sourceDir.EnumerateFileSystemInfos())
this.RecursiveCopy(entry, targetSubfolder, filter); this.RecursiveCopy(entry, targetSubfolder, filter);
break; break;
@ -664,22 +629,22 @@ namespace StardewModdingApi.Installer
} }
/// <summary>Interactively ask the user to choose a value.</summary> /// <summary>Interactively ask the user to choose a value.</summary>
/// <param name="print">A callback which prints a message to the console.</param> /// <param name="printLine">A callback which prints a message to the console.</param>
/// <param name="message">The message to print.</param> /// <param name="message">The message to print.</param>
/// <param name="options">The allowed options (not case sensitive).</param> /// <param name="options">The allowed options (not case sensitive).</param>
/// <param name="indent">The indentation to prefix to output.</param> /// <param name="indent">The indentation to prefix to output.</param>
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null) private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? printLine = null)
{ {
print ??= this.PrintInfo; printLine ??= this.PrintInfo;
while (true) while (true)
{ {
print(indent + message); printLine(indent + message);
Console.Write(indent); Console.Write(indent);
string input = Console.ReadLine()?.Trim().ToLowerInvariant(); string? input = Console.ReadLine()?.Trim().ToLowerInvariant();
if (!options.Contains(input)) if (input == null || !options.Contains(input))
{ {
print($"{indent}That's not a valid option."); printLine($"{indent}That's not a valid option.");
continue; continue;
} }
return input; return input;
@ -687,100 +652,166 @@ namespace StardewModdingApi.Installer
} }
/// <summary>Interactively locate the game install path to update.</summary> /// <summary>Interactively locate the game install path to update.</summary>
/// <param name="platform">The current platform.</param>
/// <param name="toolkit">The mod toolkit.</param> /// <param name="toolkit">The mod toolkit.</param>
/// <param name="context">The installer context.</param>
/// <param name="specifiedPath">The path specified as a command-line argument (if any), which should override automatic path detection.</param> /// <param name="specifiedPath">The path specified as a command-line argument (if any), which should override automatic path detection.</param>
private DirectoryInfo InteractivelyGetInstallPath(Platform platform, ModToolkit toolkit, string specifiedPath) private DirectoryInfo? InteractivelyGetInstallPath(ModToolkit toolkit, InstallerContext context, string? specifiedPath)
{ {
// get executable name // use specified path
string executableFilename = EnvironmentUtility.GetExecutableName(platform);
// validate specified path
if (specifiedPath != null) if (specifiedPath != null)
{ {
string errorPrefix = $"You specified --game-path \"{specifiedPath}\", but";
var dir = new DirectoryInfo(specifiedPath); var dir = new DirectoryInfo(specifiedPath);
if (!dir.Exists) if (!dir.Exists)
{ {
this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't exist."); this.PrintError($"{errorPrefix} that folder doesn't exist.");
return null; return null;
} }
if (!dir.EnumerateFiles(executableFilename).Any())
switch (context.GetGameFolderType(dir))
{ {
this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't contain the Stardew Valley executable."); case GameFolderType.Valid:
return null; return dir;
case GameFolderType.Legacy154OrEarlier:
this.PrintWarning($"{errorPrefix} that directory seems to have Stardew Valley 1.5.4 or earlier.");
this.PrintWarning("Please update your game to the latest version to use SMAPI.");
return null;
case GameFolderType.LegacyCompatibilityBranch:
this.PrintWarning($"{errorPrefix} that directory seems to have the Stardew Valley legacy 'compatibility' branch.");
this.PrintWarning("Unfortunately SMAPI is only compatible with the modern version of the game.");
this.PrintWarning("Please update your game to the main branch to use SMAPI.");
return null;
case GameFolderType.NoGameFound:
this.PrintWarning($"{errorPrefix} that directory doesn't contain a Stardew Valley executable.");
return null;
default:
this.PrintWarning($"{errorPrefix} that directory doesn't seem to contain a valid game install.");
return null;
} }
return dir;
} }
// get installed paths // let user choose detected path
DirectoryInfo[] defaultPaths = toolkit.GetGameFolders().ToArray(); DirectoryInfo[] defaultPaths = this.DetectGameFolders(toolkit, context).ToArray();
if (defaultPaths.Any()) if (defaultPaths.Any())
{ {
// only one path this.PrintInfo("Where do you want to add or remove SMAPI?");
if (defaultPaths.Length == 1)
return defaultPaths.First();
// let user choose path
Console.WriteLine(); Console.WriteLine();
this.PrintInfo("Found multiple copies of the game:");
for (int i = 0; i < defaultPaths.Length; i++) for (int i = 0; i < defaultPaths.Length; i++)
this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}"); this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}");
this.PrintInfo($"[{defaultPaths.Length + 1}] Enter a custom game path.");
Console.WriteLine(); Console.WriteLine();
string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); string[] validOptions = Enumerable.Range(1, defaultPaths.Length + 1).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray();
string choice = this.InteractivelyChoose("Where do you want to add/remove SMAPI? Type the number next to your choice, then press enter.", validOptions); string choice = this.InteractivelyChoose("Type the number next to your choice, then press enter.", validOptions);
int index = int.Parse(choice, CultureInfo.InvariantCulture) - 1; int index = int.Parse(choice, CultureInfo.InvariantCulture) - 1;
return defaultPaths[index];
}
// ask user if (index < defaultPaths.Length)
this.PrintInfo("Oops, couldn't find the game automatically."); return defaultPaths[index];
}
else
this.PrintInfo("Oops, couldn't find the game automatically.");
// let user enter manual path
while (true) while (true)
{ {
// get path from user // get path from user
this.PrintInfo($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter."); Console.WriteLine();
string path = Console.ReadLine()?.Trim(); this.PrintInfo($"Type the file path to the game directory (the one containing '{Constants.GameDllName}'), then press enter.");
string? path = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
{ {
this.PrintInfo(" You must specify a directory path to continue."); this.PrintWarning("You must specify a directory path to continue.");
continue; continue;
} }
// normalize path // normalize path
if (platform == Platform.Windows) path = context.IsWindows
path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path ? path.Replace("\"", "") // in Windows, quotes are used to escape spaces and aren't part of the file path
if (platform == Platform.Linux || platform == Platform.Mac) : path.Replace("\\ ", " "); // in Linux/macOS, spaces in paths may be escaped if copied from the command line
path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line
if (path.StartsWith("~/")) if (path.StartsWith("~/"))
{ {
string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE"); string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE")!;
path = Path.Combine(home, path.Substring(2)); path = Path.Combine(home, path.Substring(2));
} }
// get directory // get directory
if (File.Exists(path)) if (File.Exists(path))
path = Path.GetDirectoryName(path); path = Path.GetDirectoryName(path)!;
DirectoryInfo directory = new DirectoryInfo(path); DirectoryInfo directory = new(path);
// validate path // validate path
if (!directory.Exists) if (!directory.Exists)
{ {
this.PrintInfo(" That directory doesn't seem to exist."); this.PrintWarning("That directory doesn't seem to exist.");
continue;
}
if (!directory.EnumerateFiles(executableFilename).Any())
{
this.PrintInfo(" That directory doesn't contain a Stardew Valley executable.");
continue; continue;
} }
// looks OK switch (context.GetGameFolderType(directory))
this.PrintInfo(" OK!"); {
return directory; case GameFolderType.Valid:
this.PrintInfo(" OK!");
return directory;
case GameFolderType.Legacy154OrEarlier:
this.PrintWarning("That directory seems to have Stardew Valley 1.5.4 or earlier.");
this.PrintWarning("Please update your game to the latest version to use SMAPI.");
continue;
case GameFolderType.LegacyCompatibilityBranch:
this.PrintWarning("That directory seems to have the Stardew Valley legacy 'compatibility' branch.");
this.PrintWarning("Unfortunately SMAPI is only compatible with the modern version of the game.");
this.PrintWarning("Please update your game to the main branch to use SMAPI.");
continue;
case GameFolderType.NoGameFound:
this.PrintWarning("That directory doesn't contain a Stardew Valley executable.");
continue;
default:
this.PrintWarning("That directory doesn't seem to contain a valid game install.");
continue;
}
} }
} }
/// <summary>Interactively move mods out of the appdata directory.</summary> /// <summary>Get the possible game paths to update.</summary>
/// <param name="toolkit">The mod toolkit.</param>
/// <param name="context">The installer context.</param>
private IEnumerable<DirectoryInfo> DetectGameFolders(ModToolkit toolkit, InstallerContext context)
{
HashSet<string> foundPaths = new HashSet<string>();
// game folder which contains the installer, if any
{
DirectoryInfo? curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
while (curPath?.Parent != null) // must be in a folder (not at the root)
{
if (context.LooksLikeGameFolder(curPath))
{
foundPaths.Add(curPath.FullName);
yield return curPath;
break;
}
curPath = curPath.Parent;
}
}
// game paths detected by toolkit
foreach (DirectoryInfo dir in toolkit.GetGameFolders())
{
if (foundPaths.Add(dir.FullName))
yield return dir;
}
}
#if SMAPI_DEPRECATED
/// <summary>Interactively move mods out of the app data directory.</summary>
/// <param name="properModsDir">The directory which should contain all mods.</param> /// <param name="properModsDir">The directory which should contain all mods.</param>
/// <param name="packagedModsDir">The installer directory containing packaged mods.</param> /// <param name="packagedModsDir">The installer directory containing packaged mods.</param>
private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir)
@ -790,7 +821,7 @@ namespace StardewModdingApi.Installer
// get path // get path
string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods")); DirectoryInfo modDir = new(Path.Combine(appDataPath, "Mods"));
// check if migration needed // check if migration needed
if (!modDir.Exists) if (!modDir.Exists)
@ -803,7 +834,7 @@ namespace StardewModdingApi.Installer
{ {
// get type // get type
bool isDir = entry is DirectoryInfo; bool isDir = entry is DirectoryInfo;
if (!isDir && !(entry is FileInfo)) if (!isDir && entry is not FileInfo)
continue; // should never happen continue; // should never happen
// delete packaged mods (newer version bundled into SMAPI) // delete packaged mods (newer version bundled into SMAPI)
@ -840,7 +871,7 @@ namespace StardewModdingApi.Installer
/// <summary>Move a filesystem entry to a new parent directory.</summary> /// <summary>Move a filesystem entry to a new parent directory.</summary>
/// <param name="entry">The filesystem entry to move.</param> /// <param name="entry">The filesystem entry to move.</param>
/// <param name="newPath">The destination path.</param> /// <param name="newPath">The destination path.</param>
/// <remarks>We can't use <see cref="FileInfo.MoveTo"/> or <see cref="DirectoryInfo.MoveTo"/>, because those don't work across partitions.</remarks> /// <remarks>We can't use <see cref="FileInfo.MoveTo(string)"/> or <see cref="DirectoryInfo.MoveTo"/>, because those don't work across partitions.</remarks>
private void Move(FileSystemInfo entry, string newPath) private void Move(FileSystemInfo entry, string newPath)
{ {
// file // file
@ -862,20 +893,18 @@ namespace StardewModdingApi.Installer
directory.Delete(recursive: true); directory.Delete(recursive: true);
} }
} }
#endif
/// <summary>Get whether a file or folder should be copied from the installer files.</summary> /// <summary>Get whether a file or folder should be copied from the installer files.</summary>
/// <param name="entry">The file or folder info.</param> /// <param name="entry">The file or folder info.</param>
private bool ShouldCopy(FileSystemInfo entry) private bool ShouldCopy(FileSystemInfo entry)
{ {
switch (entry.Name) return entry.Name switch
{ {
case "mcs": "mcs" => false, // ignore macOS symlink
return false; // ignore Mac symlink "Mods" => false, // Mods folder handled separately
case "Mods": _ => true
return false; // Mods folder handled separately };
default:
return true;
}
} }
} }
} }

View File

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Reflection; using System.Reflection;
using System.Threading;
namespace StardewModdingApi.Installer namespace StardewModdingApi.Installer
{ {
@ -14,7 +15,7 @@ namespace StardewModdingApi.Installer
*********/ *********/
/// <summary>The absolute path of the installer folder.</summary> /// <summary>The absolute path of the installer folder.</summary>
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")]
private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
/// <summary>The absolute path of the folder containing the unzipped installer files.</summary> /// <summary>The absolute path of the folder containing the unzipped installer files.</summary>
private static readonly string ExtractedBundlePath = Path.Combine(Path.GetTempPath(), $"SMAPI-installer-{Guid.NewGuid():N}"); private static readonly string ExtractedBundlePath = Path.Combine(Path.GetTempPath(), $"SMAPI-installer-{Guid.NewGuid():N}");
@ -30,8 +31,7 @@ namespace StardewModdingApi.Installer
public static void Main(string[] args) public static void Main(string[] args)
{ {
// find install bundle // find install bundle
PlatformID platform = Environment.OSVersion.Platform; FileInfo zipFile = new(Path.Combine(Program.InstallerPath, "install.dat"));
FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-install.dat"));
if (!zipFile.Exists) if (!zipFile.Exists)
{ {
Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})"); Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})");
@ -40,7 +40,7 @@ namespace StardewModdingApi.Installer
} }
// unzip bundle into temp folder // unzip bundle into temp folder
DirectoryInfo bundleDir = new DirectoryInfo(Program.ExtractedBundlePath); DirectoryInfo bundleDir = new(Program.ExtractedBundlePath);
Console.WriteLine("Extracting install files..."); Console.WriteLine("Extracting install files...");
ZipFile.ExtractToDirectory(zipFile.FullName, bundleDir.FullName); ZipFile.ExtractToDirectory(zipFile.FullName, bundleDir.FullName);
@ -49,7 +49,15 @@ namespace StardewModdingApi.Installer
// launch installer // launch installer
var installer = new InteractiveInstaller(bundleDir.FullName); var installer = new InteractiveInstaller(bundleDir.FullName);
installer.Run(args);
try
{
installer.Run(args);
}
catch (Exception ex)
{
Program.PrintErrorAndExit($"The installer failed with an unexpected exception.\nIf you need help fixing this error, see https://smapi.io/help\n\n{ex}");
}
} }
/********* /*********
@ -58,14 +66,14 @@ namespace StardewModdingApi.Installer
/// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary> /// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary>
/// <param name="sender">The event sender.</param> /// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param> /// <param name="e">The event arguments.</param>
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs e)
{ {
try try
{ {
AssemblyName name = new AssemblyName(e.Name); AssemblyName name = new(e.Name);
foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll")) foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll"))
{ {
if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase)) if (name.Name != null && name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase))
return Assembly.LoadFrom(dll.FullName); return Assembly.LoadFrom(dll.FullName);
} }
return null; return null;
@ -76,5 +84,19 @@ namespace StardewModdingApi.Installer
return null; return null;
} }
} }
/// <summary>Write an error directly to the console and exit.</summary>
/// <param name="message">The error message to display.</param>
private static void PrintErrorAndExit(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.ResetColor();
Console.WriteLine("Game has ended. Press any key to exit.");
Thread.Sleep(100);
Console.ReadKey();
Environment.Exit(0);
}
} }
} }

View File

@ -2,9 +2,8 @@
<PropertyGroup> <PropertyGroup>
<RootNamespace>StardewModdingAPI.Installer</RootNamespace> <RootNamespace>StardewModdingAPI.Installer</RootNamespace>
<Description>The SMAPI installer for players.</Description> <Description>The SMAPI installer for players.</Description>
<TargetFramework>net45</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PlatformTarget>x86</PlatformTarget>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup> </PropertyGroup>
@ -18,5 +17,4 @@
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" /> <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
<Import Project="..\..\build\common.targets" /> <Import Project="..\..\build\common.targets" />
<Import Project="..\..\build\prepare-install-package.targets" />
</Project> </Project>

View File

@ -14,31 +14,34 @@
SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately.
Player's guide Automated install
-------------------------------- --------------------------------
See https://stardewvalleywiki.com/Modding:Player_Guide for help installing SMAPI, adding mods, etc. See https://stardewvalleywiki.com/Modding:Player_Guide for help installing SMAPI, adding mods, etc.
Manual install Manual install
-------------------------------- --------------------------------
THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See the instructions above instead.
If you really want to install SMAPI manually, here's how. If you really want to install SMAPI manually, here's how.
1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on Linux/Mac). 1. Unzip "internal/windows/install.dat" (on Windows) or "internal/unix/install.dat" (on Linux or
You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent confusion. macOS). You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent
confusion.
2. Copy the files from the folder you just unzipped into your game folder. The 2. Copy the files from the folder you just unzipped into your game folder. The
`StardewModdingAPI.exe` file should be right next to the game's executable. `StardewModdingAPI.exe` file should be right next to the game's executable.
3.
3. Copy `Stardew Valley.deps.json` in the game folder, and rename the copy to
`StardewModdingAPI.deps.json`.
4.
- Windows only: if you use Steam, see the install guide above to enable achievements and - Windows only: if you use Steam, see the install guide above to enable achievements and
overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods.
- Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and - Linux/macOS only: rename the "StardewValley" file (no extension) to "StardewValley-original", and
"StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to
play with mods. play with mods.
When installing on Linux or Mac: When installing on Linux or macOS:
- Make sure Mono is installed (normally the installer checks for you). While it's not required,
many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or
freeze the game.)
- To configure the color scheme, edit the `smapi-internal/config.json` file and see instructions - To configure the color scheme, edit the `smapi-internal/config.json` file and see instructions
there for the 'ColorScheme' setting. there for the 'ColorScheme' setting.

View File

@ -0,0 +1,4 @@
#!/bin/bash
cd "`dirname "$0"`"
internal/linux/SMAPI.Installer

View File

@ -0,0 +1,41 @@
@echo off
setlocal enabledelayedexpansion
SET installerDir="%~dp0"
REM make sure we're not running within a zip folder
echo %installerDir% | findstr /C:"%TEMP%" 1>nul
if %ERRORLEVEL% EQU 0 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
echo.
pause
exit
)
REM make sure an antivirus hasn't deleted the installer DLL
if not exist %installerDir%"internal\windows\SMAPI.Installer.dll" (
echo Oops! SMAPI is missing one of its files. Your antivirus might have deleted it.
echo Missing file: %installerDir%internal\windows\SMAPI.Installer.dll
echo.
pause
exit
)
if not exist %installerDir%"internal\windows\SMAPI.Installer.exe" (
echo Oops! SMAPI is missing one of its files. Your antivirus might have deleted it.
echo Missing file: %installerDir%internal\windows\SMAPI.Installer.exe
echo.
pause
exit
)
REM start installer
internal\windows\SMAPI.Installer.exe
REM keep window open if it failed
if %ERRORLEVEL% NEQ 0 (
echo.
echo Oops! The SMAPI installer seems to have failed. The error details may be shown above.
echo.
pause
exit
)

View File

@ -0,0 +1,6 @@
#!/bin/bash
cd "`dirname "$0"`"
xattr -r -d com.apple.quarantine internal
internal/macOS/SMAPI.Installer

View File

@ -0,0 +1,17 @@
{
"runtimeOptions": {
"tfm": "net5.0",
"includedFrameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "5.0.0",
"rollForward": "latestMinor"
}
],
"configProperties": {
// disable tiered runtime JIT: https://github.com/dotnet/runtime/blob/main/docs/design/features/tiered-compilation.md
// This is disabled by the base game, and causes issues with Harmony patches.
"System.Runtime.TieredCompilation": false
}
}
}

View File

@ -1,24 +0,0 @@
#!/bin/bash
# Run the SMAPI installer through Mono on Linux or Mac.
# Move to script's directory
cd "`dirname "$0"`"
# get cross-distro version of POSIX command
COMMAND=""
if command -v command >/dev/null 2>&1; then
COMMAND="command -v"
elif type type >/dev/null 2>&1; then
COMMAND="type"
fi
# if $TERM is not set to xterm, mono will bail out when attempting to write to the console.
export TERM=xterm
# validate Mono & run installer
if $COMMAND mono >/dev/null 2>&1; then
mono internal/unix-install.exe
else
echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again."
read
fi

View File

@ -1,109 +1,164 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# MonoKickstart Shell Script
# Written by Ethan "flibitijibibo" Lee
# Modified for SMAPI by various contributors
# Move to script's directory ##########
## Initial setup
##########
# move to script's directory
cd "$(dirname "$0")" || exit $? cd "$(dirname "$0")" || exit $?
# Get the system architecture # Whether to avoid opening a separate terminal window, and avoid logging anything to the console.
UNAME=$(uname) # This isn't recommended since you won't see errors, warnings, and update alerts.
ARCH=$(uname -m) SKIP_TERMINAL=false
# MonoKickstart picks the right libfolder, so just execute the right binary. # Whether to avoid opening a separate terminal, but still send the usual log output to the console.
if [ "$UNAME" == "Darwin" ]; then USE_CURRENT_SHELL=false
# ... Except on OSX.
export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/
# El Capitan is a total idiot and wipes this variable out, making the
# Steam overlay disappear. This sidesteps "System Integrity Protection"
# and resets the variable with Valve's own variable (they provided this
# fix by the way, thanks Valve!). Note that you will need to update your
# launch configuration to the script location, NOT just the app location
# (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app).
# -flibit
if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then
export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES"
fi
# this was here before ##########
ln -sf mcs.bin.osx mcs ## Read environment variables
##########
# fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI if [ "$SMAPI_NO_TERMINAL" == "true" ]; then
if [ -f libgdiplus.dylib ]; then SKIP_TERMINAL=true
rm libgdiplus.dylib fi
fi if [ "$SMAPI_USE_CURRENT_SHELL" == "true" ]; then
if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then USE_CURRENT_SHELL=true
ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib fi
fi
# launch SMAPI ##########
cp StardewValley.bin.osx StardewModdingAPI.bin.osx ## Read command-line arguments
open -a Terminal ./StardewModdingAPI.bin.osx "$@" ##########
else while [ "$#" -gt 0 ]; do
# choose launcher case "$1" in
LAUNCHER="" --skip-terminal ) SKIP_TERMINAL=true; shift ;;
if [ "$ARCH" == "x86_64" ]; then --use-current-shell ) USE_CURRENT_SHELL=true; shift ;;
ln -sf mcs.bin.x86_64 mcs -- ) shift; break ;;
cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 * ) shift ;;
LAUNCHER="./StardewModdingAPI.bin.x86_64" esac
else done
ln -sf mcs.bin.x86 mcs
cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 if [ "$SKIP_TERMINAL" == "true" ]; then
LAUNCHER="./StardewModdingAPI.bin.x86" USE_CURRENT_SHELL=true
fi fi
export LAUNCHER
# get cross-distro version of POSIX command ##########
COMMAND="" ## Open terminal if needed
if command -v command 2>/dev/null; then ##########
COMMAND="command -v" # on macOS, make sure we're running in a Terminal
elif type type 2>/dev/null; then # Besides letting the player see errors/warnings/alerts in the console, this is also needed because
COMMAND="type -p" # Steam messes with the PATH.
fi if [ "$(uname)" == "Darwin" ]; then
if [ ! -t 1 ]; then # not open in Terminal (https://stackoverflow.com/q/911168/262123)
# select terminal (prefer xterm for best compatibility, then known supported terminals) # reopen in Terminal if needed
for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do # https://stackoverflow.com/a/29511052/262123
if $COMMAND "$terminal" 2>/dev/null; then if [ "$USE_CURRENT_SHELL" == "false" ]; then
export LAUNCHTERM=$terminal echo "Reopening in the Terminal app..."
break; echo '#!/bin/sh' > /tmp/open-smapi-terminal.command
fi echo "\"$0\" $@ --use-current-shell" >> /tmp/open-smapi-terminal.command
done chmod +x /tmp/open-smapi-terminal.command
cat /tmp/open-smapi-terminal.command
# find the true shell behind x-terminal-emulator open -W /tmp/open-smapi-terminal.command
if [ "$LAUNCHTERM" = "x-terminal-emulator" ]; then rm /tmp/open-smapi-terminal.command
export LAUNCHTERM="$(basename "$(readlink -f $(COMMAND x-terminal-emulator))")" exit 0
fi fi
fi
# run in selected terminal and account for quirks fi
case $LAUNCHTERM in
terminal|termite)
# LAUNCHTERM consumes only one argument after -e ##########
# options containing space characters are unsupported ## Validate assumptions
exec $LAUNCHTERM -e "env TERM=xterm $LAUNCHER $@" ##########
;; # script must be run from the game folder
xterm|konsole|alacritty) if [ ! -f "Stardew Valley.dll" ]; then
# LAUNCHTERM consumes all arguments after -e printf "Oops! SMAPI must be placed in the Stardew Valley game folder.\nSee instructions: https://stardewvalleywiki.com/Modding:Player_Guide";
exec $LAUNCHTERM -e env TERM=xterm $LAUNCHER "$@" read -r
;; exit 1
terminator|xfce4-terminal|mate-terminal) fi
# LAUNCHTERM consumes all arguments after -x
exec $LAUNCHTERM -x env TERM=xterm $LAUNCHER "$@"
;; ##########
gnome-terminal) ## Launch SMAPI
# LAUNCHTERM consumes all arguments after -- ##########
exec $LAUNCHTERM -- env TERM=xterm $LAUNCHER "$@" # macOS
;; if [ "$(uname)" == "Darwin" ]; then
kitty) ./StardewModdingAPI "$@"
# LAUNCHTERM consumes all trailing arguments
exec $LAUNCHTERM env TERM=xterm $LAUNCHER "$@" # Linux
;; else
*) # choose binary file to launch
# If we don't know the terminal, just try to run it in the current shell. LAUNCH_FILE="./StardewModdingAPI"
env TERM=xterm $LAUNCHER "$@" export LAUNCH_FILE
# if THAT fails, launch with no output
if [ $? -eq 127 ]; then # run in terminal
exec $LAUNCHER --no-terminal "$@" if [ "$USE_CURRENT_SHELL" == "false" ]; then
fi # select terminal (prefer xterm for best compatibility, then known supported terminals)
esac for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do
if command -v "$terminal" 2>/dev/null; then
export TERMINAL_NAME=$terminal
break;
fi
done
# find the true shell behind x-terminal-emulator
if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then
TERMINAL_NAME="$(basename "$(readlink -f "$(command -v x-terminal-emulator)")")"
export TERMINAL_NAME
fi
# run in selected terminal and account for quirks
TERMINAL_PATH="$(command -v "$TERMINAL_NAME")"
export TERMINAL_PATH
if [ -x "$TERMINAL_PATH" ]; then
case $TERMINAL_NAME in
terminal|termite)
# consumes only one argument after -e
# options containing space characters are unsupported
exec "$TERMINAL_NAME" -e "env TERM=xterm $LAUNCH_FILE $@"
;;
xterm|konsole|alacritty)
# consumes all arguments after -e
exec "$TERMINAL_NAME" -e env TERM=xterm $LAUNCH_FILE "$@"
;;
terminator|xfce4-terminal|mate-terminal)
# consumes all arguments after -x
exec "$TERMINAL_NAME" -x env TERM=xterm $LAUNCH_FILE "$@"
;;
gnome-terminal)
# consumes all arguments after --
exec "$TERMINAL_NAME" -- env TERM=xterm $LAUNCH_FILE "$@"
;;
kitty)
# consumes all trailing arguments
exec "$TERMINAL_NAME" env TERM=xterm $LAUNCH_FILE "$@"
;;
*)
# If we don't know the terminal, just try to run it in the current shell.
# If THAT fails, launch with no output.
env TERM=xterm $LAUNCH_FILE "$@"
if [ $? -eq 127 ]; then
exec $LAUNCH_FILE --no-terminal "$@"
fi
esac
## terminal isn't executable; fallback to current shell or no terminal
else
echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell."
env TERM=xterm $LAUNCH_FILE "$@"
if [ $? -eq 127 ]; then
exec $LAUNCH_FILE --no-terminal "$@"
fi
fi
# explicitly run without terminal
elif [ "$SKIP_TERMINAL" == "true" ]; then
exec $LAUNCH_FILE --no-terminal "$@"
else
exec $LAUNCH_FILE "$@"
fi
fi fi

View File

@ -1,8 +0,0 @@
@echo off
echo %~dp0 | findstr /C:"%TEMP%" 1>nul
if not errorlevel 1 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
pause
) else (
start /WAIT /B internal/windows-install.exe
)

View File

@ -0,0 +1,54 @@
using System;
using System.Reflection;
using HarmonyLib;
namespace StardewModdingAPI.Internal.Patching
{
/// <summary>Provides base implementation logic for <see cref="IPatcher"/> instances.</summary>
internal abstract class BasePatcher : IPatcher
{
/*********
** Public methods
*********/
/// <inheritdoc />
public abstract void Apply(Harmony harmony, IMonitor monitor);
/*********
** Protected methods
*********/
/// <summary>Get a method and assert that it was found.</summary>
/// <typeparam name="TTarget">The type containing the method.</typeparam>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
protected ConstructorInfo RequireConstructor<TTarget>(params Type[] parameters)
{
return PatchHelper.RequireConstructor<TTarget>(parameters);
}
/// <summary>Get a method and assert that it was found.</summary>
/// <typeparam name="TTarget">The type containing the method.</typeparam>
/// <param name="name">The method name.</param>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
protected MethodInfo RequireMethod<TTarget>(string name, Type[]? parameters = null, Type[]? generics = null)
{
return PatchHelper.RequireMethod<TTarget>(name, parameters, generics);
}
/// <summary>Get a Harmony patch method on the current patcher instance.</summary>
/// <param name="name">The method name.</param>
/// <param name="priority">The patch priority to apply, usually specified using Harmony's <see cref="Priority"/> enum, or <c>null</c> to keep the default value.</param>
protected HarmonyMethod GetHarmonyMethod(string name, int? priority = null)
{
HarmonyMethod method = new(
AccessTools.Method(this.GetType(), name)
?? throw new InvalidOperationException($"Can't find patcher method {PatchHelper.GetMethodString(this.GetType(), name)}.")
);
if (priority.HasValue)
method.priority = priority.Value;
return method;
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using HarmonyLib;
namespace StardewModdingAPI.Internal.Patching
{
/// <summary>Simplifies applying <see cref="IPatcher"/> instances to the game.</summary>
internal static class HarmonyPatcher
{
/*********
** Public methods
*********/
/// <summary>Apply the given Harmony patchers.</summary>
/// <param name="id">The mod ID applying the patchers.</param>
/// <param name="monitor">The monitor with which to log any errors.</param>
/// <param name="patchers">The patchers to apply.</param>
public static Harmony Apply(string id, IMonitor monitor, params IPatcher[] patchers)
{
Harmony harmony = new(id);
foreach (IPatcher patcher in patchers)
{
try
{
patcher.Apply(harmony, monitor);
}
catch (Exception ex)
{
monitor.Log($"Couldn't apply runtime patch '{patcher.GetType().Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error);
monitor.Log($"Technical details:\n{ex.GetLogSummary()}");
}
}
return harmony;
}
}
}

View File

@ -0,0 +1,16 @@
using HarmonyLib;
namespace StardewModdingAPI.Internal.Patching
{
/// <summary>A set of Harmony patches to apply.</summary>
internal interface IPatcher
{
/*********
** Public methods
*********/
/// <summary>Apply the Harmony patches for this instance.</summary>
/// <param name="harmony">The Harmony instance.</param>
/// <param name="monitor">The monitor with which to log any errors.</param>
public void Apply(Harmony harmony, IMonitor monitor);
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using HarmonyLib;
namespace StardewModdingAPI.Internal.Patching
{
/// <summary>Provides utility methods for patching game code with Harmony.</summary>
internal static class PatchHelper
{
/*********
** Public methods
*********/
/// <summary>Get a constructor and assert that it was found.</summary>
/// <typeparam name="TTarget">The type containing the method.</typeparam>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <exception cref="InvalidOperationException">The type has no matching constructor.</exception>
public static ConstructorInfo RequireConstructor<TTarget>(Type[]? parameters = null)
{
return
AccessTools.Constructor(typeof(TTarget), parameters)
?? throw new InvalidOperationException($"Can't find constructor {PatchHelper.GetMethodString(typeof(TTarget), null, parameters)} to patch.");
}
/// <summary>Get a method and assert that it was found.</summary>
/// <typeparam name="TTarget">The type containing the method.</typeparam>
/// <param name="name">The method name.</param>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
/// <exception cref="InvalidOperationException">The type has no matching method.</exception>
public static MethodInfo RequireMethod<TTarget>(string name, Type[]? parameters = null, Type[]? generics = null)
{
return
AccessTools.Method(typeof(TTarget), name, parameters, generics)
?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(typeof(TTarget), name, parameters, generics)} to patch.");
}
/// <summary>Get a human-readable representation of a method target.</summary>
/// <param name="type">The type containing the method.</param>
/// <param name="name">The method name, or <c>null</c> for a constructor.</param>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
public static string GetMethodString(Type type, string? name, Type[]? parameters = null, Type[]? generics = null)
{
StringBuilder str = new();
// type
str.Append(type.FullName);
// method name (if not constructor)
if (name != null)
{
str.Append('.');
str.Append(name);
}
// generics
if (generics?.Any() == true)
{
str.Append('<');
str.Append(string.Join(", ", generics.Select(p => p.FullName)));
str.Append('>');
}
// parameters
if (parameters?.Any() == true)
{
str.Append('(');
str.Append(string.Join(", ", parameters.Select(p => p.FullName)));
str.Append(')');
}
return str.ToString();
}
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>6c16e948-3e5c-47a7-bf4b-07a7469a87a5</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>SMAPI.Internal.Patching</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)BasePatcher.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HarmonyPatcher.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IPatcher.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PatchHelper.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>6c16e948-3e5c-47a7-bf4b-07a7469a87a5</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="SMAPI.Internal.Patching.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -6,10 +6,26 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
/// <summary>The console color scheme options.</summary> /// <summary>The console color scheme options.</summary>
internal class ColorSchemeConfig internal class ColorSchemeConfig
{ {
/*********
** Accessors
*********/
/// <summary>The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</summary> /// <summary>The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</summary>
public MonitorColorScheme UseScheme { get; set; } public MonitorColorScheme UseScheme { get; }
/// <summary>The available console color schemes.</summary> /// <summary>The available console color schemes.</summary>
public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; set; } public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="useScheme">The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</param>
/// <param name="schemes">The available console color schemes.</param>
public ColorSchemeConfig(MonitorColorScheme useScheme, IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> schemes)
{
this.UseScheme = useScheme;
this.Schemes = schemes;
}
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Internal.ConsoleWriting namespace StardewModdingAPI.Internal.ConsoleWriting
@ -11,10 +12,11 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
** Fields ** Fields
*********/ *********/
/// <summary>The console text color for each log level.</summary> /// <summary>The console text color for each log level.</summary>
private readonly IDictionary<ConsoleLogLevel, ConsoleColor> Colors; private readonly IDictionary<ConsoleLogLevel, ConsoleColor>? Colors;
/// <summary>Whether the current console supports color formatting.</summary> /// <summary>Whether the current console supports color formatting.</summary>
private readonly bool SupportsColor; [MemberNotNullWhen(true, nameof(ColorfulConsoleWriter.Colors))]
private bool SupportsColor { get; }
/********* /*********
@ -72,10 +74,9 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
/// <remarks>The colors here should be kept in sync with the SMAPI config file.</remarks> /// <remarks>The colors here should be kept in sync with the SMAPI config file.</remarks>
public static ColorSchemeConfig GetDefaultColorSchemeConfig(MonitorColorScheme useScheme) public static ColorSchemeConfig GetDefaultColorSchemeConfig(MonitorColorScheme useScheme)
{ {
return new ColorSchemeConfig return new ColorSchemeConfig(
{ useScheme: useScheme,
UseScheme = useScheme, schemes: new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
Schemes = new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
{ {
[MonitorColorScheme.DarkBackground] = new Dictionary<ConsoleLogLevel, ConsoleColor> [MonitorColorScheme.DarkBackground] = new Dictionary<ConsoleLogLevel, ConsoleColor>
{ {
@ -98,7 +99,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen [ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
} }
} }
}; );
} }
@ -129,12 +130,12 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
if (schemeID == MonitorColorScheme.AutoDetect) if (schemeID == MonitorColorScheme.AutoDetect)
{ {
schemeID = platform == Platform.Mac schemeID = platform == Platform.Mac
? MonitorColorScheme.LightBackground // MacOS doesn't provide console background color info, but it's usually white. ? MonitorColorScheme.LightBackground // macOS doesn't provide console background color info, but it's usually white.
: ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; : ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground;
} }
// get colors for scheme // get colors for scheme
return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor> scheme) return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor>? scheme)
? scheme ? scheme
: throw new NotSupportedException($"Unknown color scheme '{schemeID}'."); : throw new NotSupportedException($"Unknown color scheme '{schemeID}'.");
} }

View File

@ -0,0 +1,67 @@
using System;
using System.Reflection;
using System.Text.RegularExpressions;
namespace StardewModdingAPI.Internal
{
/// <summary>Provides extension methods for handling exceptions.</summary>
internal static class ExceptionHelper
{
/*********
** Public methods
*********/
/// <summary>Get a string representation of an exception suitable for writing to the error log.</summary>
/// <param name="exception">The error to summarize.</param>
public static string GetLogSummary(this Exception? exception)
{
try
{
string message;
switch (exception)
{
case TypeLoadException ex:
message = $"Failed loading type '{ex.TypeName}': {exception}";
break;
case ReflectionTypeLoadException ex:
string summary = ex.ToString();
foreach (Exception? childEx in ex.LoaderExceptions)
summary += $"\n\n{childEx?.GetLogSummary()}";
message = summary;
break;
default:
message = exception?.ToString() ?? $"<null exception>\n{Environment.StackTrace}";
break;
}
return ExceptionHelper.SimplifyExtensionMessage(message);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed handling {exception?.GetType().FullName} (original message: {exception?.Message})", ex);
}
}
/// <summary>Simplify common patterns in exception log messages that don't convey useful info.</summary>
/// <param name="message">The log message to simplify.</param>
public static string SimplifyExtensionMessage(string message)
{
// remove namespace for core exception types
message = Regex.Replace(
message,
@"(?:StardewModdingAPI\.Framework\.Exceptions|Microsoft\.Xna\.Framework|System|System\.IO)\.([a-zA-Z]+Exception):",
"$1:"
);
// remove unneeded root build paths for SMAPI and Stardew Valley
message = message
.Replace(@"E:\source\_Stardew\SMAPI\src\", "")
.Replace(@"C:\GitlabRunner\builds\Gq5qA5P4\0\ConcernedApe\", "");
// remove placeholder info in Linux/macOS stack traces
return message
.Replace(@"<filename unknown>:0", "");
}
}
}

View File

@ -14,5 +14,6 @@
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ConsoleLogLevel.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ConsoleLogLevel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\IConsoleWriter.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\IConsoleWriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExceptionHelper.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,7 @@
// <generated /> // <generated />
using Microsoft.CodeAnalysis; // ReSharper disable All -- generated code
using System; using System;
using Microsoft.CodeAnalysis;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {

View File

@ -1,12 +1,14 @@
// <generated /> // <generated />
using Microsoft.CodeAnalysis; // ReSharper disable All -- generated code
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
@ -51,17 +53,17 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
{ {
var projects = new HashSet<Project>(); var projects = new HashSet<Project>();
foreach (var document in documents) foreach (Document document in documents)
{ {
projects.Add(document.Project); projects.Add(document.Project);
} }
var diagnostics = new List<Diagnostic>(); var diagnostics = new List<Diagnostic>();
foreach (var project in projects) foreach (Project project in projects)
{ {
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); CompilationWithAnalyzers compilationWithAnalyzers = project.GetCompilationAsync().Result!.WithAnalyzers(ImmutableArray.Create(analyzer));
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
foreach (var diag in diags) foreach (Diagnostic diag in diags)
{ {
if (diag.Location == Location.None || diag.Location.IsInMetadata) if (diag.Location == Location.None || diag.Location.IsInMetadata)
{ {
@ -71,8 +73,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
for (int i = 0; i < documents.Length; i++) for (int i = 0; i < documents.Length; i++)
{ {
var document = documents[i]; Document document = documents[i];
var tree = document.GetSyntaxTreeAsync().Result; SyntaxTree? tree = document.GetSyntaxTreeAsync().Result;
if (tree == diag.Location.SourceTree) if (tree == diag.Location.SourceTree)
{ {
diagnostics.Add(diag); diagnostics.Add(diag);
@ -113,7 +115,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
throw new ArgumentException("Unsupported Language"); throw new ArgumentException("Unsupported Language");
} }
var project = CreateProject(sources, language); Project project = CreateProject(sources, language);
var documents = project.Documents.ToArray(); var documents = project.Documents.ToArray();
if (sources.Length != documents.Length) if (sources.Length != documents.Length)
@ -124,17 +126,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
return documents; return documents;
} }
/// <summary>
/// Create a Document from a string through creating a project that contains it.
/// </summary>
/// <param name="source">Classes in the form of a string</param>
/// <param name="language">The language the source code is in</param>
/// <returns>A Document created from the source string</returns>
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
{
return CreateProject(new[] { source }, language).Documents.First();
}
/// <summary> /// <summary>
/// Create a project using the inputted strings as sources. /// Create a project using the inputted strings as sources.
/// </summary> /// </summary>
@ -146,9 +137,9 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
string fileNamePrefix = DefaultFilePathPrefix; string fileNamePrefix = DefaultFilePathPrefix;
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
var projectId = ProjectId.CreateNewId(debugName: TestProjectName); ProjectId projectId = ProjectId.CreateNewId(debugName: TestProjectName);
var solution = new AdhocWorkspace() Solution solution = new AdhocWorkspace()
.CurrentSolution .CurrentSolution
.AddProject(projectId, TestProjectName, TestProjectName, language) .AddProject(projectId, TestProjectName, TestProjectName, language)
.AddMetadataReference(projectId, DiagnosticVerifier.SelfReference) .AddMetadataReference(projectId, DiagnosticVerifier.SelfReference)
@ -158,14 +149,14 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
.AddMetadataReference(projectId, CodeAnalysisReference); .AddMetadataReference(projectId, CodeAnalysisReference);
int count = 0; int count = 0;
foreach (var source in sources) foreach (string source in sources)
{ {
var newFileName = fileNamePrefix + count + "." + fileExt; string newFileName = fileNamePrefix + count + "." + fileExt;
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
count++; count++;
} }
return solution.GetProject(projectId); return solution.GetProject(projectId)!;
} }
#endregion #endregion
} }

View File

@ -1,4 +1,6 @@
// <generated /> // <generated />
// ReSharper disable All -- generated code
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -17,18 +19,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
/// <summary> /// <summary>
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class /// Get the CSharp analyzer being tested - to be implemented in non-abstract class
/// </summary> /// </summary>
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() protected abstract DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer();
{
return null;
}
/// <summary>
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
/// </summary>
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
{
return null;
}
#endregion #endregion
#region Verifier wrappers #region Verifier wrappers
@ -41,18 +32,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
/// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param> /// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
{ {
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); this.VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzer(), expected);
}
/// <summary>
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
/// </summary>
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
{
VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
} }
/// <summary> /// <summary>
@ -65,8 +45,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param> /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
{ {
var diagnostics = GetSortedDiagnostics(sources, language, analyzer); var diagnostics = DiagnosticVerifier.GetSortedDiagnostics(sources, language, analyzer);
VerifyDiagnosticResults(diagnostics, analyzer, expected); DiagnosticVerifier.VerifyDiagnosticResults(diagnostics, analyzer, expected);
} }
#endregion #endregion
@ -86,7 +66,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
if (expectedCount != actualCount) if (expectedCount != actualCount)
{ {
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; string diagnosticsOutput = actualResults.Any() ? DiagnosticVerifier.FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
@ -103,12 +83,12 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
FormatDiagnostics(analyzer, actual))); DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
} }
} }
else else
{ {
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); DiagnosticVerifier.VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
var additionalLocations = actual.AdditionalLocations.ToArray(); var additionalLocations = actual.AdditionalLocations.ToArray();
if (additionalLocations.Length != expected.Locations.Length - 1) if (additionalLocations.Length != expected.Locations.Length - 1)
@ -116,12 +96,12 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
expected.Locations.Length - 1, additionalLocations.Length, expected.Locations.Length - 1, additionalLocations.Length,
FormatDiagnostics(analyzer, actual))); DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
} }
for (int j = 0; j < additionalLocations.Length; ++j) for (int j = 0; j < additionalLocations.Length; ++j)
{ {
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); DiagnosticVerifier.VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
} }
} }
@ -129,21 +109,21 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); expected.Id, actual.Id, DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
} }
if (actual.Severity != expected.Severity) if (actual.Severity != expected.Severity)
{ {
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); expected.Severity, actual.Severity, DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
} }
if (actual.GetMessage() != expected.Message) if (actual.GetMessage() != expected.Message)
{ {
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); expected.Message, actual.GetMessage(), DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
} }
} }
} }
@ -161,7 +141,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); expected.Path, actualSpan.Path, DiagnosticVerifier.FormatDiagnostics(analyzer, diagnostic)));
var actualLinePosition = actualSpan.StartLinePosition; var actualLinePosition = actualSpan.StartLinePosition;
@ -172,7 +152,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); expected.Line, actualLinePosition.Line + 1, DiagnosticVerifier.FormatDiagnostics(analyzer, diagnostic)));
} }
} }
@ -183,7 +163,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
Assert.IsTrue(false, Assert.IsTrue(false,
string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); expected.Column, actualLinePosition.Character + 1, DiagnosticVerifier.FormatDiagnostics(analyzer, diagnostic)));
} }
} }
} }
@ -201,7 +181,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
var builder = new StringBuilder(); var builder = new StringBuilder();
for (int i = 0; i < diagnostics.Length; ++i) for (int i = 0; i < diagnostics.Length; ++i)
{ {
builder.AppendLine("// " + diagnostics[i].ToString()); builder.AppendLine("// " + diagnostics[i]);
var analyzerType = analyzer.GetType(); var analyzerType = analyzer.GetType();
var rules = analyzer.SupportedDiagnostics; var rules = analyzer.SupportedDiagnostics;
@ -220,11 +200,10 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
Assert.IsTrue(location.IsInSource, Assert.IsTrue(location.IsInSource,
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
builder.AppendFormat("{0}({1}, {2}, {3}.{4})", builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
resultMethodName, "GetCSharpResultAt",
linePosition.Line + 1, linePosition.Line + 1,
linePosition.Character + 1, linePosition.Character + 1,
analyzerType.Name, analyzerType.Name,

View File

@ -1,10 +1,8 @@
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Netcode namespace Netcode
{ {
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary> /// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary>
public class NetCollection<T> : Collection<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { } public class NetCollection<T> : Collection<T> { }
} }

View File

@ -7,10 +7,13 @@ namespace Netcode
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf> public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
{ {
/// <summary>The synchronised value.</summary> /// <summary>The synchronised value.</summary>
public T Value { get; set; } public T? Value { get; set; }
/// <summary>Implicitly convert a net field to the its type.</summary> /// <summary>Implicitly convert a net field to the its type.</summary>
/// <param name="field">The field to convert.</param> /// <param name="field">The field to convert.</param>
public static implicit operator T(NetFieldBase<T, TSelf> field) => field.Value; public static implicit operator T?(NetFieldBase<T, TSelf> field)
{
return field.Value;
}
} }
} }

View File

@ -1,9 +1,8 @@
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
namespace Netcode namespace Netcode
{ {
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary> /// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary>
public class NetList<T> : List<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { } public class NetList<T> : List<T> { }
} }

View File

@ -1,5 +1,5 @@
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
#pragma warning disable 649 // (never assigned) -- only used to test type conversions // ReSharper disable UnusedMember.Global -- used dynamically for unit tests
using System.Collections.Generic; using System.Collections.Generic;
namespace StardewValley namespace StardewValley
@ -8,6 +8,6 @@ namespace StardewValley
internal class Farmer internal class Farmer
{ {
/// <summary>A sample field which should be replaced with a different property.</summary> /// <summary>A sample field which should be replaced with a different property.</summary>
public readonly IDictionary<string, int[]> friendships; public readonly IDictionary<string, int[]> friendships = new Dictionary<string, int[]>();
} }
} }

View File

@ -1,4 +1,5 @@
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
// ReSharper disable UnusedMember.Global -- used dynamically for unit tests
using Netcode; using Netcode;
namespace StardewValley namespace StardewValley
@ -7,27 +8,27 @@ namespace StardewValley
public class Item public class Item
{ {
/// <summary>A net int field with an equivalent non-net <c>Category</c> property.</summary> /// <summary>A net int field with an equivalent non-net <c>Category</c> property.</summary>
public readonly NetInt category = new NetInt { Value = 42 }; public readonly NetInt category = new() { Value = 42 };
/// <summary>A generic net int field with no equivalent non-net property.</summary> /// <summary>A generic net int field with no equivalent non-net property.</summary>
public readonly NetInt netIntField = new NetInt { Value = 42 }; public readonly NetInt netIntField = new() { Value = 42 };
/// <summary>A generic net ref field with no equivalent non-net property.</summary> /// <summary>A generic net ref field with no equivalent non-net property.</summary>
public readonly NetRef<object> netRefField = new NetRef<object>(); public readonly NetRef<object> netRefField = new();
/// <summary>A generic net int property with no equivalent non-net property.</summary> /// <summary>A generic net int property with no equivalent non-net property.</summary>
public NetInt netIntProperty = new NetInt { Value = 42 }; public NetInt netIntProperty = new() { Value = 42 };
/// <summary>A generic net ref property with no equivalent non-net property.</summary> /// <summary>A generic net ref property with no equivalent non-net property.</summary>
public NetRef<object> netRefProperty { get; } = new NetRef<object>(); public NetRef<object> netRefProperty { get; } = new();
/// <summary>A sample net list.</summary> /// <summary>A sample net list.</summary>
public readonly NetList<int> netList = new NetList<int>(); public readonly NetList<int> netList = new();
/// <summary>A sample net object list.</summary> /// <summary>A sample net object list.</summary>
public readonly NetObjectList<int> netObjectList = new NetObjectList<int>(); public readonly NetObjectList<int> netObjectList = new();
/// <summary>A sample net collection.</summary> /// <summary>A sample net collection.</summary>
public readonly NetCollection<int> netCollection = new NetCollection<int>(); public readonly NetCollection<int> netCollection = new();
} }
} }

View File

@ -7,6 +7,6 @@ namespace StardewValley
public class Object : Item public class Object : Item
{ {
/// <summary>A net int field with an equivalent non-net property.</summary> /// <summary>A net int field with an equivalent non-net property.</summary>
public NetInt type = new NetInt { Value = 42 }; public NetInt type = new() { Value = 42 };
} }
} }

View File

@ -87,13 +87,13 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
[TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")]
[TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type [TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type
[TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")] [TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")]
[TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetInt", "int")] // ↓ explicit conversion to invalid type [TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetFieldBase", "int")] // ↓ explicit conversion to invalid type
[TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")] [TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")]
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType) public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType)
{ {
// arrange // arrange
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
DiagnosticResult expected = new DiagnosticResult DiagnosticResult expected = new()
{ {
Id = "AvoidImplicitNetFieldCast", Id = "AvoidImplicitNetFieldCast",
Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.", Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.",
@ -135,7 +135,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
{ {
// arrange // arrange
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
DiagnosticResult expected = new DiagnosticResult DiagnosticResult expected = new()
{ {
Id = "AvoidNetField", Id = "AvoidNetField",
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/package/avoid-net-field for details.", Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/package/avoid-net-field for details.",

View File

@ -64,7 +64,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
{ {
// arrange // arrange
string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText); string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
DiagnosticResult expected = new DiagnosticResult DiagnosticResult expected = new()
{ {
Id = "AvoidObsoleteField", Id = "AvoidObsoleteField",
Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/package/avoid-obsolete-field for details.", Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/package/avoid-obsolete-field for details.",

View File

@ -1,18 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0"> <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -20,5 +16,4 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\common.targets" /> <Import Project="..\..\build\common.targets" />
</Project> </Project>

View File

@ -0,0 +1,7 @@
## Release 2.1.0
### New Rules
Rule ID | Category | Severity | Notes
------------------------- | ------------------ | -------- | ------------------------------------------------------------
AvoidImplicitNetFieldCast | SMAPI.CommonErrors | Warning | See [documentation](https://smapi.io/package/code-warnings).
AvoidNetField | SMAPI.CommonErrors | Warning | See [documentation](https://smapi.io/package/code-warnings).
AvoidObsoleteField | SMAPI.CommonErrors | Warning | See [documentation](https://smapi.io/package/code-warnings).

View File

@ -40,8 +40,8 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
// invalid // invalid
fromExpression = null; fromExpression = null;
fromType = default(TypeInfo); fromType = default;
toType = default(TypeInfo); toType = default;
return false; return false;
} }
@ -64,7 +64,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
} }
// conditional access // conditional access
if (node is ConditionalAccessExpressionSyntax conditionalAccess && conditionalAccess.WhenNotNull is MemberBindingExpressionSyntax conditionalBinding) if (node is ConditionalAccessExpressionSyntax { WhenNotNull: MemberBindingExpressionSyntax conditionalBinding } conditionalAccess)
{ {
declaringType = semanticModel.GetTypeInfo(conditionalAccess.Expression).Type; declaringType = semanticModel.GetTypeInfo(conditionalAccess.Expression).Type;
memberType = semanticModel.GetTypeInfo(node); memberType = semanticModel.GetTypeInfo(node);
@ -74,7 +74,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
// invalid // invalid
declaringType = null; declaringType = null;
memberType = default(TypeInfo); memberType = default;
memberName = null; memberName = null;
return false; return false;
} }

View File

@ -132,7 +132,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
}; };
/// <summary>The diagnostic info for an implicit net field cast.</summary> /// <summary>The diagnostic info for an implicit net field cast.</summary>
private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor( private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new(
id: "AvoidImplicitNetFieldCast", id: "AvoidImplicitNetFieldCast",
title: "Netcode types shouldn't be implicitly converted", title: "Netcode types shouldn't be implicitly converted",
messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.", messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.",
@ -143,7 +143,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
); );
/// <summary>The diagnostic info for an avoidable net field access.</summary> /// <summary>The diagnostic info for an avoidable net field access.</summary>
private readonly DiagnosticDescriptor AvoidNetFieldRule = new DiagnosticDescriptor( private readonly DiagnosticDescriptor AvoidNetFieldRule = new(
id: "AvoidNetField", id: "AvoidNetField",
title: "Avoid Netcode types when possible", title: "Avoid Netcode types when possible",
messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/package/avoid-net-field for details.", messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/package/avoid-net-field for details.",
@ -174,6 +174,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/// <param name="context">The analysis context.</param> /// <param name="context">The analysis context.</param>
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction( context.RegisterSyntaxNodeAction(
this.AnalyzeMemberAccess, this.AnalyzeMemberAccess,
SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.SimpleMemberAccessExpression,
@ -224,10 +227,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
// warn: implicit conversion // warn: implicit conversion
if (this.IsInvalidConversion(memberType.Type, memberType.ConvertedType)) if (this.IsInvalidConversion(memberType.Type, memberType.ConvertedType))
{
context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType));
return;
}
}); });
} }
@ -312,7 +312,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
return false; return false;
// conversion to implemented interface is OK // conversion to implemented interface is OK
if (fromType.AllInterfaces.Contains(toType)) if (fromType.AllInterfaces.Contains(toType, SymbolEqualityComparer.Default))
return false; return false;
// avoid any other conversions // avoid any other conversions

View File

@ -24,7 +24,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/// <summary>Describes the diagnostic rule covered by the analyzer.</summary> /// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor> private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
{ {
["AvoidObsoleteField"] = new DiagnosticDescriptor( ["AvoidObsoleteField"] = new(
id: "AvoidObsoleteField", id: "AvoidObsoleteField",
title: "Reference to obsolete field", title: "Reference to obsolete field",
messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/package/avoid-obsolete-field for details.", messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/package/avoid-obsolete-field for details.",
@ -56,6 +56,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/// <param name="context">The analysis context.</param> /// <param name="context">The analysis context.</param>
public override void Initialize(AnalysisContext context) public override void Initialize(AnalysisContext context)
{ {
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction( context.RegisterSyntaxNodeAction(
this.AnalyzeObsoleteFields, this.AnalyzeObsoleteFields,
SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.SimpleMemberAccessExpression,
@ -74,7 +77,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
try try
{ {
// get reference info // get reference info
if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out _, out string memberName))
return; return;
// suggest replacement // suggest replacement

View File

@ -9,8 +9,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />
<PackageReference Update="NETStandard.Library" PrivateAssets="all" /> </ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,12 @@ using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
using Microsoft.Build.Utilities; using Microsoft.Build.Utilities;
using Newtonsoft.Json;
using StardewModdingAPI.ModBuildConfig.Framework; using StardewModdingAPI.ModBuildConfig.Framework;
using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.ModBuildConfig namespace StardewModdingAPI.ModBuildConfig
{ {
@ -17,6 +22,10 @@ namespace StardewModdingAPI.ModBuildConfig
/********* /*********
** Accessors ** Accessors
*********/ *********/
/// <summary>The name (without extension or path) of the current mod's DLL.</summary>
[Required]
public string ModDllName { get; set; }
/// <summary>The name of the mod folder.</summary> /// <summary>The name of the mod folder.</summary>
[Required] [Required]
public string ModFolderName { get; set; } public string ModFolderName { get; set; }
@ -45,9 +54,15 @@ namespace StardewModdingAPI.ModBuildConfig
[Required] [Required]
public bool EnableModZip { get; set; } public bool EnableModZip { get; set; }
/// <summary>Custom comma-separated regex patterns matching files to ignore when deploying or zipping the mod.</summary> /// <summary>A comma-separated list of regex patterns matching files to ignore when deploying or zipping the mod.</summary>
public string IgnoreModFilePatterns { get; set; } public string IgnoreModFilePatterns { get; set; }
/// <summary>A comma-separated list of relative file paths to ignore when deploying or zipping the mod.</summary>
public string IgnoreModFilePaths { get; set; }
/// <summary>A comma-separated list of <see cref="ExtraAssemblyTypes"/> values which indicate which extra DLLs to bundle.</summary>
public string BundleExtraAssemblies { get; set; }
/********* /*********
** Public methods ** Public methods
@ -64,16 +79,52 @@ namespace StardewModdingAPI.ModBuildConfig
this.Log.LogMessage(MessageImportance.High, $"[mod build package] Handling build with options {string.Join(", ", properties)}"); this.Log.LogMessage(MessageImportance.High, $"[mod build package] Handling build with options {string.Join(", ", properties)}");
} }
// skip if nothing to do
// (This must be checked before the manifest validation, to allow cases like unit test projects.)
if (!this.EnableModDeploy && !this.EnableModZip) if (!this.EnableModDeploy && !this.EnableModZip)
return true; // nothing to do return true;
// validate the manifest file
IManifest manifest;
{
try
{
string manifestPath = Path.Combine(this.ProjectDir, "manifest.json");
if (!new JsonHelper().ReadJsonFileIfExists(manifestPath, out Manifest rawManifest))
{
this.Log.LogError("[mod build package] The mod's manifest.json file doesn't exist.");
return false;
}
manifest = rawManifest;
}
catch (JsonReaderException ex)
{
// log the inner exception, otherwise the message will be generic
Exception exToShow = ex.InnerException ?? ex;
this.Log.LogError($"[mod build package] The mod's manifest.json file isn't valid JSON: {exToShow.Message}");
return false;
}
// validate manifest fields
if (!ManifestValidator.TryValidateFields(manifest, out string error))
{
this.Log.LogError($"[mod build package] The mod's manifest.json file is invalid: {error}");
return false;
}
}
// deploy files
try try
{ {
// parse extra DLLs to bundle
ExtraAssemblyTypes bundleAssemblyTypes = this.GetExtraAssembliesToBundleOption();
// parse ignore patterns // parse ignore patterns
string[] ignoreFilePaths = this.GetCustomIgnoreFilePaths().ToArray();
Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray(); Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray();
// get mod info // get mod info
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip); ModFileManager package = new(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip);
// deploy mod files // deploy mod files
if (this.EnableModDeploy) if (this.EnableModDeploy)
@ -86,7 +137,7 @@ namespace StardewModdingAPI.ModBuildConfig
// create release zip // create release zip
if (this.EnableModZip) if (this.EnableModZip)
{ {
string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModFolderName} {package.GetManifestVersion()}.zip"); string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModFolderName} {manifest.Version}.zip");
string zipPath = Path.Combine(this.ModZipPath, zipName); string zipPath = Path.Combine(this.ModZipPath, zipName);
this.Log.LogMessage(MessageImportance.High, $"[mod build package] Generating the release zip at {zipPath}..."); this.Log.LogMessage(MessageImportance.High, $"[mod build package] Generating the release zip at {zipPath}...");
@ -134,6 +185,28 @@ namespace StardewModdingAPI.ModBuildConfig
} }
} }
/// <summary>Parse the extra assembly types which should be bundled with the mod.</summary>
private ExtraAssemblyTypes GetExtraAssembliesToBundleOption()
{
ExtraAssemblyTypes flags = ExtraAssemblyTypes.None;
if (!string.IsNullOrWhiteSpace(this.BundleExtraAssemblies))
{
foreach (string raw in this.BundleExtraAssemblies.Split(','))
{
if (!Enum.TryParse(raw, out ExtraAssemblyTypes type))
{
this.Log.LogWarning($"[mod build package] Ignored invalid <{nameof(this.BundleExtraAssemblies)}> value '{raw}', expected one of '{string.Join("', '", Enum.GetNames(typeof(ExtraAssemblyTypes)))}'.");
continue;
}
flags |= type;
}
}
return flags;
}
/// <summary>Get the custom ignore patterns provided by the user.</summary> /// <summary>Get the custom ignore patterns provided by the user.</summary>
private IEnumerable<Regex> GetCustomIgnorePatterns() private IEnumerable<Regex> GetCustomIgnorePatterns()
{ {
@ -157,6 +230,29 @@ namespace StardewModdingAPI.ModBuildConfig
} }
} }
/// <summary>Get the custom relative file paths provided by the user to ignore.</summary>
private IEnumerable<string> GetCustomIgnoreFilePaths()
{
if (string.IsNullOrWhiteSpace(this.IgnoreModFilePaths))
yield break;
foreach (string raw in this.IgnoreModFilePaths.Split(','))
{
string path;
try
{
path = PathUtilities.NormalizePath(raw);
}
catch (Exception ex)
{
this.Log.LogWarning($"[mod build package] Ignored invalid <{nameof(this.IgnoreModFilePaths)}> path {raw}:\n{ex}");
continue;
}
yield return path;
}
}
/// <summary>Copy the mod files into the game's mod folder.</summary> /// <summary>Copy the mod files into the game's mod folder.</summary>
/// <param name="files">The files to include.</param> /// <param name="files">The files to include.</param>
/// <param name="modFolderPath">The folder path to create with the mod files.</param> /// <param name="modFolderPath">The folder path to create with the mod files.</param>
@ -167,8 +263,7 @@ namespace StardewModdingAPI.ModBuildConfig
string fromPath = entry.Value.FullName; string fromPath = entry.Value.FullName;
string toPath = Path.Combine(modFolderPath, entry.Key); string toPath = Path.Combine(modFolderPath, entry.Key);
// ReSharper disable once AssignNullToNotNullAttribute -- not applicable in this context Directory.CreateDirectory(Path.GetDirectoryName(toPath)!);
Directory.CreateDirectory(Path.GetDirectoryName(toPath));
File.Copy(fromPath, toPath, overwrite: true); File.Copy(fromPath, toPath, overwrite: true);
} }
@ -186,7 +281,7 @@ namespace StardewModdingAPI.ModBuildConfig
// create zip file // create zip file
Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!); Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!);
using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write); using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write);
using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create); using ZipArchive archive = new(zipStream, ZipArchiveMode.Create);
foreach (var fileEntry in files) foreach (var fileEntry in files)
{ {

View File

@ -0,0 +1,21 @@
using System;
namespace StardewModdingAPI.ModBuildConfig.Framework
{
/// <summary>An extra assembly type for the <see cref="DeployModTask.BundleExtraAssemblies"/> field.</summary>
[Flags]
internal enum ExtraAssemblyTypes
{
/// <summary>Don't include extra assemblies.</summary>
None = 0,
/// <summary>Assembly files which are part of MonoGame, SMAPI, or Stardew Valley.</summary>
Game = 1,
/// <summary>Assembly files whose names start with <c>Microsoft.*</c> or <c>System.*</c>.</summary>
System = 2,
/// <summary>Assembly files which don't match any other category.</summary>
ThirdParty = 4
}
}

View File

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.ModBuildConfig.Framework namespace StardewModdingAPI.ModBuildConfig.Framework
@ -21,6 +19,45 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// <summary>The files that are part of the package.</summary> /// <summary>The files that are part of the package.</summary>
private readonly IDictionary<string, FileInfo> Files; private readonly IDictionary<string, FileInfo> Files;
/// <summary>The file extensions used by assembly files.</summary>
private readonly ISet<string> AssemblyFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".dll",
".exe",
".pdb",
".xml"
};
/// <summary>The DLLs which match the <see cref="ExtraAssemblyTypes.Game"/> type.</summary>
private readonly ISet<string> GameDllNames = new HashSet<string>
{
// SMAPI
"0Harmony",
"Mono.Cecil",
"Mono.Cecil.Mdb",
"Mono.Cecil.Pdb",
"MonoMod.Common",
"Newtonsoft.Json",
"StardewModdingAPI",
"SMAPI.Toolkit",
"SMAPI.Toolkit.CoreInterfaces",
"TMXTile",
// game + framework
"BmFont",
"FAudio-CS",
"GalaxyCSharp",
"GalaxyCSharpGlue",
"Lidgren.Network",
"MonoGame.Framework",
"SkiaSharp",
"Stardew Valley",
"StardewValley.GameData",
"Steamworks.NET",
"TextCopy",
"xTile"
};
/********* /*********
** Public methods ** Public methods
@ -28,10 +65,13 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="projectDir">The folder containing the project files.</param> /// <param name="projectDir">The folder containing the project files.</param>
/// <param name="targetDir">The folder containing the build output.</param> /// <param name="targetDir">The folder containing the build output.</param>
/// <param name="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param>
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param> /// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
/// <param name="bundleAssemblyTypes">The extra assembly types which should be bundled with the mod.</param>
/// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param>
/// <param name="validateRequiredModFiles">Whether to validate that required mod files like the manifest are present.</param> /// <param name="validateRequiredModFiles">Whether to validate that required mod files like the manifest are present.</param>
/// <exception cref="UserErrorException">The mod package isn't valid.</exception> /// <exception cref="UserErrorException">The mod package isn't valid.</exception>
public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns, bool validateRequiredModFiles) public ModFileManager(string projectDir, string targetDir, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, ExtraAssemblyTypes bundleAssemblyTypes, string modDllName, bool validateRequiredModFiles)
{ {
this.Files = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase); this.Files = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
@ -47,7 +87,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
string relativePath = entry.Item1; string relativePath = entry.Item1;
FileInfo file = entry.Item2; FileInfo file = entry.Item2;
if (!this.ShouldIgnore(file, relativePath, ignoreFilePatterns)) if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName))
this.Files[relativePath] = file; this.Files[relativePath] = file;
} }
@ -71,16 +111,6 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
return new Dictionary<string, FileInfo>(this.Files, StringComparer.OrdinalIgnoreCase); return new Dictionary<string, FileInfo>(this.Files, StringComparer.OrdinalIgnoreCase);
} }
/// <summary>Get a semantic version from the mod manifest.</summary>
/// <exception cref="UserErrorException">The manifest is missing or invalid.</exception>
public string GetManifestVersion()
{
if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile) || !new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest manifest))
throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor
return manifest.Version.ToString();
}
/********* /*********
** Private methods ** Private methods
@ -94,7 +124,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
// project manifest // project manifest
bool hasProjectManifest = false; bool hasProjectManifest = false;
{ {
FileInfo manifest = new FileInfo(Path.Combine(projectDir, this.ManifestFileName)); FileInfo manifest = new(Path.Combine(projectDir, this.ManifestFileName));
if (manifest.Exists) if (manifest.Exists)
{ {
yield return Tuple.Create(this.ManifestFileName, manifest); yield return Tuple.Create(this.ManifestFileName, manifest);
@ -104,7 +134,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
// project i18n files // project i18n files
bool hasProjectTranslations = false; bool hasProjectTranslations = false;
DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n")); DirectoryInfo translationsFolder = new(Path.Combine(projectDir, "i18n"));
if (translationsFolder.Exists) if (translationsFolder.Exists)
{ {
foreach (FileInfo file in translationsFolder.EnumerateFiles()) foreach (FileInfo file in translationsFolder.EnumerateFiles())
@ -114,7 +144,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
// project assets folder // project assets folder
bool hasAssetsFolder = false; bool hasAssetsFolder = false;
DirectoryInfo assetsFolder = new DirectoryInfo(Path.Combine(projectDir, "assets")); DirectoryInfo assetsFolder = new(Path.Combine(projectDir, "assets"));
if (assetsFolder.Exists) if (assetsFolder.Exists)
{ {
foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories)) foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories))
@ -126,7 +156,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
} }
// build output // build output
DirectoryInfo buildFolder = new DirectoryInfo(targetDir); DirectoryInfo buildFolder = new(targetDir);
foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories)) foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{ {
// get path info // get path info
@ -149,36 +179,83 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// <summary>Get whether a build output file should be ignored.</summary> /// <summary>Get whether a build output file should be ignored.</summary>
/// <param name="file">The file to check.</param> /// <param name="file">The file to check.</param>
/// <param name="relativePath">The file's relative path in the package.</param> /// <param name="relativePath">The file's relative path in the package.</param>
/// <param name="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param>
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param> /// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
private bool ShouldIgnore(FileInfo file, string relativePath, Regex[] ignoreFilePatterns) /// <param name="bundleAssemblyTypes">The extra assembly types which should be bundled with the mod.</param>
/// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param>
private bool ShouldIgnore(FileInfo file, string relativePath, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, ExtraAssemblyTypes bundleAssemblyTypes, string modDllName)
{ {
return // apply custom patterns
// release zips if (ignoreFilePaths.Any(p => p == relativePath) || ignoreFilePatterns.Any(p => p.IsMatch(relativePath)))
this.EqualsInvariant(file.Extension, ".zip") return true;
// Harmony (bundled into SMAPI) // ignore unneeded files
|| this.EqualsInvariant(file.Name, "0Harmony.dll") {
bool shouldIgnore =
// release zips
this.EqualsInvariant(file.Extension, ".zip")
// Json.NET (bundled into SMAPI) // *.deps.json (only SMAPI's top-level one is used)
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || file.Name.EndsWith(".deps.json")
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.pdb")
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")
// mod translation class builder (not used at runtime) // code analysis files
|| this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.dll") || file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.OrdinalIgnoreCase)
|| this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.pdb") || file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.OrdinalIgnoreCase)
|| this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.xml")
// code analysis files // translation class builder (not used at runtime)
|| file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.OrdinalIgnoreCase) || (
|| file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.OrdinalIgnoreCase) file.Name.StartsWith("Pathoschild.Stardew.ModTranslationClassBuilder")
&& this.AssemblyFileExtensions.Contains(file.Extension)
)
// OS metadata files // OS metadata files
|| this.EqualsInvariant(file.Name, ".DS_Store") || this.EqualsInvariant(file.Name, ".DS_Store")
|| this.EqualsInvariant(file.Name, "Thumbs.db") || this.EqualsInvariant(file.Name, "Thumbs.db");
if (shouldIgnore)
return true;
}
// custom ignore patterns // ignore by assembly type
|| ignoreFilePatterns.Any(p => p.IsMatch(relativePath)); ExtraAssemblyTypes type = this.GetExtraAssemblyType(file, modDllName);
switch (bundleAssemblyTypes)
{
// Only explicitly-referenced assemblies are in the build output. These should be added to the zip,
// since it's possible the game won't load them (except game assemblies which will always be loaded
// separately). If they're already loaded, SMAPI will just ignore them.
case ExtraAssemblyTypes.None:
if (type is ExtraAssemblyTypes.Game)
return true;
break;
// All assemblies are in the build output (due to how .NET builds references), but only those which
// match the bundled type should be in the zip.
default:
if (type != ExtraAssemblyTypes.None && !bundleAssemblyTypes.HasFlag(type))
return true;
break;
}
return false;
}
/// <summary>Get the extra assembly type for a file, assuming that the user specified one or more extra types to bundle.</summary>
/// <param name="file">The file to check.</param>
/// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param>
private ExtraAssemblyTypes GetExtraAssemblyType(FileInfo file, string modDllName)
{
string baseName = Path.GetFileNameWithoutExtension(file.Name);
string extension = file.Extension;
if (baseName == modDllName || !this.AssemblyFileExtensions.Contains(extension))
return ExtraAssemblyTypes.None;
if (this.GameDllNames.Contains(baseName))
return ExtraAssemblyTypes.Game;
if (baseName.StartsWith("System.", StringComparison.OrdinalIgnoreCase) || baseName.StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase))
return ExtraAssemblyTypes.System;
return ExtraAssemblyTypes.ThirdParty;
} }
/// <summary>Get whether a string is equal to another case-insensitively.</summary> /// <summary>Get whether a string is equal to another case-insensitively.</summary>

View File

@ -2,33 +2,35 @@
<PropertyGroup> <PropertyGroup>
<!--build--> <!--build-->
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace> <RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
<TargetFramework>net45</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<!--NuGet package--> <!--NuGet package-->
<PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId> <PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId>
<Title>Build package for SMAPI mods</Title> <Title>Build package for SMAPI mods</Title>
<Version>3.2.2</Version> <Version>4.1.0</Version>
<Authors>Pathoschild</Authors> <Authors>Pathoschild</Authors>
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later.</Description> <Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>images/icon.png</PackageIcon> <PackageIcon>images/icon.png</PackageIcon>
<PackageProjectUrl>https://smapi.io/package/readme</PackageProjectUrl> <PackageProjectUrl>https://smapi.io/package/readme</PackageProjectUrl>
<PackageReleaseNotes>
- Reworked and streamlined how the package is compiled.
- Added SMAPI-ModTranslationClassBuilder files to the ignore list.
</PackageReleaseNotes>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<!--copy dependency DLLs to bin folder so we can include them in package -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.Build" /> <PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" />
<Reference Include="Microsoft.Build.Framework" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<Reference Include="Microsoft.Build.Utilities.v4.0" />
<Reference Include="System.IO.Compression" /> <!--
<Reference Include="System.Web.Extensions" /> This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet
otherwise imports version 4.3.0 instead of 5.0.0, which conflicts with SMAPI's version.
-->
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -49,5 +51,4 @@
<None PackagePath="build/Pathoschild.Stardew.ModBuildConfig.targets" Include="build\smapi.targets" Pack="true" /> <None PackagePath="build/Pathoschild.Stardew.ModBuildConfig.targets" Include="build\smapi.targets" Pack="true" />
<None PackagePath="images/icon.png" Include="assets\nuget-icon.png" Pack="true" /> <None PackagePath="images/icon.png" Include="assets\nuget-icon.png" Pack="true" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -8,12 +8,14 @@
** Set build options ** Set build options
**********************************************--> **********************************************-->
<PropertyGroup> <PropertyGroup>
<!-- include PDB file by default to enable line numbers in stack traces --> <!-- enable line numbers in stack traces -->
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<!-- recognise XNA Framework DLLs in the GAC (only affects mods using new csproj format) --> <!-- don't create the 'refs' folder (which isn't useful for mods) -->
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<!-- suppress processor architecture mismatch warning (mods should be compiled in 'Any CPU' so they work in both 32-bit and 64-bit mode) -->
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<!-- set default package options --> <!-- set default package options -->
<ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName> <ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName>
@ -23,7 +25,14 @@
<EnableModZip Condition="'$(EnableModZip)' == ''">true</EnableModZip> <EnableModZip Condition="'$(EnableModZip)' == ''">true</EnableModZip>
<EnableHarmony Condition="'$(EnableHarmony)' == ''">false</EnableHarmony> <EnableHarmony Condition="'$(EnableHarmony)' == ''">false</EnableHarmony>
<EnableGameDebugging Condition="'$(EnableGameDebugging)' == ''">true</EnableGameDebugging> <EnableGameDebugging Condition="'$(EnableGameDebugging)' == ''">true</EnableGameDebugging>
<CopyModReferencesToBuildOutput Condition="'$(CopyModReferencesToBuildOutput)' == '' OR ('$(CopyModReferencesToBuildOutput)' != 'true' AND '$(CopyModReferencesToBuildOutput)' != 'false')">false</CopyModReferencesToBuildOutput> <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="$(_BundleExtraAssembliesForAny)">true</CopyLocalLockFileAssemblies>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(OS)' == 'Windows_NT' AND '$(EnableGameDebugging)' == 'true'"> <PropertyGroup Condition="'$(OS)' == 'Windows_NT' AND '$(EnableGameDebugging)' == 'true'">
@ -37,40 +46,36 @@
<!--********************************************* <!--*********************************************
** Add assembly references ** Add assembly references
**********************************************--> **********************************************-->
<!-- common -->
<ItemGroup> <ItemGroup>
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="$(CopyModReferencesToBuildOutput)" /> <!-- game -->
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(CopyModReferencesToBuildOutput)" /> <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.exe" Private="$(CopyModReferencesToBuildOutput)" /> <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(CopyModReferencesToBuildOutput)" /> <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(CopyModReferencesToBuildOutput)" /> <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
<!-- Windows --> <!-- SMAPI -->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'"> <Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" /> <Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
<!-- Linux/Mac --> <!-- Harmony -->
<ItemGroup Condition="'$(OS)' != 'Windows_NT'"> <Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(_BundleExtraAssembliesForGame)" />
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup> </ItemGroup>
<!--********************************************* <!--*********************************************
** Show friendly error for invalid OS or game path ** Show validation messages
**********************************************--> **********************************************-->
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">
<!-- unknown OS type -->
<Error Condition="'$(OS)' != 'OSX' AND '$(OS)' != 'Unix' AND '$(OS)' != 'Windows_NT'" Text="The mod build package doesn't recognise OS type '$(OS)'." /> <Error Condition="'$(OS)' != 'OSX' AND '$(OS)' != 'Unix' AND '$(OS)' != 'Windows_NT'" Text="The mod build package doesn't recognise OS type '$(OS)'." />
<Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see https://smapi.io/package/custom-game-path." /> <!-- invalid game path -->
<Error Condition="!Exists('$(GamePath)\$(GameExecutableName).exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the $(GameExecutableName) file. If this folder is invalid, delete it and the package will autodetect another game install path." /> <Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see https://smapi.io/package/custom-game-path." ContinueOnError="false" />
<Error Condition="!Exists('$(GamePath)\StardewModdingAPI.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." /> <Error Condition="!Exists('$(GamePath)\Stardew Valley.dll')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the Stardew Valley file. If this folder is invalid, delete it and the package will autodetect another game install path." ContinueOnError="false" />
<Error Condition="!Exists('$(GamePath)\StardewModdingAPI.dll')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." ContinueOnError="false" />
<!-- invalid target architecture (note: internal value is 'AnyCPU', value shown in Visual Studio is 'Any CPU') -->
<Warning Condition="'$(Platform)' != 'AnyCPU'" Text="The target platform should be set to 'Any CPU' for compatibility with both 32-bit and 64-bit versions of Stardew Valley (currently set to '$(Platform)'). See https://smapi.io/package/wrong-processor-architecture for details." HelpLink="https://smapi.io/package/wrong-processor-architecture" />
</Target> </Target>
@ -79,6 +84,7 @@
**********************************************--> **********************************************-->
<Target Name="AfterBuild"> <Target Name="AfterBuild">
<DeployModTask <DeployModTask
ModDllName="$(TargetName)"
ModFolderName="$(ModFolderName)" ModFolderName="$(ModFolderName)"
ModZipPath="$(ModZipPath)" ModZipPath="$(ModZipPath)"
@ -89,6 +95,9 @@
TargetDir="$(TargetDir)" TargetDir="$(TargetDir)"
GameModsDir="$(GameModsPath)" GameModsDir="$(GameModsPath)"
IgnoreModFilePatterns="$(IgnoreModFilePatterns)" IgnoreModFilePatterns="$(IgnoreModFilePatterns)"
IgnoreModFilePaths="$(IgnoreModFilePaths)"
BundleExtraAssemblies="$(BundleExtraAssemblies)"
/> />
</Target> </Target>
</Project> </Project>

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
@ -53,7 +53,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="value">The parsed value.</param> /// <param name="value">The parsed value.</param>
/// <param name="required">Whether to show an error if the argument is missing.</param> /// <param name="required">Whether to show an error if the argument is missing.</param>
/// <param name="oneOf">Require that the argument match one of the given values (case-insensitive).</param> /// <param name="oneOf">Require that the argument match one of the given values (case-insensitive).</param>
public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null) public bool TryGet(int index, string name, [NotNullWhen(true)] out string? value, bool required = true, string[]? oneOf = null)
{ {
value = null; value = null;
@ -87,7 +87,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
value = 0; value = 0;
// get argument // get argument
if (!this.TryGet(index, name, out string raw, required)) if (!this.TryGet(index, name, out string? raw, required))
return false; return false;
// parse // parse
@ -107,38 +107,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
return true; return true;
} }
/// <summary>Try to read a decimal argument.</summary>
/// <param name="index">The argument index.</param>
/// <param name="name">The argument name for error messages.</param>
/// <param name="value">The parsed value.</param>
/// <param name="required">Whether to show an error if the argument is missing.</param>
/// <param name="min">The minimum value allowed.</param>
/// <param name="max">The maximum value allowed.</param>
public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null)
{
value = 0;
// get argument
if (!this.TryGet(index, name, out string raw, required))
return false;
// parse
if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value))
{
this.LogDecimalFormatError(index, name, min, max);
return false;
}
// validate
if ((min.HasValue && value < min) || (max.HasValue && value > max))
{
this.LogDecimalFormatError(index, name, min, max);
return false;
}
return true;
}
/// <summary>Returns an enumerator that iterates through the collection.</summary> /// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns> /// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<string> GetEnumerator() public IEnumerator<string> GetEnumerator()
@ -180,22 +148,5 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
else else
this.LogError($"Argument {index} ({name}) must be an integer."); this.LogError($"Argument {index} ({name}) must be an integer.");
} }
/// <summary>Print an error for an invalid decimal argument.</summary>
/// <param name="index">The argument index.</param>
/// <param name="name">The argument name for error messages.</param>
/// <param name="min">The minimum value allowed.</param>
/// <param name="max">The maximum value allowed.</param>
private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max)
{
if (min.HasValue && max.HasValue)
this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}.");
else if (min.HasValue)
this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}.");
else if (max.HasValue)
this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}.");
else
this.LogError($"Argument {index} ({name}) must be a decimal.");
}
} }
} }

View File

@ -4,8 +4,8 @@ using System.Linq;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
{ {
/// <summary>The base implementation for a trainer command.</summary> /// <summary>The base implementation for a console command.</summary>
internal abstract class TrainerCommand : ITrainerCommand internal abstract class ConsoleCommand : IConsoleCommand
{ {
/********* /*********
** Accessors ** Accessors
@ -50,7 +50,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="description">The command description.</param> /// <param name="description">The command description.</param>
/// <param name="mayNeedInput">Whether the command may need to perform logic when the player presses a button.</param> /// <param name="mayNeedInput">Whether the command may need to perform logic when the player presses a button.</param>
/// <param name="mayNeedUpdate">Whether the command may need to perform logic when the game updates.</param> /// <param name="mayNeedUpdate">Whether the command may need to perform logic when the game updates.</param>
protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) protected ConsoleCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false)
{ {
this.Name = name; this.Name = name;
this.Description = description; this.Description = description;
@ -78,8 +78,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="data">The data to display.</param> /// <param name="data">The data to display.</param>
/// <param name="header">The table header.</param> /// <param name="header">The table header.</param>
/// <param name="getRow">Returns a set of fields for a data value.</param> /// <param name="getRow">Returns a set of fields for a data value.</param>
/// <param name="rightAlign">Whether to right-align the data.</param> protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow)
protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow, bool rightAlign = false)
{ {
// get table data // get table data
int[] widths = header.Select(p => p.Length).ToArray(); int[] widths = header.Select(p => p.Length).ToArray();
@ -101,14 +100,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
List<string[]> lines = new List<string[]>(rows.Length + 2) List<string[]> lines = new List<string[]>(rows.Length + 2)
{ {
header, header,
header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() header.Select((_, i) => "".PadRight(widths[i], '-')).ToArray()
}; };
lines.AddRange(rows); lines.AddRange(rows);
return string.Join( return string.Join(
Environment.NewLine, Environment.NewLine,
lines.Select(line => string.Join(" | ", lines.Select(line => string.Join(" | ",
line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' ')) line.Select((field, i) => field.PadLeft(widths[i], ' '))
)) ))
); );
} }

View File

@ -1,7 +1,7 @@
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
{ {
/// <summary>A console command to register.</summary> /// <summary>A console command to register.</summary>
internal interface ITrainerCommand internal interface IConsoleCommand
{ {
/********* /*********
** Accessors ** Accessors

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{
/// <summary>A command which runs one of the game's save migrations.</summary>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ApplySaveFixCommand : ConsoleCommand
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public ApplySaveFixCommand()
: base("apply_save_fix", "Apply one of the game's save migrations to the currently loaded save. WARNING: This may corrupt or make permanent changes to your save. DO NOT USE THIS unless you're absolutely sure.\n\nUsage: apply_save_fix list\nList all valid save IDs.\n\nUsage: apply_save_fix <fix ID>\nApply the named save fix.") { }
/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="command">The command name.</param>
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// get fix ID
if (!args.TryGet(0, "fix_id", out string? rawFixId, required: false))
{
monitor.Log("Invalid usage. Type 'help apply_save_fix' for details.", LogLevel.Error);
return;
}
rawFixId = rawFixId.Trim();
// list mode
if (rawFixId == "list")
{
monitor.Log("Valid save fix IDs:\n - " + string.Join("\n - ", this.GetSaveIds()), LogLevel.Info);
return;
}
// validate fix ID
if (!Enum.TryParse(rawFixId, ignoreCase: true, out SaveGame.SaveFixes fixId))
{
monitor.Log($"Invalid save ID '{rawFixId}'. Type 'help apply_save_fix' for details.", LogLevel.Error);
return;
}
// apply
monitor.Log("THIS MAY CAUSE PERMANENT CHANGES TO YOUR SAVE FILE. If you're not sure, exit your game without saving to avoid issues.", LogLevel.Warn);
monitor.Log($"Trying to apply save fix ID: '{fixId}'.", LogLevel.Warn);
try
{
Game1.applySaveFix(fixId);
monitor.Log("Save fix applied.", LogLevel.Info);
}
catch (Exception ex)
{
monitor.Log("Applying save fix failed. The save may be in an invalid state; you should exit your game now without saving to avoid issues.", LogLevel.Error);
monitor.Log($"Technical details: {ex}", LogLevel.Debug);
}
}
/*********
** Private methods
*********/
/// <summary>Get the valid save fix IDs.</summary>
private IEnumerable<string> GetSaveIds()
{
foreach (SaveGame.SaveFixes id in Enum.GetValues(typeof(SaveGame.SaveFixes)))
{
if (id == SaveGame.SaveFixes.MAX)
continue;
yield return id.ToString();
}
}
}
}

View File

@ -1,9 +1,11 @@
using StardewValley; using System.Diagnostics.CodeAnalysis;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{ {
/// <summary>A command which sends a debug command to the game.</summary> /// <summary>A command which sends a debug command to the game.</summary>
internal class DebugCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class DebugCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,647 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.PerformanceMonitoring;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{
/// <summary>A set of commands which displays or configures performance monitoring.</summary>
internal class PerformanceCounterCommand : TrainerCommand
{
/*********
** Fields
*********/
/// <summary>The name of the command.</summary>
private const string CommandName = "performance";
/// <summary>The available commands.</summary>
private enum SubCommand
{
Summary,
Detail,
Reset,
Trigger,
Enable,
Disable,
Help
}
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public PerformanceCounterCommand()
: base(CommandName, PerformanceCounterCommand.GetDescription()) { }
/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="command">The command name.</param>
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// parse args
SubCommand subcommand = SubCommand.Summary;
{
if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand))
{
this.LogUsageError(monitor, $"Unknown command {subcommandStr}");
return;
}
}
// handle
switch (subcommand)
{
case SubCommand.Summary:
this.HandleSummarySubCommand(monitor, args);
break;
case SubCommand.Detail:
this.HandleDetailSubCommand(monitor, args);
break;
case SubCommand.Reset:
this.HandleResetSubCommand(monitor, args);
break;
case SubCommand.Trigger:
this.HandleTriggerSubCommand(monitor, args);
break;
case SubCommand.Enable:
SCore.PerformanceMonitor.EnableTracking = true;
monitor.Log("Performance counter tracking is now enabled", LogLevel.Info);
break;
case SubCommand.Disable:
SCore.PerformanceMonitor.EnableTracking = false;
monitor.Log("Performance counter tracking is now disabled", LogLevel.Info);
break;
case SubCommand.Help:
this.OutputHelp(monitor, args.TryGet(1, "command", out _) ? subcommand : null as SubCommand?);
break;
default:
this.LogUsageError(monitor, $"Unknown command {subcommand}");
break;
}
}
/*********
** Private methods
*********/
/// <summary>Handles the summary sub command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args)
{
if (!this.AssertEnabled(monitor))
return;
IEnumerable<PerformanceCounterCollection> data = SCore.PerformanceMonitor.GetCollections();
double? threshold = null;
if (args.TryGetDecimal(1, "threshold", out decimal t, required: false))
threshold = (double?)t;
TimeSpan interval = TimeSpan.FromSeconds(60);
StringBuilder report = new StringBuilder();
report.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:");
report.AppendLine(this.GetTableString(
data: data,
header: new[] { "Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time" },
getRow: item => new[]
{
item.Name,
item.GetAverageCallsPerSecond().ToString(),
this.FormatMilliseconds(item.GetGameAverageExecutionTime(interval), threshold),
this.FormatMilliseconds(item.GetModsAverageExecutionTime(interval), threshold),
this.FormatMilliseconds(item.GetAverageExecutionTime(interval), threshold),
this.FormatMilliseconds(item.GetPeakExecutionTime(interval), threshold)
},
true
));
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Handles the detail sub command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args)
{
if (!this.AssertEnabled(monitor))
return;
// parse args
double thresholdMilliseconds = 0;
if (args.TryGetDecimal(1, "threshold", out decimal t, required: false))
thresholdMilliseconds = (double)t;
// get collections
var collections = SCore.PerformanceMonitor.GetCollections();
// render
TimeSpan averageInterval = TimeSpan.FromSeconds(60);
StringBuilder report = new StringBuilder($"Showing details for performance counters of {thresholdMilliseconds}+ milliseconds:\n\n");
bool anyShown = false;
foreach (PerformanceCounterCollection collection in collections)
{
KeyValuePair<string, PerformanceCounter>[] data = collection.PerformanceCounters
.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds)
.ToArray();
if (data.Any())
{
anyShown = true;
report.AppendLine($"{collection.Name}:");
report.AppendLine(this.GetTableString(
data: data,
header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" },
getRow: item => new[]
{
item.Key,
this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds),
this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds),
this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds),
this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds)
},
true
));
}
}
if (!anyShown)
report.AppendLine("No performance counters found.");
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Handles the trigger sub command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args)
{
if (!this.AssertEnabled(monitor))
return;
if (args.TryGet(1, "mode", out string mode, false))
{
switch (mode)
{
case "list":
this.OutputAlertTriggers(monitor);
break;
case "collection":
if (args.TryGet(2, "name", out string collectionName))
{
if (args.TryGetDecimal(3, "threshold", out decimal threshold))
{
if (!args.TryGet(4, "source", out string source, required: false))
source = null;
this.ConfigureAlertTrigger(monitor, collectionName, source, threshold);
}
}
break;
case "pause":
SCore.PerformanceMonitor.PauseAlerts = true;
monitor.Log("Alerts are now paused.", LogLevel.Info);
break;
case "resume":
SCore.PerformanceMonitor.PauseAlerts = false;
monitor.Log("Alerts are now resumed.", LogLevel.Info);
break;
case "dump":
this.OutputAlertTriggers(monitor, true);
break;
case "clear":
this.ClearAlertTriggers(monitor);
break;
default:
this.LogUsageError(monitor, $"Unknown mode {mode}. See '{CommandName} help trigger' for usage.");
break;
}
}
else
this.OutputAlertTriggers(monitor);
}
/// <summary>Sets up an an alert trigger.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="collectionName">The name of the collection.</param>
/// <param name="sourceName">The name of the source, or null for all sources.</param>
/// <param name="threshold">The trigger threshold, or 0 to remove.</param>
private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold)
{
foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
{
if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant()))
{
if (sourceName == null)
{
if (threshold != 0)
{
collection.EnableAlerts = true;
collection.AlertThresholdMilliseconds = (double)threshold;
monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}'", LogLevel.Info);
}
else
{
collection.EnableAlerts = false;
monitor.Log($"Cleared alert triggering for '{collection}'.");
}
return;
}
else
{
foreach (var performanceCounter in collection.PerformanceCounters)
{
if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant()))
{
if (threshold != 0)
{
performanceCounter.Value.EnableAlerts = true;
performanceCounter.Value.AlertThresholdMilliseconds = (double)threshold;
monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}", LogLevel.Info);
}
else
performanceCounter.Value.EnableAlerts = false;
return;
}
}
monitor.Log($"Could not find the source '{sourceName}' in collection '{collectionName}'", LogLevel.Warn);
return;
}
}
}
monitor.Log($"Could not find the collection '{collectionName}'", LogLevel.Warn);
}
/// <summary>Clears alert triggering for all collections.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
private void ClearAlertTriggers(IMonitor monitor)
{
int clearedTriggers = 0;
foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
{
if (collection.EnableAlerts)
{
collection.EnableAlerts = false;
clearedTriggers++;
}
foreach (var performanceCounter in collection.PerformanceCounters)
{
if (performanceCounter.Value.EnableAlerts)
{
performanceCounter.Value.EnableAlerts = false;
clearedTriggers++;
}
}
}
monitor.Log($"Cleared {clearedTriggers} alert triggers.", LogLevel.Info);
}
/// <summary>Lists all configured alert triggers.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="asDump">True to dump the triggers as commands.</param>
private void OutputAlertTriggers(IMonitor monitor, bool asDump = false)
{
StringBuilder report = new StringBuilder();
report.AppendLine("Configured triggers:");
report.AppendLine();
var collectionTriggers = new List<CollectionTrigger>();
var sourceTriggers = new List<SourceTrigger>();
foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
{
if (collection.EnableAlerts)
collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.AlertThresholdMilliseconds));
sourceTriggers.AddRange(
from counter in collection.PerformanceCounters
where counter.Value.EnableAlerts
select new SourceTrigger(collection.Name, counter.Value.Source, counter.Value.AlertThresholdMilliseconds)
);
}
if (collectionTriggers.Count > 0)
{
report.AppendLine("Collection Triggers:");
report.AppendLine();
if (asDump)
{
foreach (var item in collectionTriggers)
report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold}");
}
else
{
report.AppendLine(this.GetTableString(
data: collectionTriggers,
header: new[] { "Collection", "Threshold" },
getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) },
true
));
}
report.AppendLine();
}
else
report.AppendLine("No collection triggers.");
if (sourceTriggers.Count > 0)
{
report.AppendLine("Source Triggers:");
report.AppendLine();
if (asDump)
{
foreach (SourceTrigger item in sourceTriggers)
report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold} {item.SourceName}");
}
else
{
report.AppendLine(this.GetTableString(
data: sourceTriggers,
header: new[] { "Collection", "Source", "Threshold" },
getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) },
true
));
}
report.AppendLine();
}
else
report.AppendLine("No source triggers.");
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Handles the reset sub command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args)
{
if (!this.AssertEnabled(monitor))
return;
if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" }))
{
args.TryGet(2, "name", out string name);
switch (type)
{
case "category":
SCore.PerformanceMonitor.ResetCollection(name);
monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info);
break;
case "source":
SCore.PerformanceMonitor.ResetSource(name);
monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info);
break;
}
}
else
{
SCore.PerformanceMonitor.Reset();
monitor.Log("All performance counters are now cleared.", LogLevel.Info);
}
}
/// <summary>Formats the given milliseconds value into a string format. Optionally
/// allows a threshold to return "-" if the value is less than the threshold.</summary>
/// <param name="milliseconds">The milliseconds to format. Returns "-" if null</param>
/// <param name="thresholdMilliseconds">The threshold. Any value below this is returned as "-".</param>
/// <returns>The formatted milliseconds.</returns>
private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null)
{
thresholdMilliseconds ??= 1;
return milliseconds != null && milliseconds >= thresholdMilliseconds
? ((double)milliseconds).ToString("F2")
: "-";
}
/// <summary>Shows detailed help for a specific sub command.</summary>
/// <param name="monitor">The output monitor.</param>
/// <param name="subcommand">The subcommand.</param>
private void OutputHelp(IMonitor monitor, SubCommand? subcommand)
{
StringBuilder report = new StringBuilder();
report.AppendLine();
switch (subcommand)
{
case SubCommand.Detail:
report.AppendLine($" {CommandName} detail <threshold>");
report.AppendLine();
report.AppendLine("Displays details for a specific collection.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <threshold> Optional. The threshold in milliseconds. Any average execution time below that");
report.AppendLine(" threshold is not reported.");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine($"{CommandName} detail 5 Show counters exceeding an average of 5ms");
break;
case SubCommand.Summary:
report.AppendLine($"Usage: {CommandName} summary <threshold>");
report.AppendLine();
report.AppendLine("Displays the performance counter summary.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <threshold> Optional. Hides the actual execution time if it's below this threshold");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine($"{CommandName} summary Show all events");
report.AppendLine($"{CommandName} summary 5 Shows events exceeding an average of 5ms");
break;
case SubCommand.Trigger:
report.AppendLine($"Usage: {CommandName} trigger <mode>");
report.AppendLine($"Usage: {CommandName} trigger collection <collectionName> <threshold>");
report.AppendLine($"Usage: {CommandName} trigger collection <collectionName> <threshold> <sourceName>");
report.AppendLine();
report.AppendLine("Manages alert triggers.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <mode> Optional. Specifies if a specific source or a specific collection should be triggered.");
report.AppendLine(" - list Lists current triggers");
report.AppendLine(" - collection Sets up a trigger for a collection");
report.AppendLine(" - clear Clears all trigger entries");
report.AppendLine(" - pause Pauses triggering of alerts");
report.AppendLine(" - resume Resumes triggering of alerts");
report.AppendLine(" - dump Dumps all triggers as commands for copy and paste");
report.AppendLine(" Defaults to 'list' if not specified.");
report.AppendLine();
report.AppendLine(" <collectionName> Required if the mode 'collection' is specified.");
report.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match.");
report.AppendLine();
report.AppendLine(" <sourceName> Optional. Specifies the name of a specific source. Must be an exact match.");
report.AppendLine();
report.AppendLine(" <threshold> Required if the mode 'collection' is specified.");
report.AppendLine(" Specifies the threshold in milliseconds (fractions allowed).");
report.AppendLine(" Specify '0' to remove the threshold.");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine();
report.AppendLine($"{CommandName} trigger collection Display.Rendering 10");
report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in");
report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds.");
report.AppendLine();
report.AppendLine($"{CommandName} trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere");
report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in");
report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds.");
report.AppendLine();
report.AppendLine($"{CommandName} trigger collection Display.Rendering 0");
report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact.");
report.AppendLine();
report.AppendLine($"{CommandName} trigger clear");
report.AppendLine(" Clears all previously setup alert triggers.");
break;
case SubCommand.Reset:
report.AppendLine($"Usage: {CommandName} reset <type> <name>");
report.AppendLine();
report.AppendLine("Resets performance counters.");
report.AppendLine();
report.AppendLine("Arguments:");
report.AppendLine(" <type> Optional. Specifies if a collection or source should be reset.");
report.AppendLine(" If omitted, all performance counters are reset.");
report.AppendLine();
report.AppendLine(" - source Clears performance counters for a specific source");
report.AppendLine(" - collection Clears performance counters for a specific collection");
report.AppendLine();
report.AppendLine(" <name> Required if a <type> is given. Specifies the name of either the collection");
report.AppendLine(" or the source. The name must be an exact match.");
report.AppendLine();
report.AppendLine("Examples:");
report.AppendLine($"{CommandName} reset Resets all performance counters");
report.AppendLine($"{CommandName} reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere");
report.AppendLine($"{CommandName} reset collection Display.Rendering Resets all performance for the collection named Display.Rendering");
break;
}
report.AppendLine();
monitor.Log(report.ToString(), LogLevel.Info);
}
/// <summary>Get the command description.</summary>
private static string GetDescription()
{
StringBuilder report = new StringBuilder();
report.AppendLine("Displays or configures performance monitoring to diagnose issues. Performance monitoring is disabled by default.");
report.AppendLine();
report.AppendLine("For example, the counter collection named 'Display.Rendered' contains one performance");
report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it.");
report.AppendLine();
report.AppendLine($"Usage: {CommandName} <command> <action>");
report.AppendLine();
report.AppendLine("Commands:");
report.AppendLine();
report.AppendLine(" summary Show a summary of collections.");
report.AppendLine(" detail Show a summary for a given collection.");
report.AppendLine(" reset Reset all performance counters.");
report.AppendLine(" trigger Configure alert triggers.");
report.AppendLine(" enable Enable performance counter recording.");
report.AppendLine(" disable Disable performance counter recording.");
report.AppendLine(" help Show verbose help for the available commands.");
report.AppendLine();
report.AppendLine($"To get help for a specific command, use '{CommandName} help <command>', for example:");
report.AppendLine($"{CommandName} help summary");
report.AppendLine();
report.AppendLine("Defaults to summary if no command is given.");
report.AppendLine();
return report.ToString();
}
/// <summary>Log a warning if performance monitoring isn't enabled.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <returns>Returns whether performance monitoring is enabled.</returns>
private bool AssertEnabled(IMonitor monitor)
{
if (!SCore.PerformanceMonitor.EnableTracking)
{
monitor.Log($"Performance monitoring is currently disabled; enter '{CommandName} enable' to enable it.", LogLevel.Warn);
return false;
}
return true;
}
/*********
** Private models
*********/
/// <summary>An alert trigger for a collection.</summary>
private class CollectionTrigger
{
/*********
** Accessors
*********/
/// <summary>The collection name.</summary>
public string CollectionName { get; }
/// <summary>The trigger threshold.</summary>
public double Threshold { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="collectionName">The collection name.</param>
/// <param name="threshold">The trigger threshold.</param>
public CollectionTrigger(string collectionName, double threshold)
{
this.CollectionName = collectionName;
this.Threshold = threshold;
}
}
/// <summary>An alert triggered for a source.</summary>
private class SourceTrigger : CollectionTrigger
{
/*********
** Accessors
*********/
/// <summary>The source name.</summary>
public string SourceName { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="collectionName">The collection name.</param>
/// <param name="sourceName">The source name.</param>
/// <param name="threshold">The trigger threshold.</param>
public SourceTrigger(string collectionName, string sourceName, double threshold)
: base(collectionName, threshold)
{
this.SourceName = sourceName;
}
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Netcode;
using StardewValley;
using StardewValley.Network;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{
/// <summary>A command which regenerates the game's bundles.</summary>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class RegenerateBundlesCommand : ConsoleCommand
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public RegenerateBundlesCommand()
: base("regenerate_bundles", $"Regenerate the game's community center bundle data. WARNING: this will reset all bundle progress, and may have unintended effects if you've already completed bundles. DO NOT USE THIS unless you're absolutely sure.\n\nUsage: regenerate_bundles confirm [<type>] [ignore_seed]\nRegenerate all bundles for this save. If the <type> is set to '{string.Join("' or '", Enum.GetNames(typeof(Game1.BundleType)))}', change the bundle type for the save. If an 'ignore_seed' option is included, remixed bundles are re-randomized without using the predetermined save seed.\n\nExample: regenerate_bundles remixed confirm") { }
/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="command">The command name.</param>
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// get flags
var bundleType = Game1.bundleType;
bool confirmed = false;
bool useSeed = true;
foreach (string arg in args)
{
if (arg.Equals("confirm", StringComparison.OrdinalIgnoreCase))
confirmed = true;
else if (arg.Equals("ignore_seed", StringComparison.OrdinalIgnoreCase))
useSeed = false;
else if (Enum.TryParse(arg, ignoreCase: true, out Game1.BundleType type))
bundleType = type;
else
{
monitor.Log($"Invalid option '{arg}'. Type 'help {command}' for usage.", LogLevel.Error);
return;
}
}
// require confirmation
if (!confirmed)
{
monitor.Log($"WARNING: this may have unintended consequences (type 'help {command}' for details). Are you sure?", LogLevel.Warn);
string[] newArgs = args.Concat(new[] { "confirm" }).ToArray();
monitor.Log($"To confirm, enter this command: '{command} {string.Join(" ", newArgs)}'.", LogLevel.Info);
return;
}
// need a loaded save
if (!Context.IsWorldReady)
{
monitor.Log("You need to load a save to use this command.", LogLevel.Error);
return;
}
// get private fields
IWorldState state = Game1.netWorldState.Value;
var bundleData = state.GetType().GetField("_bundleData", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)?.GetValue(state) as IDictionary<string, string>
?? throw new InvalidOperationException("Can't access '_bundleData' field on world state.");
var netBundleData = state.GetType().GetField("netBundleData", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)?.GetValue(state) as NetStringDictionary<string, NetString>
?? throw new InvalidOperationException("Can't access 'netBundleData' field on world state.");
// clear bundle data
state.BundleData.Clear();
state.Bundles.Clear();
state.BundleRewards.Clear();
bundleData.Clear();
netBundleData.Clear();
// regenerate bundles
var locale = LocalizedContentManager.CurrentLanguageCode;
try
{
LocalizedContentManager.CurrentLanguageCode = LocalizedContentManager.LanguageCode.en; // the base bundle data needs to be unlocalized (the game will add localized names later)
Game1.bundleType = bundleType;
Game1.GenerateBundles(bundleType, use_seed: useSeed);
}
finally
{
LocalizedContentManager.CurrentLanguageCode = locale;
}
monitor.Log("Regenerated bundles and reset bundle progress.", LogLevel.Info);
monitor.Log("This may have unintended effects if you've already completed any bundles. If you're not sure, exit your game without saving to cancel.", LogLevel.Warn);
}
}
}

View File

@ -1,9 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{ {
/// <summary>A command which shows the data files.</summary> /// <summary>A command which shows the data files.</summary>
internal class ShowDataFilesCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ShowDataFilesCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,9 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{ {
/// <summary>A command which shows the game files.</summary> /// <summary>A command which shows the game files.</summary>
internal class ShowGameFilesCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ShowGameFilesCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods
@ -18,8 +20,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
/// <param name="args">The command arguments.</param> /// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args) public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{ {
Process.Start(Constants.ExecutionPath); Process.Start(Constants.GamePath);
monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info); monitor.Log($"OK, opening {Constants.GamePath}.", LogLevel.Info);
} }
} }
} }

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{ {
/// <summary>A command which logs the keys being pressed for 30 seconds once enabled.</summary> /// <summary>A command which logs the keys being pressed for 30 seconds once enabled.</summary>
internal class TestInputCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class TestInputCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields
@ -37,9 +39,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
public override void OnUpdated(IMonitor monitor) public override void OnUpdated(IMonitor monitor)
{ {
// handle expiry // handle expiry
if (this.ExpiryTicks == null) if (this.ExpiryTicks != null && this.ExpiryTicks <= DateTime.UtcNow.Ticks)
return;
if (this.ExpiryTicks <= DateTime.UtcNow.Ticks)
{ {
monitor.Log("No longer logging input.", LogLevel.Info); monitor.Log("No longer logging input.", LogLevel.Info);
this.ExpiryTicks = null; this.ExpiryTicks = null;

View File

@ -7,13 +7,13 @@ using Object = StardewValley.Object;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which adds an item to the player inventory.</summary> /// <summary>A command which adds an item to the player inventory.</summary>
internal class AddCommand : TrainerCommand internal class AddCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields
*********/ *********/
/// <summary>Provides methods for searching and constructing items.</summary> /// <summary>Provides methods for searching and constructing items.</summary>
private readonly ItemRepository Items = new ItemRepository(); private readonly ItemRepository Items = new();
/// <summary>The type names recognized by this command.</summary> /// <summary>The type names recognized by this command.</summary>
private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray(); private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray();
@ -40,7 +40,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
} }
// read arguments // read arguments
if (!args.TryGet(0, "item type", out string type, oneOf: this.ValidTypes)) if (!args.TryGet(0, "item type", out string? type, oneOf: this.ValidTypes))
return; return;
if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) if (!args.TryGetInt(2, "count", out int count, min: 1, required: false))
count = 1; count = 1;
@ -48,7 +48,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
quality = Object.lowQuality; quality = Object.lowQuality;
// find matching item // find matching item
SearchableItem match = Enum.TryParse(type, true, out ItemType itemType) SearchableItem? match = Enum.TryParse(type, true, out ItemType itemType)
? this.FindItemByID(monitor, args, itemType) ? this.FindItemByID(monitor, args, itemType)
: this.FindItemByName(monitor, args); : this.FindItemByName(monitor, args);
if (match == null) if (match == null)
@ -76,14 +76,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <param name="monitor">Writes messages to the console and log file.</param> /// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param> /// <param name="args">The command arguments.</param>
/// <param name="type">The item type.</param> /// <param name="type">The item type.</param>
private SearchableItem FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type) private SearchableItem? FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
{ {
// read arguments // read arguments
if (!args.TryGetInt(1, "item ID", out int id, min: 0)) if (!args.TryGetInt(1, "item ID", out int id, min: 0))
return null; return null;
// find matching item // find matching item
SearchableItem item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); SearchableItem? item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
if (item == null) if (item == null)
monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error);
return item; return item;
@ -92,10 +92,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <summary>Get a matching item by its name.</summary> /// <summary>Get a matching item by its name.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param> /// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param> /// <param name="args">The command arguments.</param>
private SearchableItem FindItemByName(IMonitor monitor, ArgumentParser args) private SearchableItem? FindItemByName(IMonitor monitor, ArgumentParser args)
{ {
// read arguments // read arguments
if (!args.TryGet(1, "item name", out string name)) if (!args.TryGet(1, "item name", out string? name))
return null; return null;
// find matching items // find matching items

View File

@ -1,16 +1,18 @@
using System.Linq; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which list item types.</summary> /// <summary>A command which list item types.</summary>
internal class ListItemTypesCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ListItemTypesCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields
*********/ *********/
/// <summary>Provides methods for searching and constructing items.</summary> /// <summary>Provides methods for searching and constructing items.</summary>
private readonly ItemRepository Items = new ItemRepository(); private readonly ItemRepository Items = new();
/********* /*********

View File

@ -1,18 +1,20 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which list items available to spawn.</summary> /// <summary>A command which list items available to spawn.</summary>
internal class ListItemsCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ListItemsCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields
*********/ *********/
/// <summary>Provides methods for searching and constructing items.</summary> /// <summary>Provides methods for searching and constructing items.</summary>
private readonly ItemRepository Items = new ItemRepository(); private readonly ItemRepository Items = new();
/********* /*********
@ -59,15 +61,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
private IEnumerable<SearchableItem> GetItems(string[] searchWords) private IEnumerable<SearchableItem> GetItems(string[] searchWords)
{ {
// normalize search term // normalize search term
searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); searchWords = searchWords.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray();
if (searchWords?.Any() == false) bool getAll = !searchWords.Any();
searchWords = null;
// find matches // find matches
return ( return (
from item in this.Items.GetAll() from item in this.Items.GetAll()
let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}" let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}"
where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1) where getAll || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1)
select item select item
); );
} }

View File

@ -1,10 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the color of a player feature.</summary> /// <summary>A command which edits the color of a player feature.</summary>
internal class SetColorCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetColorCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods
@ -20,9 +22,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Handle(IMonitor monitor, string command, ArgumentParser args) public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{ {
// parse arguments // parse arguments
if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "eyes", "pants" })) if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "eyes", "pants" }))
return; return;
if (!args.TryGet(1, "color", out string rawColor)) if (!args.TryGet(1, "color", out string? rawColor))
return; return;
// parse color // parse color
@ -61,7 +63,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <param name="color">The color to set.</param> /// <param name="color">The color to set.</param>
private bool TryParseColor(string input, out Color color) 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)) 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); color = new Color(r, g, b);

View File

@ -0,0 +1,224 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using StardewValley;
using StardewValley.GameData;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{
/// <summary>A command which changes the player's farm type.</summary>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetFarmTypeCommand : ConsoleCommand
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public SetFarmTypeCommand()
: base("set_farm_type", "Sets the current player's farm type.\n\nUsage: set_farm_type <farm type>\n- farm type: the farm type to set. Enter `set_farm_type list` for a list of available farm types.") { }
/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="command">The command name.</param>
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// validate
if (!Context.IsWorldReady)
{
monitor.Log("You must load a save to use this command.", LogLevel.Error);
return;
}
// parse arguments
if (!args.TryGet(0, "farm type", out string? farmType))
return;
bool isVanillaId = int.TryParse(farmType, out int vanillaId) && vanillaId is (>= 0 and < Farm.layout_max);
// handle argument
if (farmType == "list")
this.HandleList(monitor);
else if (isVanillaId)
this.HandleVanillaFarmType(vanillaId, monitor);
else
this.HandleCustomFarmType(farmType, monitor);
}
/*********
** Private methods
*********/
/****
** Handlers
****/
/// <summary>Print a list of available farm types.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
private void HandleList(IMonitor monitor)
{
StringBuilder result = new();
// list vanilla types
result.AppendLine("The farm type can be one of these vanilla types:");
foreach (var type in this.GetVanillaFarmTypes())
result.AppendLine($" - {type.Key} ({type.Value})");
result.AppendLine();
// list custom types
{
var customTypes = this.GetCustomFarmTypes();
if (customTypes.Any())
{
result.AppendLine("Or one of these custom farm types:");
foreach (var type in customTypes.Values.OrderBy(p => p.ID))
result.AppendLine($" - {type.ID} ({this.GetCustomName(type)})");
}
else
result.AppendLine("Or a custom farm type (though none is loaded currently).");
}
// print
monitor.Log(result.ToString(), LogLevel.Info);
}
/// <summary>Set a vanilla farm type.</summary>
/// <param name="type">The farm type.</param>
/// <param name="monitor">Writes messages to the console and log file.</param>
private void HandleVanillaFarmType(int type, IMonitor monitor)
{
if (Game1.whichFarm == type)
{
monitor.Log($"Your current farm is already set to {type} ({this.GetVanillaName(type)}).", LogLevel.Info);
return;
}
this.SetFarmType(type, null);
this.PrintSuccess(monitor, $"{type} ({this.GetVanillaName(type)}");
}
/// <summary>Set a custom farm type.</summary>
/// <param name="id">The farm type ID.</param>
/// <param name="monitor">Writes messages to the console and log file.</param>
private void HandleCustomFarmType(string id, IMonitor monitor)
{
if (Game1.whichModFarm?.ID == id)
{
monitor.Log($"Your current farm is already set to {id} ({this.GetCustomName(Game1.whichModFarm)}).", LogLevel.Info);
return;
}
if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType? customFarmType))
{
monitor.Log($"Invalid farm type '{id}'. Enter `help set_farm_type` for more info.", LogLevel.Error);
return;
}
this.SetFarmType(Farm.mod_layout, customFarmType);
this.PrintSuccess(monitor, $"{id} ({this.GetCustomName(customFarmType)})");
}
/// <summary>Change the farm type.</summary>
/// <param name="type">The farm type ID.</param>
/// <param name="customFarmData">The custom farm type data, if applicable.</param>
private void SetFarmType(int type, ModFarmType? customFarmData)
{
// set flags
Game1.whichFarm = type;
Game1.whichModFarm = customFarmData;
// update farm map
Farm farm = Game1.getFarm();
farm.mapPath.Value = $@"Maps\{Farm.getMapNameFromTypeInt(Game1.whichFarm)}";
farm.reloadMap();
farm.updateWarps();
// clear spouse area cache to avoid errors
FieldInfo? cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (cacheField == null)
throw new InvalidOperationException("Failed to access '_baseSpouseAreaTiles' field to clear spouse area cache.");
if (cacheField.GetValue(farm) is not IDictionary cache)
throw new InvalidOperationException($"The farm's '_baseSpouseAreaTiles' field didn't match the expected {nameof(IDictionary)} type.");
cache.Clear();
}
private void PrintSuccess(IMonitor monitor, string label)
{
StringBuilder result = new();
result.AppendLine($"Your current farm has been converted to {label}. Saving and reloading is recommended to make sure everything is updated for the change.");
result.AppendLine();
result.AppendLine("This doesn't move items that are out of bounds on the new map. If you need to clean up, you can...");
result.AppendLine(" - temporarily switch back to the previous farm type;");
result.AppendLine(" - or use a mod like Noclip Mode: https://www.nexusmods.com/stardewvalley/mods/3900 ;");
result.AppendLine(" - or use the world_clear console command (enter `help world_clear` for details).");
monitor.Log(result.ToString(), LogLevel.Warn);
}
/****
** Vanilla farm types
****/
/// <summary>Get the display name for a vanilla farm type.</summary>
/// <param name="type">The farm type.</param>
private string GetVanillaName(int type)
{
string? translationKey = type switch
{
Farm.default_layout => "Character_FarmStandard",
Farm.riverlands_layout => "Character_FarmFishing",
Farm.forest_layout => "Character_FarmForaging",
Farm.mountains_layout => "Character_FarmMining",
Farm.combat_layout => "Character_FarmCombat",
Farm.fourCorners_layout => "Character_FarmFourCorners",
Farm.beach_layout => "Character_FarmBeach",
_ => null
};
return translationKey != null
? Game1.content.LoadString(@$"Strings\UI:{translationKey}").Split('_')[0]
: type.ToString();
}
/// <summary>Get the available vanilla farm types by ID.</summary>
private IDictionary<int, string> GetVanillaFarmTypes()
{
IDictionary<int, string> farmTypes = new Dictionary<int, string>();
foreach (int id in Enumerable.Range(0, Farm.layout_max))
farmTypes[id] = this.GetVanillaName(id);
return farmTypes;
}
/****
** Custom farm types
****/
/// <summary>Get the display name for a custom farm type.</summary>
/// <param name="farmType">The custom farm type.</param>
private string? GetCustomName(ModFarmType? farmType)
{
if (string.IsNullOrWhiteSpace(farmType?.TooltipStringPath))
return farmType?.ID;
return Game1.content.LoadString(farmType.TooltipStringPath)?.Split('_')[0] ?? farmType.ID;
}
/// <summary>Get the available custom farm types by ID.</summary>
private IDictionary<string, ModFarmType> GetCustomFarmTypes()
{
IDictionary<string, ModFarmType> farmTypes = new Dictionary<string, ModFarmType>(StringComparer.OrdinalIgnoreCase);
foreach (ModFarmType farmType in Game1.content.Load<List<ModFarmType>>("Data\\AdditionalFarms"))
{
if (string.IsNullOrWhiteSpace(farmType.ID))
continue;
farmTypes[farmType.ID] = farmType;
}
return farmTypes;
}
}
}

View File

@ -1,24 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's current health.</summary> /// <summary>A command which edits the player's current health.</summary>
internal class SetHealthCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetHealthCommand : ConsoleCommand
{ {
/*********
** Fields
*********/
/// <summary>Whether to keep the player's health at its maximum.</summary>
private bool InfiniteHealth;
/********* /*********
** Public methods ** Public methods
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
public SetHealthCommand() public SetHealthCommand()
: base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { } : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount.") { }
/// <summary>Handle the command.</summary> /// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param> /// <param name="monitor">Writes messages to the console and log file.</param>
@ -29,36 +24,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
// no-argument mode // no-argument mode
if (!args.Any()) if (!args.Any())
{ {
monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info); monitor.Log($"You currently have {Game1.player.health} health. Specify a value to change it.", LogLevel.Info);
return; return;
} }
// handle // handle
string amountStr = args[0]; string amountStr = args[0];
if (amountStr == "inf") if (int.TryParse(amountStr, out int amount))
{ {
this.InfiniteHealth = true; Game1.player.health = amount;
monitor.Log("OK, you now have infinite health.", LogLevel.Info); monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info);
} }
else else
{ this.LogArgumentNotInt(monitor);
this.InfiniteHealth = false;
if (int.TryParse(amountStr, out int amount))
{
Game1.player.health = amount;
monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info);
}
else
this.LogArgumentNotInt(monitor);
}
}
/// <summary>Perform any logic needed on update tick.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
public override void OnUpdated(IMonitor monitor)
{
if (this.InfiniteHealth && Context.IsWorldReady)
Game1.player.health = Game1.player.maxHealth;
} }
} }
} }

View File

@ -1,10 +1,12 @@
using System.Linq; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's current immunity.</summary> /// <summary>A command which edits the player's current immunity.</summary>
internal class SetImmunityCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetImmunityCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,10 +1,12 @@
using System.Linq; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's maximum health.</summary> /// <summary>A command which edits the player's maximum health.</summary>
internal class SetMaxHealthCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetMaxHealthCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,10 +1,12 @@
using System.Linq; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's maximum stamina.</summary> /// <summary>A command which edits the player's maximum stamina.</summary>
internal class SetMaxStaminaCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetMaxStaminaCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,24 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's current money.</summary> /// <summary>A command which edits the player's current money.</summary>
internal class SetMoneyCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetMoneyCommand : ConsoleCommand
{ {
/*********
** Fields
*********/
/// <summary>Whether to keep the player's money at a set value.</summary>
private bool InfiniteMoney;
/********* /*********
** Public methods ** Public methods
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
public SetMoneyCommand() public SetMoneyCommand()
: base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { } : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount.") { }
/// <summary>Handle the command.</summary> /// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param> /// <param name="monitor">Writes messages to the console and log file.</param>
@ -29,36 +24,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
// validate // validate
if (!args.Any()) if (!args.Any())
{ {
monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info); monitor.Log($"You currently have {Game1.player.Money} gold. Specify a value to change it.", LogLevel.Info);
return; return;
} }
// handle // handle
string amountStr = args[0]; string amountStr = args[0];
if (amountStr == "inf") if (int.TryParse(amountStr, out int amount))
{ {
this.InfiniteMoney = true; Game1.player.Money = amount;
monitor.Log("OK, you now have infinite money.", LogLevel.Info); monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info);
} }
else else
{ this.LogArgumentNotInt(monitor);
this.InfiniteMoney = false;
if (int.TryParse(amountStr, out int amount))
{
Game1.player.Money = amount;
monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info);
}
else
this.LogArgumentNotInt(monitor);
}
}
/// <summary>Perform any logic needed on update tick.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
public override void OnUpdated(IMonitor monitor)
{
if (this.InfiniteMoney && Context.IsWorldReady)
Game1.player.Money = 999999;
} }
} }
} }

View File

@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's name.</summary> /// <summary>A command which edits the player's name.</summary>
internal class SetNameCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetNameCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods
@ -19,9 +21,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Handle(IMonitor monitor, string command, ArgumentParser args) public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{ {
// parse arguments // parse arguments
if (!args.TryGet(0, "target", out string target, oneOf: new[] { "player", "farm" })) if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "player", "farm" }))
return; return;
args.TryGet(1, "name", out string name, required: false); args.TryGet(1, "name", out string? name, required: false);
// handle // handle
switch (target) switch (target)

View File

@ -1,24 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits the player's current stamina.</summary> /// <summary>A command which edits the player's current stamina.</summary>
internal class SetStaminaCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetStaminaCommand : ConsoleCommand
{ {
/*********
** Fields
*********/
/// <summary>Whether to keep the player's stamina at its maximum.</summary>
private bool InfiniteStamina;
/********* /*********
** Public methods ** Public methods
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
public SetStaminaCommand() public SetStaminaCommand()
: base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { } : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount.") { }
/// <summary>Handle the command.</summary> /// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param> /// <param name="monitor">Writes messages to the console and log file.</param>
@ -29,36 +24,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
// validate // validate
if (!args.Any()) if (!args.Any())
{ {
monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info); monitor.Log($"You currently have {Game1.player.Stamina} stamina. Specify a value to change it.", LogLevel.Info);
return; return;
} }
// handle // handle
string amountStr = args[0]; string amountStr = args[0];
if (amountStr == "inf") if (int.TryParse(amountStr, out int amount))
{ {
this.InfiniteStamina = true; Game1.player.Stamina = amount;
monitor.Log("OK, you now have infinite stamina.", LogLevel.Info); monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info);
} }
else else
{ this.LogArgumentNotInt(monitor);
this.InfiniteStamina = false;
if (int.TryParse(amountStr, out int amount))
{
Game1.player.Stamina = amount;
monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info);
}
else
this.LogArgumentNotInt(monitor);
}
}
/// <summary>Perform any logic needed on update tick.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
public override void OnUpdated(IMonitor monitor)
{
if (this.InfiniteStamina && Context.IsWorldReady)
Game1.player.stamina = Game1.player.MaxStamina;
} }
} }
} }

View File

@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
{ {
/// <summary>A command which edits a player style.</summary> /// <summary>A command which edits a player style.</summary>
internal class SetStyleCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetStyleCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods
@ -19,7 +21,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Handle(IMonitor monitor, string command, ArgumentParser args) public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{ {
// parse arguments // parse arguments
if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" })) if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" }))
return; return;
if (!args.TryGetInt(1, "style ID", out int styleID)) if (!args.TryGetInt(1, "style ID", out int styleID))
return; return;

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley; using StardewValley;
using StardewValley.Locations; using StardewValley.Locations;
using StardewValley.Objects; using StardewValley.Objects;
@ -10,13 +11,14 @@ using SObject = StardewValley.Object;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which clears in-game objects.</summary> /// <summary>A command which clears in-game objects.</summary>
internal class ClearCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class ClearCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields
*********/ *********/
/// <summary>The valid types that can be cleared.</summary> /// <summary>The valid types that can be cleared.</summary>
private readonly string[] ValidTypes = { "crops", "debris", "fruit-trees", "grass", "trees", "everything" }; private readonly string[] ValidTypes = { "crops", "debris", "fruit-trees", "furniture", "grass", "trees", "removable", "everything" };
/// <summary>The resource clump IDs to consider debris.</summary> /// <summary>The resource clump IDs to consider debris.</summary>
private readonly int[] DebrisClumps = { ResourceClump.stumpIndex, ResourceClump.hollowLogIndex, ResourceClump.meteoriteIndex, ResourceClump.boulderIndex }; private readonly int[] DebrisClumps = { ResourceClump.stumpIndex, ResourceClump.hollowLogIndex, ResourceClump.meteoriteIndex, ResourceClump.boulderIndex };
@ -31,8 +33,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
name: "world_clear", name: "world_clear",
description: "Clears in-game entities in a given location.\n\n" description: "Clears in-game entities in a given location.\n\n"
+ "Usage: world_clear <location> <object type>\n" + "Usage: world_clear <location> <object type>\n"
+ "- location: the location name for which to clear objects (like Farm), or 'current' for the current location.\n" + " - location: the location name for which to clear objects (like Farm), or 'current' for the current location.\n"
+ " - object type: the type of object clear. You can specify 'crops', 'debris' (stones/twigs/weeds and dead crops), 'grass', and 'trees' / 'fruit-trees'. You can also specify 'everything', which includes things not removed by the other types (like furniture or resource clumps)." + " - object type: the type of object clear. You can specify 'crops', 'debris' (stones/twigs/weeds and dead crops), 'furniture', 'grass', and 'trees' / 'fruit-trees'. You can also specify 'removable' (remove everything that can be removed or destroyed during normal gameplay) or 'everything' (remove everything including permanent bushes)."
) )
{ } { }
@ -50,13 +52,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
} }
// parse arguments // parse arguments
if (!args.TryGet(0, "location", out string locationName, required: true)) if (!args.TryGet(0, "location", out string? locationName, required: true))
return; return;
if (!args.TryGet(1, "object type", out string type, required: true, oneOf: this.ValidTypes)) if (!args.TryGet(1, "object type", out string? type, required: true, oneOf: this.ValidTypes))
return; return;
// get target location // get target location
GameLocation location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase)); GameLocation? location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase));
if (location == null && locationName == "current") if (location == null && locationName == "current")
location = Game1.currentLocation; location = Game1.currentLocation;
if (location == null) if (location == null)
@ -93,11 +95,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
removed += removed +=
this.RemoveObjects(location, obj => this.RemoveObjects(location, obj =>
!(obj is Chest) obj is not Chest
&& ( && (
obj.Name == "Weeds" obj.Name is "Weeds" or "Stone"
|| obj.Name == "Stone" || obj.ParentSheetIndex is 294 or 295
|| (obj.ParentSheetIndex == 294 || obj.ParentSheetIndex == 295)
) )
) )
+ this.RemoveResourceClumps(location, clump => this.DebrisClumps.Contains(clump.parentSheetIndex.Value)); + this.RemoveResourceClumps(location, clump => this.DebrisClumps.Contains(clump.parentSheetIndex.Value));
@ -113,6 +114,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
break; break;
} }
case "furniture":
{
int removed = this.RemoveFurniture(location, _ => true);
monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
break;
}
case "grass": case "grass":
{ {
int removed = this.RemoveTerrainFeatures(location, feature => feature is Grass); int removed = this.RemoveTerrainFeatures(location, feature => feature is Grass);
@ -127,14 +135,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
break; break;
} }
case "removable":
case "everything": case "everything":
{ {
bool everything = type == "everything";
int removed = int removed =
this.RemoveFurniture(location, p => true) this.RemoveFurniture(location, _ => true)
+ this.RemoveObjects(location, p => true) + this.RemoveObjects(location, _ => true)
+ this.RemoveTerrainFeatures(location, p => true) + this.RemoveTerrainFeatures(location, _ => true)
+ this.RemoveLargeTerrainFeatures(location, p => true) + this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable(location, p.currentTileLocation))
+ this.RemoveResourceClumps(location, p => true); + this.RemoveResourceClumps(location, _ => true);
monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
break; break;
} }
@ -157,11 +167,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
int removed = 0; int removed = 0;
foreach (var pair in location.Objects.Pairs.ToArray()) foreach ((Vector2 tile, SObject? obj) in location.Objects.Pairs.ToArray())
{ {
if (shouldRemove(pair.Value)) if (shouldRemove(obj))
{ {
location.Objects.Remove(pair.Key); location.Objects.Remove(tile);
removed++; removed++;
} }
} }
@ -177,11 +187,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
int removed = 0; int removed = 0;
foreach (var pair in location.terrainFeatures.Pairs.ToArray()) foreach ((Vector2 tile, TerrainFeature? feature) in location.terrainFeatures.Pairs.ToArray())
{ {
if (shouldRemove(pair.Value)) if (shouldRemove(feature))
{ {
location.terrainFeatures.Remove(pair.Key); location.terrainFeatures.Remove(tile);
removed++; removed++;
} }
} }
@ -217,18 +227,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
int removed = 0; int removed = 0;
// get resource clumps foreach (ResourceClump clump in location.resourceClumps.Where(shouldRemove).ToArray())
IList<ResourceClump> resourceClumps =
(location as Farm)?.resourceClumps
?? (IList<ResourceClump>)(location as Woods)?.stumps
?? new List<ResourceClump>();
// remove matching clumps
foreach (var clump in resourceClumps.ToArray())
{ {
if (shouldRemove(clump)) location.resourceClumps.Remove(clump);
removed++;
}
if (location is Woods woods)
{
foreach (ResourceClump clump in woods.stumps.Where(shouldRemove).ToArray())
{ {
resourceClumps.Remove(clump); woods.stumps.Remove(clump);
removed++; removed++;
} }
} }
@ -244,15 +253,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
int removed = 0; int removed = 0;
if (location is DecoratableLocation decoratableLocation) foreach (Furniture furniture in location.furniture.ToArray())
{ {
foreach (Furniture furniture in decoratableLocation.furniture.ToArray()) if (shouldRemove(furniture))
{ {
if (shouldRemove(furniture)) location.furniture.Remove(furniture);
{ removed++;
decoratableLocation.furniture.Remove(furniture);
removed++;
}
} }
} }

View File

@ -1,10 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using StardewValley; using StardewValley;
using StardewValley.Locations; using StardewValley.Locations;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which moves the player to the next mine level.</summary> /// <summary>A command which moves the player to the next mine level.</summary>
internal class DownMineLevelCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class DownMineLevelCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -4,7 +4,7 @@ using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which freezes the current time.</summary> /// <summary>A command which freezes the current time.</summary>
internal class FreezeTimeCommand : TrainerCommand internal class FreezeTimeCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields

View File

@ -0,0 +1,56 @@
using System;
using System.Diagnostics.CodeAnalysis;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{
/// <summary>A command which immediately warps all NPCs to their scheduled positions. To hurry a single NPC, see <c>debug hurry npc-name</c> instead.</summary>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class HurryAllCommand : ConsoleCommand
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public HurryAllCommand()
: base(
name: "hurry_all",
description: "Immediately warps all NPCs to their scheduled positions. (To hurry a single NPC, use `debug hurry npc-name` instead.)\n\n"
+ "Usage: hurry_all"
)
{ }
/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="command">The command name.</param>
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// check context
if (!Context.IsWorldReady)
{
monitor.Log("You need to load a save to use this command.", LogLevel.Error);
return;
}
// hurry all NPCs
foreach (NPC npc in Utility.getAllCharacters())
{
if (!npc.isVillager())
continue;
monitor.Log($"Hurrying {npc.Name}...");
try
{
npc.warpToPathControllerDestination();
}
catch (Exception ex)
{
monitor.Log($"Failed hurrying {npc.Name}. Technical details:\n{ex}", LogLevel.Error);
}
}
monitor.Log("Done!", LogLevel.Info);
}
}
}

View File

@ -1,11 +1,13 @@
using System.Linq; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Utilities; using StardewModdingAPI.Utilities;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which sets the current day.</summary> /// <summary>A command which sets the current day.</summary>
internal class SetDayCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetDayCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which moves the player to the given mine level.</summary> /// <summary>A command which moves the player to the given mine level.</summary>
internal class SetMineLevelCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetMineLevelCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

View File

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using StardewModdingAPI.Utilities; using StardewModdingAPI.Utilities;
using StardewValley; using StardewValley;
@ -5,7 +6,8 @@ using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which sets the current season.</summary> /// <summary>A command which sets the current season.</summary>
internal class SetSeasonCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetSeasonCommand : ConsoleCommand
{ {
/********* /*********
** Fields ** Fields
@ -35,7 +37,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
} }
// parse arguments // parse arguments
if (!args.TryGet(0, "season", out string season, oneOf: this.ValidSeasons)) if (!args.TryGet(0, "season", out string? season, oneOf: this.ValidSeasons))
return; return;
// handle // handle

View File

@ -1,11 +1,13 @@
using System; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which sets the current time.</summary> /// <summary>A command which sets the current time.</summary>
internal class SetTimeCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetTimeCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods
@ -45,12 +47,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
/// <param name="time">The time of day.</param> /// <param name="time">The time of day.</param>
private void SafelySetTime(int time) private void SafelySetTime(int time)
{ {
// define conversion between game time and TimeSpan
TimeSpan ToTimeSpan(int value) => new TimeSpan(0, value / 100, value % 100, 0);
int FromTimeSpan(TimeSpan span) => (span.Hours * 100) + span.Minutes;
// transition to new time // transition to new time
int intervals = (int)((ToTimeSpan(time) - ToTimeSpan(Game1.timeOfDay)).TotalMinutes / 10); int intervals = Utility.CalculateMinutesBetweenTimes(Game1.timeOfDay, time) / 10;
if (intervals > 0) if (intervals > 0)
{ {
for (int i = 0; i < intervals; i++) for (int i = 0; i < intervals; i++)
@ -60,10 +58,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
for (int i = 0; i > intervals; i--) for (int i = 0; i > intervals; i--)
{ {
Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 minutes so game updates to next interval Game1.timeOfDay = Utility.ModifyTime(Game1.timeOfDay, -20); // offset 20 mins so game updates to next interval
Game1.performTenMinuteClockUpdate(); Game1.performTenMinuteClockUpdate();
} }
} }
// reset ambient light
// White is the default non-raining color. If it's raining or dark out, UpdateGameClock
// below will update it automatically.
Game1.outdoorLight = Color.White;
Game1.ambientLight = Color.White;
// run clock update (to correct lighting, etc)
Game1.gameTimeInterval = 0;
Game1.UpdateGameClock(Game1.currentGameTime);
} }
} }
} }

View File

@ -1,11 +1,13 @@
using System.Linq; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Utilities; using StardewModdingAPI.Utilities;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{ {
/// <summary>A command which sets the current year.</summary> /// <summary>A command which sets the current year.</summary>
internal class SetYearCommand : TrainerCommand [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
internal class SetYearCommand : ConsoleCommand
{ {
/********* /*********
** Public methods ** Public methods

Some files were not shown because too many files have changed in this diff Show More