Update Android branch to SMAPI 3.1.0

This commit is contained in:
Chris 2020-01-31 03:34:48 -05:00
commit 326c7d7db1
363 changed files with 11770 additions and 8072 deletions

2
.gitattributes vendored
View File

@ -1,3 +1,3 @@
# normalise line endings # normalize line endings
* text=auto * text=auto
README.txt text=crlf README.txt text=crlf

View File

@ -1,19 +1,16 @@
Do you want to... Do you want to...
* **Ask for help using SMAPI?** * **Ask for help using SMAPI?**
Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375) Please ask in [the Stardew Valley Discord or mod forums](https://smapi.io/community), don't
or [ask on Discord](https://stardewvalleywiki.com/Modding:Community#Discord), don't create a create a GitHub issue.
GitHub issue.
* **Report a bug?** * **Report a bug?**
Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375) Please report it in [the Stardew Valley Discord or mod forums](https://smapi.io/community), don't
or [ask on Discord](https://stardewvalleywiki.com/Modding:Community#Discord) instead, unless create a GitHub issue unless you're sure it's a bug in the SMAPI code.
you're sure it's a bug in SMAPI itself.
* **Submit a pull request?** * **Submit a pull request?**
Pull requests are welcome! If you're submitting a new feature, it's best to discuss first to make Pull requests are welcome! If you're submitting a new feature, it's best to discuss first to make
sure it'll be accepted. Feel free to come chat in [#modding on Discord](https://stardewvalleywiki.com/Modding:Community#Discord) sure it'll be accepted. Feel free to come chat [on Discord or in the SMAPI discussion thread](https://smapi.io/community).
or post in the [SMAPI support thread](http://community.playstarbound.com/threads/108375).
Documenting your code and using the same formatting conventions is appreciated, but don't worry too Documenting your code and using the same formatting conventions is appreciated, but don't worry too
much about it. We'll fix up the code after we accept the pull request if needed. much about it. We'll fix up the code after we accept the pull request if needed.

View File

@ -6,10 +6,8 @@ about: Report a problem with SMAPI.
<!-- <!--
Only report a bug here if you're sure it's a SMAPI bug! To request support instead, see: Only report a bug here if you're sure it's a SMAPI bug!
- #modding on Discord: https://stardewvalleywiki.com/Modding:Community#Discord To request support instead, see https://smapi.io/community.
- support forum thread: https://community.playstarbound.com/threads/108375
- Nexus mod page: https://www.nexusmods.com/stardewvalley/mods/2400
Replace the instructions below with the bug details. Replace the instructions below with the bug details.
@ -26,7 +24,7 @@ Exact steps which reproduce the bug, if possible. For example:
4. Error occurs. 4. Error occurs.
**Log file** **Log file**
Upload your SMAPI log to https://log.smapi.io and post a link here. Upload your SMAPI log to https://smapi.io/log and post a link here.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.

View File

@ -6,10 +6,7 @@ about: Suggest an idea for SMAPI.
<!-- <!--
GitHub issues are only used for development tasks. Please don't submit feature requests here! Instead, see... GitHub issues are only used for development tasks. Please don't submit feature requests here!
Instead, see https://smapi.io/community to discuss SMAPI.
- #modding on Discord: https://stardewvalleywiki.com/Modding:Community#Discord
- support forum thread: https://community.playstarbound.com/threads/108375
- Nexus page: https://www.nexusmods.com/stardewvalley/mods/2400
--> -->

View File

@ -6,10 +6,7 @@ about: Create a ticket about something else.
<!-- <!--
GitHub issues are only used for development tasks. For support and questions, see... GitHub issues are only used for development tasks.
For support and questions, see https://smapi.io/community instead.
- #modding on Discord: https://stardewvalleywiki.com/Modding:Community#Discord
- support forum thread: https://community.playstarbound.com/threads/108375
- Nexus page: https://www.nexusmods.com/stardewvalley/mods/2400
--> -->

5
.github/SUPPORT.md vendored
View File

@ -1,5 +1,4 @@
GitHub issues are only used for SMAPI development tasks. GitHub issues are only used for SMAPI development tasks.
To get help with SMAPI problems, you can... To get help with SMAPI problems, [ask on Discord or in the forums](https://smapi.io/community)
* [ask on Discord](https://stardewvalleywiki.com/Modding:Community#Discord); instead.
* or post in the [SMAPI support thread](https://community.playstarbound.com/threads/108375).

8
.gitignore vendored
View File

@ -18,6 +18,9 @@ _ReSharper*/
*.[Rr]e[Ss]harper *.[Rr]e[Ss]harper
*.DotSettings.user *.DotSettings.user
# Rider
.idea/
# NuGet packages # NuGet packages
*.nupkg *.nupkg
**/packages/* **/packages/*
@ -28,4 +31,7 @@ _ReSharper*/
appsettings.Development.json appsettings.Development.json
# AWS generated files # AWS generated files
src/SMAPI.Web/aws-beanstalk-tools-defaults.json src/SMAPI.Web.LegacyRedirects/aws-beanstalk-tools-defaults.json
# Azure generated files
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml

View File

@ -1,5 +0,0 @@
using System.Reflection;
[assembly: AssemblyProduct("SMAPI")]
[assembly: AssemblyVersion("2.11.3")]
[assembly: AssemblyFileVersion("2.11.3")]

70
build/common.targets Normal file
View File

@ -0,0 +1,70 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="find-game-folder.targets" />
<!--set properties -->
<PropertyGroup>
<Version>3.1.0</Version>
<Product>SMAPI</Product>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
</PropertyGroup>
<!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors -->
<Target Name="ValidateInstallPath" AfterTargets="BeforeBuild">
<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>
<!-- copy files into game directory and enable debugging -->
<Target Name="CopySmapiFiles" AfterTargets="AfterBuild">
<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)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.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 -->
<PropertyGroup>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<!-- launch SMAPI through Visual Studio -->
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'SMAPI'">
<StartAction>Program</StartAction>
<StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram>
<StartWorkingDirectory>$(GamePath)</StartWorkingDirectory>
</PropertyGroup>
<!-- Somehow this makes Visual Studio for Mac recognise the previous section. Nobody knows why. -->
<PropertyGroup Condition="'$(RunConfiguration)' == 'Default'" />
</Project>

View File

@ -0,0 +1,47 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- 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('$(USERPROFILE)\stardewvalley.targets')" Project="$(USERPROFILE)\stardewvalley.targets" />
<!-- find game path -->
<Choose>
<When Condition="$(OS) == 'Unix' OR $(OS) == 'OSX'">
<PropertyGroup>
<!-- Linux -->
<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)/.local/share/Steam/steamapps/common/Stardew Valley</GamePath>
<!-- Mac (may be 'Unix' or 'OSX') -->
<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>
</PropertyGroup>
</When>
<When Condition="$(OS) == 'Windows_NT'">
<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 -->
<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>
<!-- derive from Steam library path -->
<_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>
</PropertyGroup>
</When>
</Choose>
<!-- set game metadata -->
<PropertyGroup>
<GameExecutableName>Stardew Valley</GameExecutableName>
<GameExecutableName Condition="$(OS) != 'Windows_NT'">StardewValley</GameExecutableName>
</PropertyGroup>
</Project>

View File

@ -17,6 +17,9 @@
<PlatformName>windows</PlatformName> <PlatformName>windows</PlatformName>
<PlatformName Condition="$(OS) != 'Windows_NT'">unix</PlatformName> <PlatformName Condition="$(OS) != 'Windows_NT'">unix</PlatformName>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<TranslationFiles Include="$(CompiledSmapiPath)\i18n\*.json" />
</ItemGroup>
<!-- reset package directory --> <!-- reset package directory -->
<RemoveDir Directories="$(PackagePath)" /> <RemoveDir Directories="$(PackagePath)" />
@ -38,14 +41,15 @@
<Copy SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledSmapiPath)\SMAPI.config.json" DestinationFiles="$(PackagePath)\bundle\smapi-internal\config.json" />
<Copy SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.metadata.json" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledSmapiPath)\SMAPI.metadata.json" DestinationFiles="$(PackagePath)\bundle\smapi-internal\metadata.json" />
<Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy SourceFiles="$(CompiledToolkitPath)\SMAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(PackagePath)\bundle\smapi-internal\i18n" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(TargetDir)\unix-launcher.sh" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(TargetDir)\unix-launcher.sh" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
@ -59,7 +63,7 @@
<Copy SourceFiles="$(CompiledModsPath)\SaveBackup\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" /> <Copy SourceFiles="$(CompiledModsPath)\SaveBackup\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<Copy SourceFiles="$(CompiledModsPath)\SaveBackup\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" /> <Copy SourceFiles="$(CompiledModsPath)\SaveBackup\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<!-- fix errors on Linux/Mac (sample: https://log.smapi.io/mMdFUpgB) --> <!-- fix errors on Linux/Mac (sample: https://smapi.io/log/mMdFUpgB) -->
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(RootPath)\build\lib\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(RootPath)\build\lib\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(RootPath)\build\lib\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" /> <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(RootPath)\build\lib\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
@ -76,7 +80,7 @@
<RemoveDir Directories="$(PackageDevPath)\bundle" /> <RemoveDir Directories="$(PackageDevPath)\bundle" />
<!-- finalise normal installer --> <!-- finalise normal installer -->
<ReplaceFileText FilePath="$(PackagePath)\bundle\smapi-internal\StardewModdingAPI.config.json" Search="&quot;DeveloperMode&quot;: true" Replace="&quot;DeveloperMode&quot;: false" /> <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" /> <ZipDirectory FromDirPath="$(PackagePath)\bundle" ToFilePath="$(PackagePath)\internal\$(PlatformName)-install.dat" />
<RemoveDir Directories="$(PackagePath)\bundle" /> <RemoveDir Directories="$(PackagePath)\bundle" />
</Target> </Target>

View File

@ -11,13 +11,13 @@
</PropertyGroup> </PropertyGroup>
<RemoveDir Directories="$(PackagePath)" /> <RemoveDir Directories="$(PackagePath)" />
<Copy SourceFiles="$(ProjectDir)/package.nuspec" DestinationFolder="$(PackagePath)" /> <Copy SourceFiles="$(ProjectDir)/package.nuspec" DestinationFolder="$(PackagePath)" />
<Copy SourceFiles="$(SolutionDir)/../build/find-game-folder.targets" DestinationFolder="$(PackagePath)/build" />
<Copy SourceFiles="$(ProjectDir)/build/smapi.targets" DestinationFiles="$(PackagePath)/build/Pathoschild.Stardew.ModBuildConfig.targets" /> <Copy SourceFiles="$(ProjectDir)/build/smapi.targets" DestinationFiles="$(PackagePath)/build/Pathoschild.Stardew.ModBuildConfig.targets" />
<Copy SourceFiles="$(TargetDir)/assets/nuget-icon.png" DestinationFiles="$(PackagePath)/images/icon.png" />
<Copy SourceFiles="$(TargetDir)/Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)/build" /> <Copy SourceFiles="$(TargetDir)/Newtonsoft.Json.dll" DestinationFolder="$(PackagePath)/build" />
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.ModBuildConfig.dll" DestinationFolder="$(PackagePath)/build" /> <Copy SourceFiles="$(TargetDir)/SMAPI.ModBuildConfig.dll" DestinationFolder="$(PackagePath)/build" />
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.Toolkit.dll" DestinationFolder="$(PackagePath)/build" /> <Copy SourceFiles="$(TargetDir)/SMAPI.Toolkit.dll" DestinationFolder="$(PackagePath)/build" />
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)/build" /> <Copy SourceFiles="$(TargetDir)/SMAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackagePath)/build" />
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/bin/netstandard1.3/StardewModdingAPI.ModBuildConfig.Analyzer.dll" DestinationFolder="$(PackagePath)/analyzers/dotnet/cs" /> <Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/bin/netstandard2.0/SMAPI.ModBuildConfig.Analyzer.dll" DestinationFolder="$(PackagePath)/analyzers/dotnet/cs" />
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1" DestinationFolder="$(PackagePath)/tools" />
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1" DestinationFolder="$(PackagePath)/tools" />
</Target> </Target>
</Project> </Project>

View File

@ -19,11 +19,14 @@ doesn't change any of your game files. It serves eight main purposes:
_SMAPI detects when a mod accesses part of the game that changed in a game update which affects _SMAPI detects when a mod accesses part of the game that changed in a game update which affects
many mods, and rewrites the mod so it's compatible._ many mods, and rewrites the mod so it's compatible._
5. **Intercept errors.** 5. **Intercept errors and automatically fix saves.**
_SMAPI intercepts errors that happen in the game, displays the error details in the console _SMAPI intercepts errors, shows the error info in the SMAPI console, and in most cases
window, and in most cases automatically recovers the game. This prevents mods from accidentally automatically recovers the game. That prevents mods from crashing the game, and makes it
crashing the game, and makes it possible to troubleshoot errors in the game itself that would possible to troubleshoot errors in the game itself that would otherwise show a generic 'program
otherwise show a generic 'program has stopped working' type of message._ has stopped working' type of message._
_SMAPI also automatically fixes save data in some cases when a load would crash, e.g. due to a
custom location or NPC mod that was removed._
6. **Provide update checks.** 6. **Provide update checks.**
_SMAPI automatically checks for new versions of your installed mods, and notifies you when any _SMAPI automatically checks for new versions of your installed mods, and notifies you when any
@ -38,16 +41,36 @@ doesn't change any of your game files. It serves eight main purposes:
something goes wrong. (Via the bundled SaveBackup mod.)_ something goes wrong. (Via the bundled SaveBackup mod.)_
## Documentation ## Documentation
Have questions? Come [chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other Have questions? Come [ask the community](https://smapi.io/community) to get help from SMAPI
modders! developers and other modders!
### For players ### For players
* [Player guide](https://stardewvalleywiki.com/Modding:Player_Guide) * [Player guide](https://stardewvalleywiki.com/Modding:Player_Guide)
### For modders ### For modders
* [Modding documentation](https://stardewvalleywiki.com/Modding:Index) * [Modding documentation](https://smapi.io/docs)
* [Mod build configuration](mod-build-config.md) * [Mod build configuration](technical/mod-package.md)
* [Release notes](release-notes.md) * [Release notes](release-notes.md)
### For SMAPI developers ### For SMAPI developers
* [Technical docs](technical-docs.md) * [Technical docs](technical/smapi.md)
## Translating SMAPI
SMAPI rarely shows text in-game, so it only has a few translations. Contributions are welcome! See
[Modding:Translations](https://stardewvalleywiki.com/Modding:Translations) on the wiki for help
contributing translations.
locale | status
---------- | :----------------
default | ✓ [fully translated](../src/SMAPI/i18n/default.json)
Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json)
French | ❑ not translated
German | ✓ [fully translated](../src/SMAPI/i18n/de.json)
Hungarian | ❑ not translated
Italian | ❑ not translated
Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json)
Korean | ❑ not translated
Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json)
Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json)
Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json)
Turkish | ✓ [fully translated](../src/SMAPI/i18n/tr.json)

View File

@ -1,283 +1 @@
The **mod build package** is an open-source NuGet package which automates the MSBuild configuration [Documentation moved](technical/mod-package.md).
for SMAPI mods.
The package...
* detects your game install path;
* adds the assembly references you need (with automatic support for Linux/Mac/Windows);
* packages the mod into your `Mods` folder when you rebuild the code (configurable);
* configures Visual Studio to enable debugging into the code when the game is running (_Windows only_);
* adds C# analyzers to warn for Stardew Valley-specific issues.
## Contents
* [Install](#install)
* [Configure](#configure)
* [Code analysis warnings](#code-analysis-warnings)
* [Troubleshoot](#troubleshoot)
* [Release notes](#release-notes)
## Install
**When creating a new mod:**
1. Create an empty library project.
2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig).
3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod).
4. Compile on any platform.
**When migrating an existing mod:**
1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley,
`StardewModdingAPI`, and `xTile`.
2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig).
3. Compile on any platform.
## Configure
### Deploy files into the `Mods` folder
By default, your mod will be copied into the game's `Mods` folder (with a subfolder matching your
project name) when you rebuild the code. The package will automatically include your
`manifest.json`, any `i18n` files, and the build output.
To add custom files to the mod folder, just [add them to the build output](https://stackoverflow.com/a/10828462/262123).
(If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).)
You can change the mod's folder name by adding this above the first `</PropertyGroup>` in your
`.csproj`:
```xml
<ModFolderName>YourModName</ModFolderName>
```
If you don't want to deploy the mod automatically, you can add this:
```xml
<EnableModDeploy>False</EnableModDeploy>
```
### Create release zip
By default, a zip file will be created in the build output when you rebuild the code. This zip file
contains all the files needed to share your mod in the recommended format for uploading to Nexus
Mods or other sites.
You can change the zipped folder name (and zip name) by adding this above the first
`</PropertyGroup>` in your `.csproj`:
```xml
<ModFolderName>YourModName</ModFolderName>
```
You can change the folder path where the zip is created like this:
```xml
<ModZipPath>$(SolutionDir)\_releases</ModZipPath>
```
Finally, you can disable the zip creation with this:
```xml
<EnableModZip>False</EnableModZip>
```
Or only create it in release builds with this:
```xml
<EnableModZip Condition="$(Configuration) != 'Release'">False</EnableModZip>
```
### Game path
The package usually detects where your game is installed automatically. If it can't find your game
or you have multiple installs, you can specify the path yourself. There's two ways to do that:
* **Option 1: global game path (recommended).**
_This will apply to every project that uses the package._
1. Get the full folder path containing the Stardew Valley executable.
2. Create this file:
platform | path
--------- | ----
Linux/Mac | `~/stardewvalley.targets`
Windows | `%USERPROFILE%\stardewvalley.targets`
3. Save the file with this content:
```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
</Project>
```
4. Replace `PATH_HERE` with your game path.
* **Option 2: path in the project file.**
_You'll need to do this for each project that uses the package._
1. Get the folder path containing the Stardew Valley `.exe` file.
2. Add this to your `.csproj` file under the `<Project` line:
```xml
<PropertyGroup>
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
```
3. Replace `PATH_HERE` with your custom game install path.
The configuration will check your custom path first, then fall back to the default paths (so it'll
still compile on a different computer).
### Ignore files
If you don't want to include a file in the mod folder or release zip:
* Make sure it's not copied to the build output. For a DLL, make sure the reference is [not marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).
* Or add this to your `.csproj` file under the `<Project` line:
```xml
<IgnoreModFilePatterns>\.txt$, \.pdf$</IgnoreModFilePatterns>
```
This is a comma-delimited list of regular expression patterns. If any pattern matches a file's
relative path in your mod folder, that file won't be included.
### Non-mod projects
You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). You'll need to
disable deploying the mod and creating a release zip:
```xml
<EnableModDeploy>False</EnableModDeploy>
<EnableModZip>False</EnableModZip>
```
If this is for unit tests, you may need to copy the referenced DLLs into your build output too:
```xml
<CopyModReferencesToBuildOutput>True</CopyModReferencesToBuildOutput>
```
## Code warnings
### Overview
The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example:
![](screenshots/code-analyzer-example.png)
You can hide the warnings using the warning ID (shown under 'code' in the Error List). See...
* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning);
* for a method using this attribute:
```cs
[System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")]
```
* for an entire project:
1. Expand the _References_ node for the project in Visual Studio.
2. Right-click on _Analyzers_ and choose _Open Active Rule Set_.
4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide.
See below for help with each specific warning.
### Avoid implicit net field cast
Warning text:
> This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but
> {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual
> value instead to avoid bugs.
Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplayer sync. These types
can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`), but their
conversion rules are unintuitive and error-prone. For example,
`item?.category == null && item?.category != null` can both be true at once, and
`building.indoors != null` can be true for a null value.
Suggested fix:
* Some net fields have an equivalent non-net property like `monster.Health` (`int`) instead of
`monster.health` (`NetInt`). The package will add a separate [AvoidNetField](#avoid-net-field) warning for
these. Use the suggested property instead.
* For a reference type (i.e. one that can contain `null`), you can use the `.Value` property:
```c#
if (building.indoors.Value == null)
```
Or convert the value before comparison:
```c#
GameLocation indoors = building.indoors;
if(indoors == null)
// ...
```
* For a value type (i.e. one that can't contain `null`), check if the object is null (if applicable)
and compare with `.Value`:
```cs
if (item != null && item.category.Value == 0)
```
### Avoid net field
Warning text:
> '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead.
Your code accesses a net field, which has some unusual behavior (see [AvoidImplicitNetFieldCast](#avoid-implicit-net-field-cast)).
This field has an equivalent non-net property that avoids those issues.
Suggested fix: access the suggested property name instead.
### Avoid obsolete field
Warning text:
> The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'.
Your code accesses a field which is obsolete or no longer works. Use the suggested field instead.
## Troubleshoot
### "Failed to find the game install path"
That error means the package couldn't find your game. You can specify the game path yourself; see
_[Game path](#game-path)_ above.
## Release notes
### 2.2
* Added support for SMAPI 2.8+ (still compatible with earlier versions).
* Added default game paths for 32-bit Windows.
* Fixed valid manifests marked invalid in some cases.
### 2.1
* Added support for Stardew Valley 1.3.
* Added support for non-mod projects.
* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3.
* Added option to ignore files by regex pattern.
* Added reference to new SMAPI DLL.
* Fixed some game paths not detected by NuGet package.
### 2.0.2
* Fixed compatibility issue on Linux.
### 2.0.1
* Fixed mod deploy failing to create subfolders if they don't already exist.
### 2.0
* Added: mods are now copied into the `Mods` folder automatically (configurable).
* Added: release zips are now created automatically in your build output folder (configurable).
* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI.
* Added mod's version to release zip filename.
* Improved errors to simplify troubleshooting.
* Fixed release zip not having a mod folder.
* Fixed release zip failing if mod name contains characters that aren't valid in a filename.
### 1.7.1
* Fixed issue where i18n folders were flattened.
* The manifest/i18n files in the project now take precedence over those in the build output if both
are present.
### 1.7
* Added option to create release zips on build.
* Added reference to XNA's XACT library for audio-related mods.
### 1.6
* 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.
### 1.5
* Added support for setting a custom game path globally.
* Added default GOG path on Mac.
### 1.4
* Fixed detection of non-default game paths on 32-bit Windows.
* Removed support for SilVerPLuM (discontinued).
* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms
mods automatically).
### 1.3
* Added support for non-default game paths on Windows.
### 1.2
* Exclude game binaries from mod build output.
### 1.1
* Added support for overriding the target platform.
### 1.0
* Initial release.
* Added support for detecting the game path automatically.
* Added support for injecting XNA/MonoGame references automatically based on the OS.
* Added support for mod builders like SilVerPLuM.

View File

@ -1,4 +1,216 @@
&larr; [README](README.md)
# Release notes # Release notes
## 3.1
Released 05 January 2019 for Stardew Valley 1.4 or later.
* For players:
* Added separate group in 'skipped mods' list for broken dependencies, so it's easier to see what to fix first.
* Added friendly log message for save file-not-found errors.
* Updated for gamepad modes in Stardew Valley 1.4.1.
* Improved performance in some cases.
* Fixed compatibility with Linux Mint 18 (thanks to techge!), Arch Linux, and Linux systems with libhybris-utils installed.
* Fixed memory leak when repeatedly loading a save and returning to title.
* Fixed memory leak when mods reload assets.
* Fixes for Console Commands mod:
* added new clothing items;
* fixed spawning new flooring and rings (thanks to Mizzion!);
* fixed spawning custom rings added by mods;
* Fixed errors when some item data is invalid.
* Updated translations. Thanks to L30Bola (added Portuguese), PlussRolf (added Spanish), and shirutan (added Japanese)!
* For the web UI:
* Added option to edit & reupload in the JSON validator.
* File uploads are now stored in Azure storage instead of Pastebin, due to ongoing Pastebin perfomance issues.
* File uploads now expire after one month.
* Updated the JSON validator for Content Patcher 1.10 and 1.11.
* Fixed JSON validator no longer letting you change format when viewing a file.
* Fixed JSON validator for Content Patcher not requiring `Default` if `AllowBlank` was omitted.
* 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.
* For modders:
* Added `World.ChestInventoryChanged` event (thanks to collaboration with wartech0!).
* Added asset propagation for...
* grass textures;
* winter flooring textures;
* `Data\Bundles` changes (for added bundles only);
* `Characters\Farmer\farmer_girl_base_bald`.
* Added paranoid-mode warning for direct `Console` access.
* Improved error messages for `TargetParameterCountException` when using the reflection API.
* `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event).
* Removed `DumpMetadata` option. It was only for specific debugging cases, but players would sometimes enable it incorrectly and then report crashes.
* Fixed private textures loaded from content packs not having their `Name` field set.
* For SMAPI developers:
* You can now run local environments without configuring Amazon, Azure, MongoDB, and Pastebin accounts.
## 3.0.1
Released 02 December 2019 for Stardew Valley 1.4 or later.
* For players:
* Updated for Stardew Valley 1.4.0.1.
* Improved compatibility with some Linux terminals (thanks to archification and DanielHeath!).
* Updated translations. Thanks to berkayylmao (added Turkish), feathershine (added Chinese), and Osiris901 (added Russian)!
* For the web UI:
* Rebuilt web infrastructure to handle higher traffic.
* 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.
* For modders:
* `SemanticVersion` now supports [semver 2.0](https://semver.org/) build metadata.
## 3.0
Released 26 November 2019 for Stardew Valley 1.4.
### Release highlights
For players:
* **Updated for Stardew Valley 1.4.**
SMAPI 3.0 adds compatibility with the latest game version, and improves mod APIs for changes in
the game code.
* **Improved performance.**
SMAPI should have less impact on game performance and startup time for some players.
* **Automatic save fixing and more error recovery.**
SMAPI now detects and prevents more crashes due to game/mod bugs, and automatically fixes your
save if you remove some custom-content mods.
* **Improved mod scanning.**
SMAPI now supports some non-standard mod structures automatically, improves compatibility with
the Vortex mod manager, and improves various error/skip messages related to mod loading.
* **Overhauled update checks.**
SMAPI update checks are now handled entirely on the web server and support community-defined
version mappings. In particular, false update alerts due to author mistakes can now be solved by
the community for all players.
* **Fixed many bugs and edge cases.**
For modders:
* **New event system.**
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
existing events more useful, and make event usage and behavior more consistent. When a mod makes
changes in an event handler, those changes are now also reflected in the next event raise.
* **Improved mod build package.**
The [mod build package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig) now
includes the `assets` folder by default if present, supports the new `.csproj` project format,
enables mod `.pdb` files automatically (to provide line numbers in error messages), adds optional
Harmony support, and fixes some bugs and edge cases. This also adds compatibility with SMAPI 3.0
and Stardew Valley 1.4, and drops support for older versions.
* **Mods loaded earlier.**
SMAPI now loads mods much earlier, before the game is initialised. That lets mods do things that
were difficult before, like intercepting some core assets.
* **Improved Android support.**
SMAPI now automatically detects when it's running on Android, and updates `Constants.TargetPlatform`
so mods can adjust their logic if needed. The Save Backup mod is also now Android-compatible.
* **Improved asset propagation.**
SMAPI now automatically propagates asset changes for farm animal data, NPC default location data,
critter textures, and `DayTimeMoneyBox` buttons. Every loaded texture now also has a `Name` field
so mods can check which asset a texture was loaded for.
* **Breaking changes:**
See _[migrate to SMAPI 3.0](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0)_ and
_[migrate to Stardew Valley 1.4](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.4)_
for more info.
### For players
* Changes:
* Updated for Stardew Valley 1.4.
* Improved performance.
* Reworked update checks and added community-defined version mapping, to reduce false update alerts due to author mistakes.
* SMAPI now removes invalid locations/NPCs when loading a save to prevent crashes. A warning is shown in-game when this happens.
* Added update checks for CurseForge mods.
* Added support for editing console colors via `smapi-internal/config.json` (for players with unusual consoles).
* Added support for setting SMAPI CLI arguments as environment variables for Linux/macOS compatibility.
* Improved mod scanning:
* Now ignores metadata files/folders (like `__MACOSX` and `__folder_managed_by_vortex`) and content files (like `.txt` or `.png`), which avoids missing-manifest errors in some cases.
* Now detects XNB mods more accurately, and consolidates multi-folder XNB mods in logged messages.
* Improved launch script compatibility on Linux (thanks to kurumushi and toastal!).
* Made error messages more user-friendly in some cases.
* Save Backup now works in the background, to avoid affecting startup time for players with a large number of saves.
* The installer now recognises custom game paths stored in [`stardewvalley.targets`](http://smapi.io/package/custom-game-path).
* Duplicate-mod errors now show the mod version in each folder.
* Update checks are now faster in some cases.
* Updated mod compatibility list.
* Updated SMAPI/game version map.
* Updated translations. Thanks to eren-kemer (added German)!
* Fixes:
* Fixed some assets not updated when you switch language to English.
* Fixed lag in some cases due to incorrect asset caching when playing in non-English.
* Fixed lag when a mod invalidates many NPC portraits/sprites at once.
* Fixed Console Commands not including upgraded tools in item commands.
* Fixed Console Commands' item commands failing if a mod adds invalid item data.
* Fixed Save Backup not pruning old backups if they're uncompressed.
* Fixed issues when a farmhand reconnects before the game notices they're disconnected.
* Fixed 'received message' logs shown in non-developer mode.
* Fixed various error messages and inconsistent spelling.
* Fixed update-check error if a Nexus mod is marked as adult content.
* Fixed update-check error if the Chucklefish page for an update key doesn't exist.
### For the web UI
* Mod compatibility list:
* Added support for CurseForge mods.
* Added metadata links and dev notes (if any) to advanced info.
* Now loads faster (since data is fetched in a background service).
* Now continues working with cached data when the wiki is offline.
* Clicking a mod link now automatically adds it to the visible mods if the list is filtered.
* JSON validator:
* Added JSON validator at [smapi.io/json](https://smapi.io/json), which lets you validate a JSON file against predefined mod formats.
* Added support for the `manifest.json` format.
* Added support for the Content Patcher format (thanks to TehPers!).
* Added support for referencing a schema in a JSON Schema-compatible text editor.
* For the log parser:
* Added instructions for Android.
* The page now detects your OS and preselects the right instructions (thanks to danvolchek!).
### For modders
* 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.
* Removed all deprecated APIs.
* Removed unused APIs: `Monitor.ExitGameImmediately`, `Translation.ModName`, and `Translation.Assert`.
* Fixed `ICursorPosition.AbsolutePixels` not adjusted for zoom.
* `SemanticVersion` no longer omits `.0` patch numbers when formatting versions, for better [semver](https://semver.org/) conformity (e.g. `3.0` is now formatted as `3.0.0`).
* Changes:
* Added support for content pack translations.
* Added `IContentPack.HasFile`, `Context.IsGameLaunched`, and `SemanticVersion.TryParse`.
* Added separate `LogNetworkTraffic` option to make verbose logging less overwhelmingly verbose.
* Added asset propagation for `Data\FarmAnimals`, critter textures, and `DayTimeMoneyBox` buttons.
* Added `Texture2D.Name` values set to the asset key.
* Added trace logs for skipped loose files in the `Mods` folder and custom SMAPI settings so it's easier to troubleshoot player logs.
* `Constants.TargetPlatform` now returns `Android` when playing on an Android device.
* Trace logs for a broken mod now list all detected issues (instead of the first one).
* Trace logs when loading mods are now more clear.
* Clarified update-check errors for mods with multiple update keys.
* Updated dependencies (including Json.NET 11.0.2 → 12.0.3 and Mono.Cecil 0.10.1 → 0.11.1).
* Fixes:
* Fixed map reloads resetting tilesheet seasons.
* Fixed map reloads not updating door warps.
* Fixed outdoor tilesheets being seasonalised when added to an indoor location.
* Fixed mods needing to load custom `Map` assets before the game accesses them. SMAPI now does so automatically.
* Fixed custom maps loaded from `.xnb` files not having their tilesheet paths automatically adjusted.
* Fixed custom maps loaded from the mod folder with tilesheets in a subfolder not working crossplatform. All tilesheet paths are now normalized for the OS automatically.
* Fixed issue where mod changes weren't tracked correctly for raising events in some cases. Events now reflect a frozen snapshot of the game state, and any mod changes are reflected in the next event tick.
* Fixed issue where, when a mod's `IAssetEditor` uses `asset.ReplaceWith` on a texture asset while playing in non-English, any changes from that point forth wouldn't affect subsequent cached asset loads.
* Fixed asset propagation for NPC portraits resetting any unique portraits (e.g. Maru's hospital portrait) to the default.
* Fixed changes to `Data\NPCDispositions` not always propagated correctly to existing NPCs.
* Fixed `Rendering`/`Rendered` events not raised during minigames.
* Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save.
* Fixed `GetApi` without an interface not checking if all mods are loaded.
### For SMAPI maintainers
* Added support for core translation files.
* Migrated to new `.csproj` format.
* Internal refactoring.
## 2.11.3 ## 2.11.3
Released 13 September 2019 for Stardew Valley 1.3.36. Released 13 September 2019 for Stardew Valley 1.3.36.
@ -12,11 +224,14 @@ Released 13 September 2019 for Stardew Valley 1.3.36.
* For the web UI: * For the web UI:
* When filtering the mod list, clicking a mod link now automatically adds it to the visible mods. * When filtering the mod list, clicking a mod link now automatically adds it to the visible mods.
* Added log parser instructions for Android. * Added log parser instructions for Android.
* Fixed log parser failing in some cases due to time format localisation. * Fixed log parser failing in some cases due to time format localization.
* For modders: * For modders:
* `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 changes to a map loaded by a mod being persisted across content managers.
* Fixed `SDate.AddDays` incorrectly changing year when the result is exactly winter 28.
## 2.11.2 ## 2.11.2
Released 23 April 2019 for Stardew Valley 1.3.36. Released 23 April 2019 for Stardew Valley 1.3.36.
@ -77,12 +292,12 @@ Released 09 January 2019 for Stardew Valley 1.3.3233.
* 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.
* Fixed error when swapping maps mid-session for a location with interior doors. * Fixed error when swapping maps mid-session for a location with interior doors.
* Fixed `Constants.SaveFolderName` and `CurrentSavePath` not available during early load stages when using `Specialised.LoadStageChanged` event. * Fixed `Constants.SaveFolderName` and `CurrentSavePath` not available during early load stages when using `Specialized.LoadStageChanged` event.
* Fixed `LoadStage.SaveParsed` raised before the parsed save data is available. * Fixed `LoadStage.SaveParsed` raised before the parsed save data is available.
* 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 `Specialised.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 capitalisation 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/Mac 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.
@ -99,9 +314,9 @@ Released 29 December 2018 for Stardew Valley 1.3.3233.
* Tweaked installer to reduce antivirus false positives. * Tweaked installer to reduce antivirus false positives.
* For modders: * For modders:
* Added [events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events): `GameLoop.OneSecondUpdateTicking`, `GameLoop.OneSecondUpdateTicked`, and `Specialised.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 initialised). * You can now use `helper.Data.Read/WriteSaveData` as soon as the save is loaded (instead of once the world is initialized).
* Increased deprecation levels to _info_ for the upcoming SMAPI 3.0. * Increased deprecation levels to _info_ for the upcoming SMAPI 3.0.
* For the web UI: * For the web UI:
@ -211,7 +426,7 @@ Released 19 November 2018 for Stardew Valley 1.3.32.
* Updated compatibility list. * Updated compatibility list.
* For the web UI: * For the web UI:
* Added a [mod compatibility page](https://mods.smapi.io) and [privacy page](https://smapi.io/privacy). * Added a [mod compatibility page](https://smapi.io/mods) and [privacy page](https://smapi.io/privacy).
* 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!).
@ -274,7 +489,7 @@ Released 14 August 2018 for Stardew Valley 1.3.28.
* dialogue; * dialogue;
* map tilesheets. * map tilesheets.
* Added `--mods-path` CLI command-line argument to switch between mod folders. * Added `--mods-path` CLI command-line argument to switch between mod folders.
* All enums are now JSON-serialised by name instead of numeric value. (Previously only a few enums were serialised that way. JSON files which already have numeric enum values will still be parsed fine.) * All enums are now JSON-serialized by name instead of numeric value. (Previously only a few enums were serialized that way. JSON files which already have numeric enum values will still be parsed fine.)
* Fixed false compatibility error when constructing multidimensional arrays. * Fixed false compatibility error when constructing multidimensional arrays.
* Fixed `.ToSButton()` methods not being public. * Fixed `.ToSButton()` methods not being public.
@ -301,7 +516,7 @@ Released 01 August 2018 for Stardew Valley 1.3.27.
* Improved the Console Commands mod: * Improved the Console Commands mod:
* Added `player_add name`, which adds items to your inventory by name instead of ID. * Added `player_add name`, which adds items to your inventory by name instead of ID.
* Fixed `world_setseason` not running season-change logic. * Fixed `world_setseason` not running season-change logic.
* Fixed `world_setseason` not normalising the season value. * Fixed `world_setseason` not normalizing the season value.
* Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed).
* 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.
@ -392,10 +607,10 @@ Released 11 April 2018 for Stardew Valley 1.2.301.2.33.
* Fixed mod update alerts not shown if one mod has an invalid remote version. * Fixed mod update alerts not shown if one mod has an invalid remote version.
* Fixed SMAPI update alerts linking to the GitHub repository instead of [smapi.io](https://smapi.io). * Fixed SMAPI update alerts linking to the GitHub repository instead of [smapi.io](https://smapi.io).
* Fixed SMAPI update alerts for draft releases. * Fixed SMAPI update alerts for draft releases.
* Fixed error when two content packs use different capitalisation for the same required mod ID. * Fixed error when two content packs use different capitalization for the same required mod ID.
* Fixed rare crash if the game duplicates an item. * Fixed rare crash if the game duplicates an item.
* For the [log parser](https://log.smapi.io): * For the [log parser](https://smapi.io/log):
* Tweaked UI. * Tweaked UI.
## 2.5.4 ## 2.5.4
@ -407,7 +622,7 @@ Released 26 March 2018 for Stardew Valley 1.2.301.2.33.
* 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://log.smapi.io): * 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 modders:
@ -429,7 +644,7 @@ Released 13 March 2018 for Stardew Valley ~~1.2.30~~1.2.33.
* Fixed Linux ["magic number is wrong" errors](https://github.com/mono/mono/issues/6752) by changing default terminal order. * Fixed Linux ["magic number is wrong" errors](https://github.com/mono/mono/issues/6752) by changing default terminal order.
* Updated compatibility list and added update checks for more mods. * Updated compatibility list and added update checks for more mods.
* For the [log parser](https://log.smapi.io): * For the [log parser](https://smapi.io/log):
* Fixed incorrect filtering in some cases. * Fixed incorrect filtering in some cases.
* Fixed error if mods have duplicate names. * Fixed error if mods have duplicate names.
* Fixed parse bugs if a mod has no author name. * Fixed parse bugs if a mod has no author name.
@ -443,7 +658,7 @@ Released 25 February 2018 for Stardew Valley 1.2.301.2.33.
* For modders: * For modders:
* 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://log.smapi.io): * For the [log parser](https://smapi.io/log):
* Fixed blank page after uploading a log in some cases. * Fixed blank page after uploading a log in some cases.
## 2.5.1 ## 2.5.1
@ -467,14 +682,14 @@ Released 24 February 2018 for Stardew Valley 1.2.301.2.33.
* For modders: * For modders:
* 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 `SpecialisedEvents.UnvalidatedUpdateTick` event for specialised use cases. * Added `SpecializedEvents.UnvalidatedUpdateTick` event for specialized use cases.
* Added path normalising to `ReadJsonFile` and `WriteJsonFile` helpers (so no longer need `Path.Combine` with those). * Added path normalizing to `ReadJsonFile` and `WriteJsonFile` helpers (so no longer need `Path.Combine` with those).
* Fixed deadlock in rare cases with asset loaders. * Fixed deadlock in rare cases with asset loaders.
* Fixed unhelpful error when a mod exposes a non-public API. * Fixed unhelpful error when a mod exposes a non-public API.
* Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity. * Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity.
* Fixed some JSON field names being case-sensitive. * Fixed some JSON field names being case-sensitive.
* For the [log parser](https://log.smapi.io): * For the [log parser](https://smapi.io/log):
* Added support for SMAPI 2.5 content packs. * Added support for SMAPI 2.5 content packs.
* Reduced download size when viewing a parsed log with repeated errors. * Reduced download size when viewing a parsed log with repeated errors.
* Improved parse error handling. * Improved parse error handling.
@ -495,7 +710,7 @@ Released 24 January 2018 for Stardew Valley 1.2.301.2.33.
* Fixed intermittent errors (e.g. 'collection has been modified') with some mods when loading a save. * Fixed intermittent errors (e.g. 'collection has been modified') with some mods when loading a save.
* Fixed compatibility with Linux Terminator terminal. * Fixed compatibility with Linux Terminator terminal.
* For the [log parser](https://log.smapi.io): * 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 modders:
@ -521,16 +736,16 @@ Released 26 December 2017 for Stardew Valley 1.2.301.2.33.
* For modders: * For modders:
* **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 initialised). * 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.
* Added trace message for mods with no update keys. * Added trace message for mods with no update keys.
* Adjusted reflection API to match actual usage (e.g. renamed `GetPrivate*` to `Get*`), and deprecated previous methods. * Adjusted reflection API to match actual usage (e.g. renamed `GetPrivate*` to `Get*`), and deprecated previous methods.
* Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialized cases.
* Fixed reflection API error for properties missing a `get` and `set`. * Fixed reflection API error for properties missing a `get` and `set`.
* Fixed issue where a mod could change the cursor position reported to other mods. * Fixed issue where a mod could change the cursor position reported to other mods.
* Updated compatibility list. * Updated compatibility list.
* For the [log parser](https://log.smapi.io): * For the [log parser](https://smapi.io/log):
* Fixed broken favicon. * Fixed broken favicon.
## 2.2 ## 2.2
@ -544,13 +759,13 @@ Released 02 December 2017 for Stardew Valley 1.2.301.2.33.
* 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.
* For the [log parser](https://log.smapi.io): * For the [log parser](https://smapi.io/log):
* Logs no longer expire after a week. * Logs no longer expire after a week.
* 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 modders:
* Added `helper.Content.NormaliseAssetName` 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.
* Fixed input events' `e.SuppressButton()` method not working with mouse buttons. * Fixed input events' `e.SuppressButton()` method not working with mouse buttons.
@ -559,7 +774,7 @@ Released 02 December 2017 for Stardew Valley 1.2.301.2.33.
Released 01 November 2017 for Stardew Valley 1.2.301.2.33. Released 01 November 2017 for Stardew Valley 1.2.301.2.33.
* For players: * For players:
* Added a [log parser](https://log.smapi.io) site. * Added a [log parser](https://smapi.io/log) site.
* Added better Steam instructions to the SMAPI installer. * Added better Steam instructions to the SMAPI installer.
* Renamed the bundled _TrainerMod_ to _ConsoleCommands_ to make its purpose clearer. * Renamed the bundled _TrainerMod_ to _ConsoleCommands_ to make its purpose clearer.
* Removed the game's test messages from the console log. * Removed the game's test messages from the console log.
@ -634,7 +849,7 @@ Released 14 October 2017 for Stardew Valley 1.2.301.2.33.
* **Command-line install** * **Command-line install**
For power users and mod managers, the SMAPI installer can now be scripted using command-line arguments For power users and mod managers, the SMAPI installer can now be scripted using command-line arguments
(see [technical docs](technical-docs.md#command-line-arguments)). (see [technical docs](technical/smapi.md#command-line-arguments)).
### Change log ### Change log
For players: For players:
@ -705,7 +920,7 @@ For mod developers:
* Added content helper properties for the game's current language. * Added content helper properties for the game's current language.
* Fixed `Context.IsPlayerFree` being false if the player is performing an action. * Fixed `Context.IsPlayerFree` being false if the player is performing an action.
* Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. * Fixed `GraphicsEvents.Resize` being raised before the game updates its window data.
* Fixed `SemanticVersion` not being deserialisable through Json.NET. * Fixed `SemanticVersion` not being deserializable through Json.NET.
* Fixed terminal not launching on Xfce Linux. * Fixed terminal not launching on Xfce Linux.
For SMAPI developers: For SMAPI developers:
@ -776,7 +991,7 @@ For modders:
* SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. * SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder.
<small>_When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._</small> <small>_When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._</small>
* Added `Context.IsPlayerFree` for mods that need to check if the player can act (i.e. save is loaded, no menu is displayed, no cutscene is in progress, etc). * Added `Context.IsPlayerFree` for mods that need to check if the player can act (i.e. save is loaded, no menu is displayed, no cutscene is in progress, etc).
* Added `Context.IsInDrawLoop` for specialised mods. * Added `Context.IsInDrawLoop` for specialized mods.
* 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.
@ -803,7 +1018,7 @@ For players:
For mod developers: For mod developers:
* Added a `Context.IsWorldReady` flag for mods to use. * Added a `Context.IsWorldReady` flag for mods to use.
<small>_This indicates whether a save is loaded and the world is finished initialising, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful for events which can be raised before the world is loaded (like update tick)._</small> <small>_This indicates whether a save is loaded and the world is finished initializing, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful for events which can be raised before the world is loaded (like update tick)._</small>
* Added a `debug` console command which lets you run the game's debug commands (e.g. `debug warp FarmHouse 1 1` warps you to the farmhouse). * Added a `debug` console command which lets you run the game's debug commands (e.g. `debug warp FarmHouse 1 1` warps you to the farmhouse).
* Added basic context info to logs to simplify troubleshooting. * Added basic context info to logs to simplify troubleshooting.
* Added a `Mod.Dispose` method which can be overriden to clean up before exit. This method isn't guaranteed to be called on every exit. * Added a `Mod.Dispose` method which can be overriden to clean up before exit. This method isn't guaranteed to be called on every exit.
@ -841,8 +1056,8 @@ For players:
For mod developers: For mod developers:
* Added a content API which loads custom textures/maps/data from the mod's folder (`.xnb` or `.png` format) or game content. * Added a content API which loads custom textures/maps/data from the mod's folder (`.xnb` or `.png` format) or game content.
* `Console.Out` messages are now written to the log file. * `Console.Out` messages are now written to the log file.
* `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly. * `Monitor.ExitGameImmediately` now aborts SMAPI initialization and events more quickly.
* Fixed value-changed events being raised when the player loads a save due to values being initialised. * Fixed value-changed events being raised when the player loads a save due to values being initialized.
## 1.10 ## 1.10
Released 24 April 2017 for Stardew Valley 1.2.26. Released 24 April 2017 for Stardew Valley 1.2.26.
@ -858,7 +1073,7 @@ For players:
* Replaced `player_addmelee` with `player_addweapon` with support for non-melee weapons. * Replaced `player_addmelee` with `player_addweapon` with support for non-melee weapons.
For mod developers: For mod developers:
* Mods are now initialised after the `Initialize`/`LoadContent` phase, which means the `GameEvents.Initialize` and `GameEvents.LoadContent` events are deprecated. You can move any logic in those methods to your mod's `Entry` method. * Mods are now initialized after the `Initialize`/`LoadContent` phase, which means the `GameEvents.Initialize` and `GameEvents.LoadContent` events are deprecated. You can move any logic in those methods to your mod's `Entry` method.
* Added `IsBetween` and string overloads to the `ISemanticVersion` methods. * Added `IsBetween` and string overloads to the `ISemanticVersion` methods.
* Fixed mouse-changed event never updating prior mouse position. * Fixed mouse-changed event never updating prior mouse position.
* Fixed `monitor.ExitGameImmediately` not working correctly. * Fixed `monitor.ExitGameImmediately` not working correctly.
@ -897,7 +1112,7 @@ For mod developers:
* The SMAPI log now has a simpler filename. * The SMAPI log now has a simpler filename.
* The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. * The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available.
* The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing. * The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing.
* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised. * Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialized.
* Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly. * Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly.
* Several obsolete APIs have been removed (see [migration guides](https://stardewvalleywiki.com/Modding:Index#Migration_guides)), * Several obsolete APIs have been removed (see [migration guides](https://stardewvalleywiki.com/Modding:Index#Migration_guides)),
and all _notice_-level deprecations have been increased to _info_. and all _notice_-level deprecations have been increased to _info_.
@ -942,7 +1157,7 @@ For mod developers:
* Added a mod registry which provides metadata about loaded mods. * Added a mod registry which provides metadata about loaded mods.
* The `Entry(…)` method is now deferred until all mods are loaded. * The `Entry(…)` method is now deferred until all mods are loaded.
* Fixed `SaveEvents.BeforeSave` and `.AfterSave` not triggering on days when the player shipped something. * Fixed `SaveEvents.BeforeSave` and `.AfterSave` not triggering on days when the player shipped something.
* Fixed `PlayerEvents.LoadedGame` and `SaveEvents.AfterLoad` being fired before the world finishes initialising. * Fixed `PlayerEvents.LoadedGame` and `SaveEvents.AfterLoad` being fired before the world finishes initializing.
* Fixed some `LocationEvents`, `PlayerEvents`, and `TimeEvents` being fired during game startup. * Fixed some `LocationEvents`, `PlayerEvents`, and `TimeEvents` being fired during game startup.
* Increased deprecation levels for `SObject`, `LogWriter` (not `Log`), and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`) to _pending removal_. Increased deprecation levels for `Mod.PerSaveConfigFolder`, `Mod.PerSaveConfigPath`, and `Version.VersionString` to _info_. * Increased deprecation levels for `SObject`, `LogWriter` (not `Log`), and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`) to _pending removal_. Increased deprecation levels for `Mod.PerSaveConfigFolder`, `Mod.PerSaveConfigPath`, and `Version.VersionString` to _info_.

View File

@ -1,232 +0,0 @@
&larr; [README](README.md)
This file provides more technical documentation about SMAPI. If you only want to use or create
mods, this section isn't relevant to you; see the main README to use or create mods.
# Contents
* [SMAPI](#smapi)
* [Development](#development)
* [Compiling from source](#compiling-from-source)
* [Debugging a local build](#debugging-a-local-build)
* [Preparing a release](#preparing-a-release)
* [Customisation](#customisation)
* [Configuration file](#configuration-file)
* [Command-line arguments](#command-line-arguments)
* [Compile flags](#compile-flags)
* [SMAPI web services](#smapi-web-services)
* [Overview](#overview)
* [Log parser](#log-parser)
* [Web API](#web-api)
* [Development](#development-2)
* [Local development](#local-development)
* [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk)
* [Mod build config package](#mod-build-config-package)
# SMAPI
## Development
### Compiling from source
Using an official SMAPI release is recommended for most users.
SMAPI uses some C# 7 code, so you'll need at least
[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows,
[MonoDevelop 7.0](https://www.monodevelop.com/) on Linux,
[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent
IDE to compile it. It uses build configuration derived from the
[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#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
the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch
SMAPI with the debugger attached, so you can intercept errors and step through the code being
executed. This doesn't work in MonoDevelop on Linux, unfortunately.
### Preparing a release
To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See
[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms)
on the wiki for the first-time setup.
1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a
[semantic version](https://semver.org). Recommended format:
build type | format | example
:--------- | :----------------------- | :------
dev build | `<version>-alpha.<date>` | `3.0-alpha.20171230`
prerelease | `<version>-beta.<count>` | `3.0-beta.2`
release | `<version>` | `3.0`
2. In Windows:
1. Rebuild the solution in Release mode.
2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to
Linux/Mac.
3. In Linux/Mac:
1. Rebuild the solution in Release mode.
2. Add the `windows-install.*` files to the `bin/SMAPI installer` and
`bin/SMAPI installer for developers` folders.
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
4. Zip the two folders.
## Customisation
### Configuration file
You can customise the SMAPI behaviour by editing the `smapi-internal/StardewModdingAPI.config.json`
file in your game folder.
Basic fields:
field | purpose
----------------- | -------
`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging).
`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background.
`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context.
`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file.
### Command-line arguments
The SMAPI installer recognises three command-line arguments:
argument | purpose
-------- | -------
`--install` | Preselects the install 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.
SMAPI itself recognises two arguments, but these are intended for internal use or testing and may
change without warning.
argument | purpose
-------- | -------
`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.)
`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path.
### Compile flags
SMAPI uses a small number of conditional compilation constants, which you can set by editing the
`<DefineConstants>` element in `StardewModdingAPI.csproj`. Supported constants:
flag | purpose
---- | -------
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
`SMAPI_3_0_STRICT` | Whether to exclude all deprecated APIs from compilation. This is useful for testing mods for SMAPI 3.0 compatibility.
# SMAPI web services
## Overview
The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`.
### Log parser
The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are
persisted in a compressed form to Pastebin.
The log parser lives at https://log.smapi.io.
### Web API
SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a
`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly
accessible but not officially released; it may change at any time.
The API has one `/mods` endpoint. This provides mod info, including official versions and URLs
(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata
from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by
external tools to fetch mod data.
The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and
may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks).
The API will automatically try to fetch known update keys from the wiki and internal data based on
the given ID.
```
POST https://api.smapi.io/v2.0/mods
{
"mods": [
{
"id": "Pathoschild.LookupAnything",
"updateKeys": [ "nexus:541", "chucklefish:4250" ]
}
],
"includeExtendedMetadata": true
}
```
The API will automatically aggregate versions and errors. Each mod will include...
* an `id` (matching what you passed in);
* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g.
optional files on Nexus), and `unofficial` if newer (from the wiki);
* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified
`includeExtendedMetadata: true`);
* and `errors` containing any error messages that occurred while fetching data.
For example:
```
[
{
"id": "Pathoschild.LookupAnything",
"main": {
"version": "1.19",
"url": "https://www.nexusmods.com/stardewvalley/mods/541"
},
"metadata": {
"id": [
"Pathoschild.LookupAnything",
"LookupAnything"
],
"name": "Lookup Anything",
"nexusID": 541,
"gitHubRepo": "Pathoschild/StardewMods",
"compatibilityStatus": "Ok",
"compatibilitySummary": "✓ use latest version."
},
"errors": []
}
]
```
## Development
### Local development
`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within
Visual Studio to run a local version.
There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the
subdomain portion becomes a route (e.g. `log.smapi.io` &rarr; `localhost:59482/log`).
Before running it locally, you need to enter your credentials in the `appsettings.Development.json`
file. See the next section for a description of each setting. This file is listed in `.gitignore`
to prevent accidentally committing credentials.
### Deploying to Amazon Beanstalk
The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the
environment, make sure to specify the following environment properties:
property name | description
------------------------------- | -----------------
`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API.
`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously.
`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`.
`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info.
`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info.
## Mod build config package
### Overview
The mod build config package is a NuGet package that mods reference to automatically set up
references, configure the build, and add analyzers specific to Stardew Valley mods.
This involves three projects:
project | purpose
------------------------------------------------- | ----------------
`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc).
`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio.
`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers.
When the projects are built, the relevant files are copied into `bin/Pathoschild.Stardew.ModBuildConfig`.
### Preparing a build
To prepare a build of the NuGet package:
1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli).
1. Change the version and release notes in `package.nuspec`.
2. Rebuild the solution in _Release_ mode.
3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command:
```bash
nuget.exe pack
```
That will create a `Pathoschild.Stardew.ModBuildConfig-<version>.nupkg` file in the same directory
which can be uploaded to NuGet or referenced directly.

View File

@ -0,0 +1,366 @@
&larr; [SMAPI](../README.md)
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.
## Contents
* [Use](#use)
* [Features](#features)
* [Detect game path](#detect-game-path)
* [Add assembly references](#add-assembly-references)
* [Copy files into the `Mods` folder and create release zip](#copy-files-into-the-mods-folder-and-create-release-zip)
* [Launch or debug game](#launch-or-debug-game)
* [Preconfigure common settings](#preconfigure-common-settings)
* [Add code warnings](#add-code-warnings)
* [Code warnings](#code-warnings)
* [Special cases](#special-cases)
* [Custom game path](#custom-game-path)
* [Non-mod projects](#non-mod-projects)
* [For SMAPI developers](#for-smapi-developers)
* [Release notes](#release-notes)
## Use
1. Create an empty library project.
2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig).
3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod).
4. Compile on any platform.
5. Run the game to play with your mod.
## Features
The package automatically makes the changes listed below. In some cases you can configure how it
works by editing your mod's `.csproj` file, and adding the given properties between the first
`<PropertyGroup>` and `</PropertyGroup>` tags.
### Detect game path
The package 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:
property | description
-------- | -----------
`$(GamePath)` | The absolute path to the detected game folder.
`$(GameExecutableName)` | The game's executable name for the current OS (`Stardew Valley` on Windows, or `StardewValley` on Linux/Mac).
If you get a build error saying it can't find your game, see [_custom game path_](#custom-game-path).
### Add assembly references
The package adds assembly references to SMAPI, Stardew Valley, xTile, and MonoGame (Linux/Mac) or XNA
Framework (Windows). It automatically adjusts depending on which OS you're compiling it on.
The assemblies aren't copied to the build output, since mods loaded by SMAPI won't need them. For
non-mod projects like unit tests, you can set this property:
```xml
<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput>
```
If your mod uses [Harmony](https://github.com/pardeike/Harmony) (not recommended for most mods),
the package can add a reference to SMAPI's Harmony DLL for you:
```xml
<EnableHarmony>true</EnableHarmony>
```
### Copy files into the `Mods` folder and create release zip
<dl>
<dt>Files considered part of your mod</dt>
<dd>
These files are selected by default: `manifest.json`,
[`i18n` files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder
(if any), and all files in the build output. You can select custom files by [adding them to the
build output](https://stackoverflow.com/a/10828462/262123). (If your project references another mod,
make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).)
You can deselect a file by removing it from the build output. For a default file, you can set the
property below to a comma-delimited list of regex patterns to ignore. For crossplatform
compatibility, you should replace path delimiters with `[/\\]`.
```xml
<IgnoreModFilePatterns>\.txt$, \.pdf$, assets[/\\]paths.png</IgnoreModFilePatterns>
```
</dd>
<dt>Copy files into the `Mods` folder</dt>
<dd>
The package copies the selected files into your game's `Mods` folder when you rebuild the code,
with a subfolder matching the mod's project name.
You can change the folder name:
```xml
<ModFolderName>YourModName</ModFolderName>
```
Or disable deploying the files:
```xml
<EnableModDeploy>false</EnableModDeploy>
```
</dd>
<dt>Create release zip</dt>
<dd>
The package adds a zip file in your project's `bin` folder when you rebuild the code, in the format
recommended for sites like Nexus Mods. The zip filename can be changed using `ModFolderName` above.
You can change the folder path where the zip is created:
```xml
<ModZipPath>$(SolutionDir)\_releases</ModZipPath>
```
Or disable zip creation:
```xml
<EnableModZip>false</EnableModZip>
```
</dd>
</dl>
### Launch or debug game
On Windows only, the package configures Visual Studio so you can launch the game and attach a
debugger using _Debug > Start Debugging_ or _Debug > Start Without Debugging_. This lets you [set
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
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.
To disable game debugging (only needed for some non-mod projects):
```xml
<EnableGameDebugging>false</EnableGameDebugging>
```
### Preconfigure common settings
The package also automatically enables PDB files (so error logs show line numbers for simpler
debugging), and enables support for the simplified `.csproj` format.
### Add code warnings
The package runs code analysis on your mod and raises warnings for some common errors or pitfalls.
See [_code warnings_](#code-warnings) for more info.
## Code warnings
### Overview
The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example:
![](screenshots/code-analyzer-example.png)
You can hide the warnings using the warning ID (shown under 'code' in the Error List). See...
* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning);
* for a method using this attribute:
```cs
[System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")]
```
* for an entire project:
1. Expand the _References_ node for the project in Visual Studio.
2. Right-click on _Analyzers_ and choose _Open Active Rule Set_.
4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide.
See below for help with each specific warning.
### Avoid implicit net field cast
Warning text:
> This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but
> {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual
> value instead to avoid bugs.
Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplayer sync. These types
can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`), but their
conversion rules are unintuitive and error-prone. For example,
`item?.category == null && item?.category != null` can both be true at once, and
`building.indoors != null` can be true for a null value.
Suggested fix:
* Some net fields have an equivalent non-net property like `monster.Health` (`int`) instead of
`monster.health` (`NetInt`). The package will add a separate [AvoidNetField](#avoid-net-field) warning for
these. Use the suggested property instead.
* For a reference type (i.e. one that can contain `null`), you can use the `.Value` property:
```c#
if (building.indoors.Value == null)
```
Or convert the value before comparison:
```c#
GameLocation indoors = building.indoors;
if(indoors == null)
// ...
```
* For a value type (i.e. one that can't contain `null`), check if the object is null (if applicable)
and compare with `.Value`:
```cs
if (item != null && item.category.Value == 0)
```
### Avoid net field
Warning text:
> '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead.
Your code accesses a net field, which has some unusual behavior (see [AvoidImplicitNetFieldCast](#avoid-implicit-net-field-cast)).
This field has an equivalent non-net property that avoids those issues.
Suggested fix: access the suggested property name instead.
### Avoid obsolete field
Warning text:
> The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'.
Your code accesses a field which is obsolete or no longer works. Use the suggested field instead.
## Special cases
### Custom game path
The package usually detects where your game is installed automatically. If it can't find your game
or you have multiple installs, you can specify the path yourself. There's two ways to do that:
* **Option 1: global game path (recommended).**
_This will apply to every project that uses the package._
1. Get the full folder path containing the Stardew Valley executable.
2. Create this file:
platform | path
--------- | ----
Linux/Mac | `~/stardewvalley.targets`
Windows | `%USERPROFILE%\stardewvalley.targets`
3. Save the file with this content:
```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
</Project>
```
4. Replace `PATH_HERE` with your game's folder path.
* **Option 2: path in the project file.**
_You'll need to do this for each project that uses the package._
1. Get the folder path containing the Stardew Valley `.exe` file.
2. Add this to your `.csproj` file under the `<Project` line:
```xml
<PropertyGroup>
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
```
3. Replace `PATH_HERE` with your custom game install path.
The configuration will check your custom path first, then fall back to the default paths (so it'll
still compile on a different computer).
You access the game path via `$(GamePath)` in MSBuild properties, if you need to reference another
file in the game folder.
### Non-mod projects
You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable
the mod-related package features:
```xml
<EnableGameDebugging>false</EnableGameDebugging>
<EnableModDeploy>false</EnableModDeploy>
<EnableModZip>false</EnableModZip>
```
If you need to copy the referenced DLLs into your build output, add this too:
```xml
<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput>
```
## For SMAPI developers
The mod build package consists of three projects:
project | purpose
------------------------------------------------- | ----------------
`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc).
`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio.
`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers.
To prepare a build of the NuGet package:
1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli).
1. Change the version and release notes in `package.nuspec`.
2. Rebuild the solution in _Release_ mode.
3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command:
```bash
nuget.exe pack
```
That will create a `Pathoschild.Stardew.ModBuildConfig-<version>.nupkg` file in the same directory
which can be uploaded to NuGet or referenced directly.
## Release notes
### Upcoming release
* Updated for SMAPI 3.0 and Stardew Valley 1.4.
* Added automatic support for `assets` folders.
* Added `$(GameExecutableName)` MSBuild variable.
* Added support for projects using the simplified `.csproj` format.
* Added option to disable game debugging config.
* Added `.pdb` files to builds by default (to enable line numbers in error stack traces).
* Added optional Harmony reference.
* Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly.
* Fixed `<IgnoreModFilePatterns>` not working for `i18n` files.
* Dropped support for older versions of SMAPI and Visual Studio.
### 2.2
* Added support for SMAPI 2.8+ (still compatible with earlier versions).
* Added default game paths for 32-bit Windows.
* Fixed valid manifests marked invalid in some cases.
### 2.1
* Added support for Stardew Valley 1.3.
* Added support for non-mod projects.
* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3.
* Added option to ignore files by regex pattern.
* Added reference to new SMAPI DLL.
* Fixed some game paths not detected by NuGet package.
### 2.0.2
* Fixed compatibility issue on Linux.
### 2.0.1
* Fixed mod deploy failing to create subfolders if they don't already exist.
### 2.0
* Added: mods are now copied into the `Mods` folder automatically (configurable).
* Added: release zips are now created automatically in your build output folder (configurable).
* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI.
* Added mod's version to release zip filename.
* Improved errors to simplify troubleshooting.
* Fixed release zip not having a mod folder.
* Fixed release zip failing if mod name contains characters that aren't valid in a filename.
### 1.7.1
* Fixed issue where i18n folders were flattened.
* The manifest/i18n files in the project now take precedence over those in the build output if both
are present.
### 1.7
* Added option to create release zips on build.
* Added reference to XNA's XACT library for audio-related mods.
### 1.6
* 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.
### 1.5
* Added support for setting a custom game path globally.
* Added default GOG path on Mac.
### 1.4
* Fixed detection of non-default game paths on 32-bit Windows.
* Removed support for SilVerPLuM (discontinued).
* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms
mods automatically).
### 1.3
* Added support for non-default game paths on Windows.
### 1.2
* Exclude game binaries from mod build output.
### 1.1
* Added support for overriding the target platform.
### 1.0
* Initial release.
* Added support for detecting the game path automatically.
* Added support for injecting XNA/MonoGame references automatically based on the OS.
* Added support for mod builders like SilVerPLuM.

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

116
docs/technical/smapi.md Normal file
View File

@ -0,0 +1,116 @@
&larr; [README](../README.md)
This file provides more technical documentation about SMAPI. If you only want to use or create
mods, this section isn't relevant to you; see the main README to use or create mods.
This document is about SMAPI itself; see also [mod build package](mod-package.md) and
[web services](web.md).
# Contents
* [Customisation](#customisation)
* [Configuration file](#configuration-file)
* [Command-line arguments](#command-line-arguments)
* [Compile flags](#compile-flags)
* [For SMAPI developers](#for-smapi-developers)
* [Compiling from source](#compiling-from-source)
* [Debugging a local build](#debugging-a-local-build)
* [Preparing a release](#preparing-a-release)
* [Release notes](#release-notes)
## Customisation
### Configuration file
You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game
folder.
Basic fields:
field | purpose
----------------- | -------
`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging).
`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background.
`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context.
`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file.
### Command-line arguments
The SMAPI installer recognises three command-line arguments:
argument | purpose
-------- | -------
`--install` | Preselects the install 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.
SMAPI itself recognises two arguments **on Windows only**, but these are intended for internal use
or testing and may change without warning. On Linux/Mac, see _environment variables_ below.
argument | purpose
-------- | -------
`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.)
`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path.
### Environment variables
The above SMAPI arguments don't work on Linux/Mac due to the way the game launcher works. You can
set temporary environment variables instead. For example:
> SMAPI_MODS_PATH="Mods (multiplayer)" /path/to/StardewValley
environment variable | purpose
-------------------- | -------
`SMAPI_NO_TERMINAL` | Equivalent to `--no-terminal` above.
`SMAPI_MODS_PATH` | Equivalent to `--mods-path` above.
### Compile flags
SMAPI uses a small number of conditional compilation constants, which you can set by editing the
`<DefineConstants>` element in `SMAPI.csproj`. Supported constants:
flag | purpose
---- | -------
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
## For SMAPI developers
### Compiling from source
Using an official SMAPI release is recommended for most users.
SMAPI often uses the latest C# syntax. You may need the latest version of
[Visual Studio](https://www.visualstudio.com/vs/community/) on Windows,
[MonoDevelop](https://www.monodevelop.com/) on Linux,
[Visual Studio for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent IDE
to compile it. It 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
the `SMAPI` project with debugging from Visual Studio (on Mac or Windows) will launch SMAPI with
the debugger attached, so you can intercept errors and step through the code being executed. This
doesn't work in MonoDevelop on Linux, unfortunately.
### Preparing a release
To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See
[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms)
on the wiki for the first-time setup.
1. Update the version number in `.root/build/common.targets` and `Constants::Version`. Make sure
you use a [semantic version](https://semver.org). Recommended format:
build type | format | example
:--------- | :----------------------- | :------
dev build | `<version>-alpha.<date>` | `3.0-alpha.20171230`
prerelease | `<version>-beta.<count>` | `3.0-beta.2`
release | `<version>` | `3.0`
2. In Windows:
1. Rebuild the solution in Release mode.
2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to
Linux/Mac.
3. In Linux/Mac:
1. Rebuild the solution in Release mode.
2. Add the `windows-install.*` files to the `bin/SMAPI installer` and
`bin/SMAPI installer for developers` folders.
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
4. Zip the two folders.
## Release notes
See [release notes](../release-notes.md).

383
docs/technical/web.md Normal file
View File

@ -0,0 +1,383 @@
&larr; [README](../README.md)
**SMAPI.Web** contains the code for the `smapi.io` website, including the mod compatibility list
and update check API.
## Contents
* [Log parser](#log-parser)
* [JSON validator](#json-validator)
* [Web API](#web-api)
* [Short URLs](#short-urls)
* [For SMAPI developers](#for-smapi-developers)
* [Local development](#local-development)
* [Production environment](#production-environment)
## Log parser
The log parser at https://smapi.io/log provides a web UI for uploading, parsing, and sharing SMAPI
logs.
The logs are saved in a compressed form to Amazon Blob storage for 30 days.
## JSON validator
### Overview
The JSON validator at https://smapi.io/json provides a web UI for uploading and sharing JSON files,
and validating them as plain JSON or against a predefined format like `manifest.json` or Content
Patcher's `content.json`.
The logs are saved in a compressed form to Amazon Blob storage for 30 days.
### Schema file format
Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/)
format. The JSON validator UI recognises a superset of the standard fields to change output:
<dl>
<dt>Documentation URL</dt>
<dd>
The root schema may have a `@documentationURL` field, which is a web URL for the user
documentation:
```js
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest"
```
If present, this is shown in the JSON validator UI.
</dd>
<dt>Error messages</dt>
<dd>
Any part of the schema can define an `@errorMessages` field, which overrides matching schema
errors. You can override by error code (recommended), or by error type and a regex pattern matched
against the error message (more fragile):
```js
// by error type
"pattern": "^[a-zA-Z0-9_.-]+\\.dll$",
"@errorMessages": {
"pattern": "Invalid value; must be a filename ending with .dll."
}
```
```js
// by error type + message pattern
"@errorMessages": {
"oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.",
"oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive."
}
```
Error messages may contain special tokens:
* The `@value` token is replaced with the error's value field. This is usually (but not always) the
original field value.
* When an error has child errors, by default they're flattened into one message:
```
line | field | error
---- | ---------- | -------------------------------------------------------------------------
4 | Changes[0] | JSON does not match schema from 'then'.
| | ==> Changes[0].ToArea.Y: Invalid type. Expected Integer but got String.
| | ==> Changes[0].ToArea: Missing required fields: Height.
```
If you set the message for an error to `$transparent`, the parent error is omitted entirely and
the child errors are shown instead:
```
line | field | error
---- | ------------------- | ----------------------------------------------
8 | Changes[0].ToArea.Y | Invalid type. Expected Integer but got String.
8 | Changes[0].ToArea | Missing required fields: Height.
```
The child errors themselves may be marked `$transparent`, etc. If an error has no child errors,
this override is ignored.
Validation errors for `then` blocks are transparent by default, unless overridden.
</dd>
</dl>
### Using a schema file directly
You can reference the validator schemas in your JSON file directly using the `$schema` field, for
text editors that support schema validation. For example:
```js
{
"$schema": "https://smapi.io/schemas/manifest.json",
"Name": "Some mod",
...
}
```
Available schemas:
format | schema URL
------ | ----------
[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json
[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json
## Web API
### Overview
SMAPI provides a web API at `smapi.io/api` for use by SMAPI and external tools. The URL includes a
`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly
accessible but not officially released; it may change at any time.
### `/mods` endpoint
The API has one `/mods` endpoint. This crossreferences the mod against a variety of sources (e.g.
the wiki, Chucklefish, CurseForge, ModDrop, and Nexus) to provide metadata mainly intended for
update checks.
The API accepts a `POST` request with these fields:
<table>
<tr>
<th>field</th>
<th>summary</th>
</tr>
<tr>
<td><code>mods</code></td>
<td>
The mods for which to fetch metadata. Included fields:
field | summary
----- | -------
`id` | The unique ID in the mod's `manifest.json`. This is used to crossreference with the wiki, and to index mods in the response. If it's unknown (e.g. you just have an update key), you can use a unique fake ID like `FAKE.Nexus.2400`.
`updateKeys` | _(optional)_ [Update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks) which specify the mod pages to check, in addition to any mod pages linked to the `ID`.
`installedVersion` | _(optional)_ The installed version of the mod. If not specified, the API won't recommend an update.
`isBroken` | _(optional)_ Whether SMAPI failed to load the installed version of the mod, e.g. due to incompatibility. If true, the web API will be more permissive when recommending updates (e.g. allowing a stable → prerelease update).
</td>
</tr>
<tr>
<td><code>apiVersion</code></td>
<td>
_(optional)_ The installed version of SMAPI. If not specified, the API won't recommend an update.
</td>
</tr>
<tr>
<td><code>gameVersion</code></td>
<td>
_(optional)_ The installed version of Stardew Valley. This may be used to select updates.
</td>
</tr>
<tr>
<td><code>platform</code></td>
<td>
_(optional)_ The player's OS (`Android`, `Linux`, `Mac`, or `Windows`). This may be used to select updates.
</td>
</tr>
<tr>
<td><code>includeExtendedMetadata</code></td>
<td>
_(optional)_ Whether to include extra metadata that's not needed for SMAPI update checks, but which
may be useful to external tools.
</td>
</table>
Example request:
```js
POST https://smapi.io/api/v3.0/mods
{
"mods": [
{
"id": "Pathoschild.ContentPatcher",
"updateKeys": [ "nexus:1915" ],
"installedVersion": "1.9.2",
"isBroken": false
}
],
"apiVersion": "3.0.0",
"gameVersion": "1.4.0",
"platform": "Windows",
"includeExtendedMetadata": true
}
```
Response fields:
<table>
<tr>
<th>field</th>
<th>summary</th>
</tr>
<tr>
<td><code>id</code></td>
<td>
The mod ID you specified in the request.
</td>
</tr>
<tr>
<td><code>suggestedUpdate</code></td>
<td>
The update version recommended by the web API, if any. This is based on some internal rules (e.g.
it won't recommend a prerelease update if the player has a working stable version) and context
(e.g. whether the player is in the game beta channel). Choosing an update version yourself isn't
recommended, but you can set `includeExtendedMetadata: true` and check the `metadata` field if you
really want to do that.
</td>
</tr>
<tr>
<td><code>errors</code></td>
<td>
Human-readable errors that occurred fetching the version info (e.g. if a mod page has an invalid
version).
</td>
</tr>
<tr>
<td><code>metadata</code></td>
<td>
Extra metadata that's not needed for SMAPI update checks but which may be useful to external tools,
if you set `includeExtendedMetadata: true` in the request. Included fields:
field | summary
----- | -------
`id` | The known `manifest.json` unique IDs for this mod defined on the wiki, if any. That includes historical versions of the mod.
`name` | The normalised name for this mod based on the crossreferenced sites.
`nexusID` | The mod ID on [Nexus Mods](https://www.nexusmods.com/stardewvalley/), if any.
`chucklefishID` | The mod ID in the [Chucklefish mod repo](https://community.playstarbound.com/resources/categories/stardew-valley.22/), if any.
`curseForgeID` | The mod project ID on [CurseForge](https://www.curseforge.com/stardewvalley), if any.
`curseForgeKey` | The mod key on [CurseForge](https://www.curseforge.com/stardewvalley), if any. This is used in the mod page URL.
`modDropID` | The mod ID on [ModDrop](https://www.moddrop.com/stardew-valley), if any.
`gitHubRepo` | The GitHub repository containing the mod code, if any. Specified in the `Owner/Repo` form.
`customSourceUrl` | The custom URL to the mod code, if any. This is used for mods which aren't stored in a GitHub repo.
`customUrl` | The custom URL to the mod page, if any. This is used for mods which aren't stored on one of the standard mod sites covered by the ID fields.
`main` | The primary mod version, if any. This depends on the mod site, but it's typically either the version of the mod itself or of its latest non-optional download.
`optional` | The latest optional download version, if any.
`unofficial` | The version of the unofficial update defined on the wiki for this mod, if any.
`unofficialForBeta` | Equivalent to `unofficial`, but for beta versions of SMAPI or Stardew Valley.
`hasBetaInfo` | Whether there's an ongoing Stardew Valley or SMAPI beta which may affect update checks.
`compatibilityStatus` | The compatibility status for the mod for the stable version of the game, as defined on the wiki, if any. See [possible values](https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs).
`compatibilitySummary` | The human-readable summary of the mod's compatibility in HTML format, if any.
`brokeIn` | The SMAPI or Stardew Valley version that broke this mod, if any.
`betaCompatibilityStatus`<br />`betaCompatibilitySummary`<br />`betaBrokeIn` | Equivalent to the preceding fields, but for beta versions of SMAPI or Stardew Valley.
</td>
</tr>
</table>
Example response with `includeExtendedMetadata: false`:
```js
[
{
"id": "Pathoschild.ContentPatcher",
"suggestedUpdate": {
"version": "1.10.0",
"url": "https://www.nexusmods.com/stardewvalley/mods/1915"
},
"errors": []
}
]
```
Example response with `includeExtendedMetadata: true`:
```js
[
{
"id": "Pathoschild.ContentPatcher",
"suggestedUpdate": {
"version": "1.10.0",
"url": "https://www.nexusmods.com/stardewvalley/mods/1915"
},
"metadata": {
"id": [ "Pathoschild.ContentPatcher" ],
"name": "Content Patcher",
"nexusID": 1915,
"curseForgeID": 309243,
"curseForgeKey": "content-patcher",
"modDropID": 470174,
"gitHubRepo": "Pathoschild/StardewMods",
"main": {
"version": "1.10",
"url": "https://www.nexusmods.com/stardewvalley/mods/1915"
},
"hasBetaInfo": true,
"compatibilityStatus": "Ok",
"compatibilitySummary": "✓ use latest version."
},
"errors": []
}
]
```
## Short URLs
The SMAPI web services provides a few short URLs for convenience:
short url | → | target page
:-------- | - | :----------
[smapi.io/3.0](https://smapi.io/3.0) | → | [stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0)
[smapi.io/community](https://smapi.io/community) | → | [stardewvalleywiki.com/Modding:Community](https://stardewvalleywiki.com/Modding:Community)
[smapi.io/docs](https://smapi.io/docs) | → | [stardewvalleywiki.com/Modding:Index](https://stardewvalleywiki.com/Modding:Index)
[smapi.io/package](https://smapi.io/package) | → | [github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md](https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md)
[smapi.io/troubleshoot](https://smapi.io/troubleshoot) | → | [stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting](https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting)
[smapi.io/xnb](https://smapi.io/xnb) | → | [stardewvalleywiki.com/Modding:Using_XNB_mods](https://stardewvalleywiki.com/Modding:Using_XNB_mods)
## For SMAPI developers
### Local environment
A local environment lets you run a complete copy of the web project (including cache database) on
your machine, with no external dependencies aside from the actual mod sites.
1. Enter the Nexus credentials in `appsettings.Development.json` . You can leave the other
credentials empty to default to fetching data anonymously, and storing data in-memory and
on disk.
2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site.
### Production environment
A production environment includes the web servers and cache database hosted online for public
access.
This section assumes you're creating a new environment on Azure, but the app isn't tied to any
Azure services. If you want to host it on a different site, you'll need to adjust the instructions
accordingly.
Initial setup:
1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas))
for mod data.
2. Create an Azure Blob storage account for uploaded files.
3. Create an Azure App Services environment running the latest .NET Core on Linux or Windows.
4. Add these application settings in the new App Services environment:
property name | description
------------------------------- | -----------------
`ApiClients.AzureBlobConnectionString` | The connection string for the Azure Blob storage account created in step 2.
`ApiClients.GitHubUsername`<br />`ApiClients.GitHubPassword` | The login credentials for the GitHub account with which to fetch release info. If these are omitted, GitHub will impose much stricter rate limits.
`ApiClients:NexusApiKey` | The [Nexus API authentication key](https://github.com/Pathoschild/FluentNexus#init-a-client).
`MongoDB:ConnectionString` | The connection string for the MongoDB instance.
`MongoDB:Database` | The MongoDB database name (e.g. `smapi` in production or `smapi-edge` in testing environments).
Optional settings:
property name | description
------------------------------- | -----------------
`BackgroundServices:Enabled` | Set to `true` to enable background processes like fetching data from the wiki, or false to disable them.
`Site:BetaEnabled` | Set to `true` to show a separate download button if there's a beta version of SMAPI in its GitHub releases.
`Site:BetaBlurb` | If `Site:BetaEnabled` is true and there's a beta version of SMAPI in its GitHub releases, this is shown on the beta download button as explanatory subtext.
`Site:SupporterList` | A list of Patreon supports to credit on the download page.
To deploy updates:
1. [Deploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure).
2. If the MongoDB schema changed, delete the MongoDB database. (It'll be recreated automatically.)

View File

@ -59,7 +59,7 @@ namespace StardewModdingAPI.Installer.Framework
this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley"); this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley");
this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI"); this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI");
this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original"); this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original");
this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "StardewModdingAPI.config.json"); this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.json");
} }
} }
} }

View File

@ -7,7 +7,6 @@ using System.Threading;
using Microsoft.Win32; using Microsoft.Win32;
using StardewModdingApi.Installer.Enums; using StardewModdingApi.Installer.Enums;
using StardewModdingAPI.Installer.Framework; using StardewModdingAPI.Installer.Framework;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Internal.ConsoleWriting;
using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.ModScanning;
@ -37,64 +36,7 @@ namespace StardewModdingApi.Installer
"SMAPI.ConsoleCommands" "SMAPI.ConsoleCommands"
}; };
/// <summary>The default file paths where Stardew Valley can be installed.</summary>
/// <param name="platform">The target platform.</param>
/// <remarks>Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. </remarks>
private IEnumerable<string> GetDefaultInstallPaths(Platform platform)
{
switch (platform)
{
case Platform.Linux:
case Platform.Mac:
{
string home = Environment.GetEnvironmentVariable("HOME");
// Linux
yield return $"{home}/GOG Games/Stardew Valley/game";
yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley")
? $"{home}/.steam/steam/steamapps/common/Stardew Valley"
: $"{home}/.local/share/Steam/steamapps/common/Stardew Valley";
// Mac
yield return "/Applications/Stardew Valley.app/Contents/MacOS";
yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS";
}
break;
case Platform.Windows:
{
// Windows
foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
{
yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley";
yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
}
// Windows registry
IDictionary<string, string> registryKeys = new Dictionary<string, string>
{
[@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam
[@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows
};
foreach (var pair in registryKeys)
{
string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value);
if (!string.IsNullOrWhiteSpace(path))
yield return path;
}
// via Steam library path
string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
if (steampath != null)
yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
}
break;
default:
throw new InvalidOperationException($"Unknown platform '{platform}'.");
}
}
/// <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>
@ -112,6 +54,7 @@ namespace StardewModdingApi.Installer
yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only
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");
// obsolete // obsolete
yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
@ -133,11 +76,9 @@ 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("StardewModdingAPI.xml"); // moved in 2.8
yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8
yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8 yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8
yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8 yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8
yield return GetInstallPath("steam_appid.txt"); // moved in 2.8
if (modsDir.Exists) if (modsDir.Exists)
{ {
@ -159,13 +100,13 @@ namespace StardewModdingApi.Installer
public InteractiveInstaller(string bundlePath) public InteractiveInstaller(string bundlePath)
{ {
this.BundlePath = bundlePath; this.BundlePath = bundlePath;
this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.AutoDetect); this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform());
} }
/// <summary>Run the install or uninstall script.</summary> /// <summary>Run the install or uninstall script.</summary>
/// <param name="args">The command line arguments.</param> /// <param name="args">The command line arguments.</param>
/// <remarks> /// <remarks>
/// Initialisation flow: /// Initialization flow:
/// 1. Collect information (mainly OS and install path) and validate it. /// 1. Collect information (mainly OS and install path) and validate it.
/// 2. Ask the user whether to install or uninstall. /// 2. Ask the user whether to install or uninstall.
/// ///
@ -187,8 +128,9 @@ namespace StardewModdingApi.Installer
** Step 1: initial setup ** Step 1: initial setup
*********/ *********/
/**** /****
** Get platform & set window title ** Get basic info & set window title
****/ ****/
ModToolkit toolkit = new ModToolkit();
Platform platform = EnvironmentUtility.DetectPlatform(); Platform platform = EnvironmentUtility.DetectPlatform();
Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}";
Console.WriteLine(); Console.WriteLine();
@ -275,8 +217,8 @@ namespace StardewModdingApi.Installer
** show theme selector ** show theme selector
****/ ****/
// get theme writers // get theme writers
var lightBackgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.LightBackground); var lightBackgroundWriter = new ColorfulConsoleWriter(platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground));
var darkDarkgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.DarkBackground); var darkBackgroundWriter = new ColorfulConsoleWriter(platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground));
// print question // print question
this.PrintPlain("Which text looks more readable?"); this.PrintPlain("Which text looks more readable?");
@ -284,7 +226,7 @@ namespace StardewModdingApi.Installer
Console.Write(" [1] "); Console.Write(" [1] ");
lightBackgroundWriter.WriteLine("Dark text on light background", ConsoleLogLevel.Info); lightBackgroundWriter.WriteLine("Dark text on light background", ConsoleLogLevel.Info);
Console.Write(" [2] "); Console.Write(" [2] ");
darkDarkgroundWriter.WriteLine("Light text on dark background", ConsoleLogLevel.Info); darkBackgroundWriter.WriteLine("Light text on dark background", ConsoleLogLevel.Info);
Console.WriteLine(); Console.WriteLine();
// handle choice // handle choice
@ -297,7 +239,7 @@ namespace StardewModdingApi.Installer
break; break;
case "2": case "2":
scheme = MonitorColorScheme.DarkBackground; scheme = MonitorColorScheme.DarkBackground;
this.ConsoleWriter = darkDarkgroundWriter; this.ConsoleWriter = darkBackgroundWriter;
break; break;
default: default:
throw new InvalidOperationException($"Unexpected action key '{choice}'."); throw new InvalidOperationException($"Unexpected action key '{choice}'.");
@ -324,7 +266,7 @@ namespace StardewModdingApi.Installer
****/ ****/
// get game path // get game path
this.PrintInfo("Where is your game folder?"); this.PrintInfo("Where is your game folder?");
DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, 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.");
@ -490,7 +432,6 @@ namespace StardewModdingApi.Installer
{ {
this.PrintDebug("Adding bundled mods..."); this.PrintDebug("Adding bundled mods...");
ModToolkit toolkit = new ModToolkit();
ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray(); ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray();
foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName)) foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName))
{ {
@ -529,7 +470,7 @@ namespace StardewModdingApi.Installer
{ {
string text = File string text = File
.ReadAllText(paths.ApiConfigPath) .ReadAllText(paths.ApiConfigPath)
.Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}"""); .Replace(@"""UseScheme"": ""AutoDetect""", $@"""UseScheme"": ""{scheme}""");
File.WriteAllText(paths.ApiConfigPath, text); File.WriteAllText(paths.ApiConfigPath, text);
} }
@ -598,32 +539,6 @@ namespace StardewModdingApi.Installer
} }
} }
/// <summary>Get the value of a key in the Windows HKLM registry.</summary>
/// <param name="key">The full path of the registry key relative to HKLM.</param>
/// <param name="name">The name of the value.</param>
private string GetLocalMachineRegistryValue(string key, string name)
{
RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine;
RegistryKey openKey = localMachine.OpenSubKey(key);
if (openKey == null)
return null;
using (openKey)
return (string)openKey.GetValue(name);
}
/// <summary>Get the value of a key in the Windows HKCU registry.</summary>
/// <param name="key">The full path of the registry key relative to HKCU.</param>
/// <param name="name">The name of the value.</param>
private string GetCurrentUserRegistryValue(string key, string name)
{
RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser;
RegistryKey openKey = currentuser.OpenSubKey(key);
if (openKey == null)
return null;
using (openKey)
return (string)openKey.GetValue(name);
}
/// <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);
@ -731,7 +646,7 @@ namespace StardewModdingApi.Installer
/// <summary>Delete a file or folder regardless of file permissions, and block until deletion completes.</summary> /// <summary>Delete a file or folder regardless of file permissions, and block until deletion completes.</summary>
/// <param name="entry">The file or folder to reset.</param> /// <param name="entry">The file or folder to reset.</param>
/// <remarks>This method is mirred from <c>FileUtilities.ForceDelete</c> in the toolkit.</remarks> /// <remarks>This method is mirrored from <c>FileUtilities.ForceDelete</c> in the toolkit.</remarks>
private void ForceDelete(FileSystemInfo entry) private void ForceDelete(FileSystemInfo entry)
{ {
// ignore if already deleted // ignore if already deleted
@ -789,8 +704,9 @@ 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="platform">The current platform.</param>
/// <param name="toolkit">The mod toolkit.</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, string specifiedPath) private DirectoryInfo InteractivelyGetInstallPath(Platform platform, ModToolkit toolkit, string specifiedPath)
{ {
// get executable name // get executable name
string executableFilename = EnvironmentUtility.GetExecutableName(platform); string executableFilename = EnvironmentUtility.GetExecutableName(platform);
@ -813,18 +729,7 @@ namespace StardewModdingApi.Installer
} }
// get installed paths // get installed paths
DirectoryInfo[] defaultPaths = DirectoryInfo[] defaultPaths = toolkit.GetGameFolders().ToArray();
(
from path in this.GetDefaultInstallPaths(platform).Distinct(StringComparer.InvariantCultureIgnoreCase)
let dir = new DirectoryInfo(path)
where dir.Exists && dir.EnumerateFiles(executableFilename).Any()
select dir
)
.GroupBy(p => p.FullName, StringComparer.InvariantCultureIgnoreCase) // ignore duplicate paths
.Select(p => p.First())
.ToArray();
// choose where to install
if (defaultPaths.Any()) if (defaultPaths.Any())
{ {
// only one path // only one path
@ -857,7 +762,7 @@ namespace StardewModdingApi.Installer
continue; continue;
} }
// normalise path // normalize path
if (platform == Platform.Windows) if (platform == Platform.Windows)
path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path 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) if (platform == Platform.Linux || platform == Platform.Mac)

View File

@ -36,7 +36,7 @@ namespace StardewModdingApi.Installer
FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-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 redownloading 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})");
Console.ReadLine(); Console.ReadLine();
return; return;
} }

View File

@ -1,4 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("SMAPI.Installer")]
[assembly: AssemblyDescription("The SMAPI installer for players.")]

View File

@ -40,5 +40,5 @@ When installing on Linux or Mac:
- Make sure Mono is installed (normally the installer checks for you). While it's not required, - 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 many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or
freeze the game.) freeze the game.)
- To configure the color scheme, edit the `smapi-internal/StardewModdingAPI.config.json` file and - To configure the color scheme, edit the `smapi-internal/config.json` file and see instructions
see instructions there for the 'ColorScheme' setting. there for the 'ColorScheme' setting.

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>SMAPI.Installer</AssemblyName>
<RootNamespace>StardewModdingAPI.Installer</RootNamespace> <RootNamespace>StardewModdingAPI.Installer</RootNamespace>
<AssemblyName>StardewModdingAPI.Installer</AssemblyName> <Description>The SMAPI installer for players.</Description>
<TargetFramework>net45</TargetFramework> <TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PlatformTarget>x86</PlatformTarget> <PlatformTarget>x86</PlatformTarget>
@ -13,11 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" /> <ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -12,6 +12,9 @@ elif type type >/dev/null 2>&1; then
COMMAND="type" COMMAND="type"
fi 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 # validate Mono & run installer
if $COMMAND mono >/dev/null 2>&1; then if $COMMAND mono >/dev/null 2>&1; then
mono internal/unix-install.exe mono internal/unix-install.exe

View File

@ -1,14 +1,14 @@
#!/bin/bash #!/usr/bin/env bash
# MonoKickstart Shell Script # MonoKickstart Shell Script
# Written by Ethan "flibitijibibo" Lee # Written by Ethan "flibitijibibo" Lee
# Modified for StardewModdingAPI by Viz and Pathoschild # Modified for SMAPI by various contributors
# Move to script's directory # Move to script's directory
cd "`dirname "$0"`" cd "$(dirname "$0")" || exit $?
# Get the system architecture # Get the system architecture
UNAME=`uname` UNAME=$(uname)
ARCH=`uname -m` ARCH=$(uname -m)
# MonoKickstart picks the right libfolder, so just execute the right binary. # MonoKickstart picks the right libfolder, so just execute the right binary.
if [ "$UNAME" == "Darwin" ]; then if [ "$UNAME" == "Darwin" ]; then
@ -39,18 +39,18 @@ if [ "$UNAME" == "Darwin" ]; then
# launch SMAPI # launch SMAPI
cp StardewValley.bin.osx StardewModdingAPI.bin.osx cp StardewValley.bin.osx StardewModdingAPI.bin.osx
open -a Terminal ./StardewModdingAPI.bin.osx $@ open -a Terminal ./StardewModdingAPI.bin.osx "$@"
else else
# choose launcher # choose launcher
LAUNCHER="" LAUNCHER=""
if [ "$ARCH" == "x86_64" ]; then if [ "$ARCH" == "x86_64" ]; then
ln -sf mcs.bin.x86_64 mcs ln -sf mcs.bin.x86_64 mcs
cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64
LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" LAUNCHER="./StardewModdingAPI.bin.x86_64 $*"
else else
ln -sf mcs.bin.x86 mcs ln -sf mcs.bin.x86 mcs
cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 cp StardewValley.bin.x86 StardewModdingAPI.bin.x86
LAUNCHER="./StardewModdingAPI.bin.x86 $@" LAUNCHER="./StardewModdingAPI.bin.x86 $*"
fi fi
# get cross-distro version of POSIX command # get cross-distro version of POSIX command
@ -61,37 +61,65 @@ else
COMMAND="type" COMMAND="type"
fi fi
# open SMAPI in terminal # select terminal (prefer xterm for best compatibility, then known supported terminals)
if $COMMAND xterm 2>/dev/null; then for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do
xterm -e "$LAUNCHER" if $COMMAND "$terminal" 2>/dev/null; then
elif $COMMAND x-terminal-emulator 2>/dev/null; then # Find the true shell behind x-terminal-emulator
# Terminator converts -e to -x when used through x-terminal-emulator for some reason (per if [ "$(basename "$(readlink -f $(which "$terminal"))")" != "x-terminal-emulator" ]; then
# `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator export LAUNCHTERM=$terminal
# is mapped to Terminator, invoke it directly instead. break;
if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then else
terminator -e "sh -c 'TERM=xterm $LAUNCHER'" export LAUNCHTERM="$(basename "$(readlink -f $(which x-terminal-emulator))")"
else # Remember that we're using x-terminal-emulator just in case it points outside the $PATH
x-terminal-emulator -e "sh -c 'TERM=xterm $LAUNCHER'" export XTE=1
break;
fi
fi fi
elif $COMMAND xfce4-terminal 2>/dev/null; then done
xfce4-terminal -e "sh -c 'TERM=xterm $LAUNCHER'"
elif $COMMAND gnome-terminal 2>/dev/null; then # if no terminal was found, run in current shell or with no output
gnome-terminal -e "sh -c 'TERM=xterm $LAUNCHER'" if [ -z "$LAUNCHTERM" ]; then
elif $COMMAND konsole 2>/dev/null; then
konsole -p Environment=TERM=xterm -e "$LAUNCHER"
elif $COMMAND terminal 2>/dev/null; then
terminal -e "sh -c 'TERM=xterm $LAUNCHER'"
elif $COMMAND termite 2>/dev/null; then
termite -e "sh -c 'TERM=xterm $LAUNCHER'"
else
sh -c 'TERM=xterm $LAUNCHER' sh -c 'TERM=xterm $LAUNCHER'
if [ $? -eq 127 ]; then
$LAUNCHER --no-terminal
fi
exit
fi fi
# some Linux users get error 127 (command not found) from the above block, even though # run in selected terminal and account for quirks
# `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when case $LAUNCHTERM in
# that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal terminator)
# (which can be slow if there is none). # Terminator converts -e to -x when used through x-terminal-emulator for some reason
if [ $? -eq 127 ]; then if $XTE; then
$LAUNCHER --no-terminal terminator -e "sh -c 'TERM=xterm $LAUNCHER'"
fi else
terminator -x "sh -c 'TERM=xterm $LAUNCHER'"
fi
;;
kitty)
# Kitty overrides the TERM varible unless you set it explicitly
kitty -o term=xterm $LAUNCHER
;;
alacritty)
# Alacritty doesn't like the double quotes or the variable
if [ "$ARCH" == "x86_64" ]; then
alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86_64 $*'
else
alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86 $*'
fi
;;
xterm|xfce4-terminal|gnome-terminal|terminal|termite|mate-terminal)
$LAUNCHTERM -e "sh -c 'TERM=xterm $LAUNCHER'"
;;
konsole)
konsole -p Environment=TERM=xterm -e "$LAUNCHER"
;;
*)
# If we don't know the terminal, just try to run it in the current shell.
sh -c 'TERM=xterm $LAUNCHER'
# if THAT fails, launch with no output
if [ $? -eq 127 ]; then
$LAUNCHER --no-terminal
fi
esac
fi fi

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Internal.ConsoleWriting
{
/// <summary>The console color scheme options.</summary>
internal class ColorSchemeConfig
{
/// <summary>The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</summary>
public MonitorColorScheme UseScheme { get; set; }
/// <summary>The available console color schemes.</summary>
public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; set; }
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Internal.ConsoleWriting namespace StardewModdingAPI.Internal.ConsoleWriting
{ {
@ -21,11 +22,16 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="platform">The target platform.</param> /// <param name="platform">The target platform.</param>
/// <param name="colorScheme">The console color scheme to use.</param> public ColorfulConsoleWriter(Platform platform)
public ColorfulConsoleWriter(Platform platform, MonitorColorScheme colorScheme) : this(platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.AutoDetect)) { }
/// <summary>Construct an instance.</summary>
/// <param name="platform">The target platform.</param>
/// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
public ColorfulConsoleWriter(Platform platform, ColorSchemeConfig colorConfig)
{ {
this.SupportsColor = this.TestColorSupport(); this.SupportsColor = this.TestColorSupport();
this.Colors = this.GetConsoleColorScheme(platform, colorScheme); this.Colors = this.GetConsoleColorScheme(platform, colorConfig);
} }
/// <summary>Write a message line to the log.</summary> /// <summary>Write a message line to the log.</summary>
@ -53,6 +59,40 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
Console.WriteLine(message); Console.WriteLine(message);
} }
/// <summary>Get the default color scheme config for cases where it's not configurable (e.g. the installer).</summary>
/// <param name="useScheme">The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</param>
/// <remarks>The colors here should be kept in sync with the SMAPI config file.</remarks>
public static ColorSchemeConfig GetDefaultColorSchemeConfig(MonitorColorScheme useScheme)
{
return new ColorSchemeConfig
{
UseScheme = useScheme,
Schemes = new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
{
[MonitorColorScheme.DarkBackground] = new Dictionary<ConsoleLogLevel, ConsoleColor>
{
[ConsoleLogLevel.Trace] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Debug] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Info] = ConsoleColor.White,
[ConsoleLogLevel.Warn] = ConsoleColor.Yellow,
[ConsoleLogLevel.Error] = ConsoleColor.Red,
[ConsoleLogLevel.Alert] = ConsoleColor.Magenta,
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
},
[MonitorColorScheme.LightBackground] = new Dictionary<ConsoleLogLevel, ConsoleColor>
{
[ConsoleLogLevel.Trace] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Debug] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Info] = ConsoleColor.Black,
[ConsoleLogLevel.Warn] = ConsoleColor.DarkYellow,
[ConsoleLogLevel.Error] = ConsoleColor.Red,
[ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta,
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
}
}
};
}
/********* /*********
** Private methods ** Private methods
@ -73,47 +113,22 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
/// <summary>Get the color scheme to use for the current console.</summary> /// <summary>Get the color scheme to use for the current console.</summary>
/// <param name="platform">The target platform.</param> /// <param name="platform">The target platform.</param>
/// <param name="colorScheme">The console color scheme to use.</param> /// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
private IDictionary<ConsoleLogLevel, ConsoleColor> GetConsoleColorScheme(Platform platform, MonitorColorScheme colorScheme) private IDictionary<ConsoleLogLevel, ConsoleColor> GetConsoleColorScheme(Platform platform, ColorSchemeConfig colorConfig)
{ {
// auto detect color scheme // get color scheme ID
if (colorScheme == MonitorColorScheme.AutoDetect) MonitorColorScheme schemeID = colorConfig.UseScheme;
if (schemeID == MonitorColorScheme.AutoDetect)
{ {
colorScheme = 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
switch (colorScheme) return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor> scheme)
{ ? scheme
case MonitorColorScheme.DarkBackground: : throw new NotSupportedException($"Unknown color scheme '{schemeID}'.");
return new Dictionary<ConsoleLogLevel, ConsoleColor>
{
[ConsoleLogLevel.Trace] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Debug] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Info] = ConsoleColor.White,
[ConsoleLogLevel.Warn] = ConsoleColor.Yellow,
[ConsoleLogLevel.Error] = ConsoleColor.Red,
[ConsoleLogLevel.Alert] = ConsoleColor.Magenta,
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
};
case MonitorColorScheme.LightBackground:
return new Dictionary<ConsoleLogLevel, ConsoleColor>
{
[ConsoleLogLevel.Trace] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Debug] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Info] = ConsoleColor.Black,
[ConsoleLogLevel.Warn] = ConsoleColor.DarkYellow,
[ConsoleLogLevel.Error] = ConsoleColor.Red,
[ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta,
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
};
default:
throw new NotSupportedException($"Unknown color scheme '{colorScheme}'.");
}
} }
/// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary> /// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary>
@ -125,7 +140,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
case ConsoleColor.Black: case ConsoleColor.Black:
case ConsoleColor.Blue: case ConsoleColor.Blue:
case ConsoleColor.DarkBlue: case ConsoleColor.DarkBlue:
case ConsoleColor.DarkMagenta: // Powershell case ConsoleColor.DarkMagenta: // PowerShell
case ConsoleColor.DarkRed: case ConsoleColor.DarkRed:
case ConsoleColor.Red: case ConsoleColor.Red:
return true; return true;

View File

@ -10,9 +10,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorfulConsoleWriter.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorfulConsoleWriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EnvironmentUtility.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorSchemeConfig.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\LogLevel.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ConsoleLogLevel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Platform.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -2,7 +2,7 @@
namespace Netcode namespace Netcode
{ {
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetFieldBase</c> for unit testing.</summary> /// <summary>A simplified version of Stardew Valley's <c>Netcode.NetFieldBase</c> for unit testing.</summary>
/// <typeparam name="T">The type of the synchronised value.</typeparam> /// <typeparam name="T">The type of the synchronized value.</typeparam>
/// <typeparam name="TSelf">The type of the current instance.</typeparam> /// <typeparam name="TSelf">The type of the current instance.</typeparam>
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf> public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
{ {

View File

@ -96,7 +96,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
DiagnosticResult expected = new DiagnosticResult DiagnosticResult expected = new DiagnosticResult
{ {
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/buildmsg/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.",
Severity = DiagnosticSeverity.Warning, Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) } Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) }
}; };
@ -138,7 +138,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
DiagnosticResult expected = new DiagnosticResult DiagnosticResult expected = new DiagnosticResult
{ {
Id = "AvoidNetField", Id = "AvoidNetField",
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/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.",
Severity = DiagnosticSeverity.Warning, Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) } Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) }
}; };

View File

@ -67,7 +67,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
DiagnosticResult expected = new DiagnosticResult DiagnosticResult expected = new DiagnosticResult
{ {
Id = "AvoidObsoleteField", Id = "AvoidObsoleteField",
Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/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.",
Severity = DiagnosticSeverity.Warning, Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", ObsoleteFieldAnalyzerTests.SampleCodeLine, ObsoleteFieldAnalyzerTests.SampleCodeColumn + column) } Locations = new[] { new DiagnosticResultLocation("Test0.cs", ObsoleteFieldAnalyzerTests.SampleCodeLine, ObsoleteFieldAnalyzerTests.SampleCodeColumn + column) }
}; };

View File

@ -6,14 +6,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.8.2" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SMAPI.ModBuildConfig.Analyzer\StardewModdingAPI.ModBuildConfig.Analyzer.csproj" /> <ProjectReference Include="..\SMAPI.ModBuildConfig.Analyzer\SMAPI.ModBuildConfig.Analyzer.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\common.targets" />
</Project> </Project>

View File

@ -135,22 +135,22 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor( private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor(
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/buildmsg/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.",
category: "SMAPI.CommonErrors", category: "SMAPI.CommonErrors",
defaultSeverity: DiagnosticSeverity.Warning, defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
helpLinkUri: "https://smapi.io/buildmsg/avoid-implicit-net-field-cast" helpLinkUri: "https://smapi.io/package/avoid-implicit-net-field-cast"
); );
/// <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 DiagnosticDescriptor(
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/buildmsg/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.",
category: "SMAPI.CommonErrors", category: "SMAPI.CommonErrors",
defaultSeverity: DiagnosticSeverity.Warning, defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
helpLinkUri: "https://smapi.io/buildmsg/avoid-net-field" helpLinkUri: "https://smapi.io/package/avoid-net-field"
); );
@ -199,7 +199,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>Analyse a member access syntax node and add a diagnostic message if applicable.</summary> /// <summary>Analyze a member access syntax node and add a diagnostic message if applicable.</summary>
/// <param name="context">The analysis context.</param> /// <param name="context">The analysis context.</param>
/// <returns>Returns whether any warnings were added.</returns> /// <returns>Returns whether any warnings were added.</returns>
private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
@ -231,7 +231,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
}); });
} }
/// <summary>Analyse an explicit cast or 'x as y' node and add a diagnostic message if applicable.</summary> /// <summary>Analyze an explicit cast or 'x as y' node and add a diagnostic message if applicable.</summary>
/// <param name="context">The analysis context.</param> /// <param name="context">The analysis context.</param>
/// <returns>Returns whether any warnings were added.</returns> /// <returns>Returns whether any warnings were added.</returns>
private void AnalyzeCast(SyntaxNodeAnalysisContext context) private void AnalyzeCast(SyntaxNodeAnalysisContext context)
@ -248,7 +248,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
}); });
} }
/// <summary>Analyse a binary comparison syntax node and add a diagnostic message if applicable.</summary> /// <summary>Analyze a binary comparison syntax node and add a diagnostic message if applicable.</summary>
/// <param name="context">The analysis context.</param> /// <param name="context">The analysis context.</param>
/// <returns>Returns whether any warnings were added.</returns> /// <returns>Returns whether any warnings were added.</returns>
private void AnalyzeBinaryComparison(SyntaxNodeAnalysisContext context) private void AnalyzeBinaryComparison(SyntaxNodeAnalysisContext context)
@ -288,7 +288,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
} }
/// <summary>Handle exceptions raised while analyzing a node.</summary> /// <summary>Handle exceptions raised while analyzing a node.</summary>
/// <param name="node">The node being analysed.</param> /// <param name="node">The node being analyzed.</param>
/// <param name="action">The callback to invoke.</param> /// <param name="action">The callback to invoke.</param>
private void HandleErrors(SyntaxNode node, Action action) private void HandleErrors(SyntaxNode node, Action action)
{ {

View File

@ -27,11 +27,11 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
["AvoidObsoleteField"] = new DiagnosticDescriptor( ["AvoidObsoleteField"] = new DiagnosticDescriptor(
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/buildmsg/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.",
category: "SMAPI.CommonErrors", category: "SMAPI.CommonErrors",
defaultSeverity: DiagnosticSeverity.Warning, defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
helpLinkUri: "https://smapi.io/buildmsg/avoid-obsolete-field" helpLinkUri: "https://smapi.io/package/avoid-obsolete-field"
) )
}; };
@ -67,7 +67,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>Analyse a syntax node and add a diagnostic message if it references an obsolete field.</summary> /// <summary>Analyze a syntax node and add a diagnostic message if it references an obsolete field.</summary>
/// <param name="context">The analysis context.</param> /// <param name="context">The analysis context.</param>
private void AnalyzeObsoleteFields(SyntaxNodeAnalysisContext context) private void AnalyzeObsoleteFields(SyntaxNodeAnalysisContext context)
{ {

View File

@ -1,4 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("SMAPI.ModBuildConfig.Analyzer")]
[assembly: AssemblyDescription("")]

View File

@ -1,19 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard1.3</TargetFramework> <AssemblyName>SMAPI.ModBuildConfig.Analyzer</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <RootNamespace>StardewModdingAPI.ModBuildConfig.Analyzer</RootNamespace>
<Version>3.0.0</Version>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<OutputPath>bin</OutputPath> <OutputPath>bin</OutputPath>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.8.2" PrivateAssets="all" />
<PackageReference Update="NETStandard.Library" PrivateAssets="all" /> <PackageReference Update="NETStandard.Library" PrivateAssets="all" />
</ItemGroup> </ItemGroup>

View File

@ -1,58 +0,0 @@
param($installPath, $toolsPath, $package, $project)
if($project.Object.SupportsPackageDependencyResolution)
{
if($project.Object.SupportsPackageDependencyResolution())
{
# Do not install analyzers via install.ps1, instead let the project system handle it.
return
}
}
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
foreach($analyzersPath in $analyzersPaths)
{
if (Test-Path $analyzersPath)
{
# Install the language agnostic analyzers.
foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
}
}
}
}
# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
$languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
$languageFolder = "vb"
}
if($languageFolder -eq "")
{
return
}
foreach($analyzersPath in $analyzersPaths)
{
# Install language specific analyzers.
$languageAnalyzersPath = join-path $analyzersPath $languageFolder
if (Test-Path $languageAnalyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
}
}
}
}

View File

@ -1,65 +0,0 @@
param($installPath, $toolsPath, $package, $project)
if($project.Object.SupportsPackageDependencyResolution)
{
if($project.Object.SupportsPackageDependencyResolution())
{
# Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it.
return
}
}
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
foreach($analyzersPath in $analyzersPaths)
{
# Uninstall the language agnostic analyzers.
if (Test-Path $analyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
}
}
}
}
# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
$languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
$languageFolder = "vb"
}
if($languageFolder -eq "")
{
return
}
foreach($analyzersPath in $analyzersPaths)
{
# Uninstall language specific analyzers.
$languageAnalyzersPath = join-path $analyzersPath $languageFolder
if (Test-Path $languageAnalyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
try
{
$project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
}
catch
{
}
}
}
}
}

View File

@ -3,8 +3,9 @@ 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.Serialisation; using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.ModBuildConfig.Framework namespace StardewModdingAPI.ModBuildConfig.Framework
{ {
@ -40,47 +41,14 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
if (!Directory.Exists(targetDir)) if (!Directory.Exists(targetDir))
throw new UserErrorException("Could not create mod package because no build output was found."); throw new UserErrorException("Could not create mod package because no build output was found.");
// project manifest // collect files
bool hasProjectManifest = false; foreach (Tuple<string, FileInfo> entry in this.GetPossibleFiles(projectDir, targetDir))
{ {
FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json")); string relativePath = entry.Item1;
if (manifest.Exists) FileInfo file = entry.Item2;
{
this.Files[this.ManifestFileName] = manifest;
hasProjectManifest = true;
}
}
// project i18n files if (!this.ShouldIgnore(file, relativePath, ignoreFilePatterns))
bool hasProjectTranslations = false; this.Files[relativePath] = file;
DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
if (translationsFolder.Exists)
{
foreach (FileInfo file in translationsFolder.EnumerateFiles())
this.Files[Path.Combine("i18n", file.Name)] = file;
hasProjectTranslations = true;
}
// build output
DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
// get relative paths
string relativePath = file.FullName.Replace(buildFolder.FullName, "");
string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, "");
// prefer project manifest/i18n files
if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName))
continue;
if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n"))
continue;
// handle ignored files
if (this.ShouldIgnore(file, relativePath, ignoreFilePatterns))
continue;
// add file
this.Files[relativePath] = file;
} }
// check for required files // check for required files
@ -117,6 +85,67 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>Get all files to include in the mod folder, not accounting for ignore patterns.</summary>
/// <param name="projectDir">The folder containing the project files.</param>
/// <param name="targetDir">The folder containing the build output.</param>
/// <returns>Returns tuples containing the relative path within the mod folder, and the file to copy to it.</returns>
private IEnumerable<Tuple<string, FileInfo>> GetPossibleFiles(string projectDir, string targetDir)
{
// project manifest
bool hasProjectManifest = false;
{
FileInfo manifest = new FileInfo(Path.Combine(projectDir, this.ManifestFileName));
if (manifest.Exists)
{
yield return Tuple.Create(this.ManifestFileName, manifest);
hasProjectManifest = true;
}
}
// project i18n files
bool hasProjectTranslations = false;
DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
if (translationsFolder.Exists)
{
foreach (FileInfo file in translationsFolder.EnumerateFiles())
yield return Tuple.Create(Path.Combine("i18n", file.Name), file);
hasProjectTranslations = true;
}
// project assets folder
bool hasAssetsFolder = false;
DirectoryInfo assetsFolder = new DirectoryInfo(Path.Combine(projectDir, "assets"));
if (assetsFolder.Exists)
{
foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName);
yield return Tuple.Create(relativePath, file);
}
hasAssetsFolder = true;
}
// build output
DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
// get path info
string relativePath = PathUtilities.GetRelativePath(buildFolder.FullName, file.FullName);
string[] segments = PathUtilities.GetSegments(relativePath);
// prefer project manifest/i18n/assets files
if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName))
continue;
if (hasProjectTranslations && this.EqualsInvariant(segments[0], "i18n"))
continue;
if (hasAssetsFolder && this.EqualsInvariant(segments[0], "assets"))
continue;
// add file
yield return Tuple.Create(relativePath, file);
}
}
/// <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>
@ -129,6 +158,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
// Json.NET (bundled into SMAPI) // Json.NET (bundled into SMAPI)
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll")
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.pdb")
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")
// code analysis files // code analysis files
@ -148,6 +178,8 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// <param name="other">The string to compare with.</param> /// <param name="other">The string to compare with.</param>
private bool EqualsInvariant(string str, string other) private bool EqualsInvariant(string str, string other)
{ {
if (str == null)
return other == null;
return str.Equals(other, StringComparison.InvariantCultureIgnoreCase); return str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
} }
} }

View File

@ -1,6 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("SMAPI.ModBuildConfig")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyVersion("2.2.0")]
[assembly: AssemblyFileVersion("2.2.0")]

View File

@ -1,23 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>SMAPI.ModBuildConfig</AssemblyName>
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace> <RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
<AssemblyName>StardewModdingAPI.ModBuildConfig</AssemblyName> <Version>3.0.0</Version>
<TargetFramework>net45</TargetFramework> <TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PlatformTarget>x86</PlatformTarget> <PlatformTarget>x86</PlatformTarget>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" /> <ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\..\docs\mod-build-config.md"> <None Include="..\..\build\find-game-folder.targets" Link="build\find-game-folder.targets" />
<Link>mod-build-config.md</Link> <None Include="..\..\docs\technical\mod-package.md" Link="mod-build-config.md" />
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -28,6 +27,20 @@
<Reference Include="System.Web.Extensions" /> <Reference Include="System.Web.Extensions" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="..\..\docs\technical\mod-package.md">
<Link>mod-package.md</Link>
</None>
</ItemGroup>
<ItemGroup>
<None Update="assets\nuget-icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<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\prepare-nuget-package.targets" />
</Project> </Project>

View File

@ -1,175 +1,113 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--*********************************************
** Import build tasks <Import Project="find-game-folder.targets" />
**********************************************--> <UsingTask TaskName="DeployModTask" AssemblyFile="SMAPI.ModBuildConfig.dll" />
<UsingTask TaskName="DeployModTask" AssemblyFile="StardewModdingAPI.ModBuildConfig.dll" />
<!--********************************************* <!--*********************************************
** Find the basic mod metadata ** Set build options
**********************************************--> **********************************************-->
<!-- import developer's custom settings (if any) -->
<Import Condition="$(OS) != 'Windows_NT' AND Exists('$(HOME)\stardewvalley.targets')" Project="$(HOME)\stardewvalley.targets" />
<Import Condition="$(OS) == 'Windows_NT' AND Exists('$(USERPROFILE)\stardewvalley.targets')" Project="$(USERPROFILE)\stardewvalley.targets" />
<!-- set setting defaults -->
<PropertyGroup> <PropertyGroup>
<!-- map legacy settings --> <!-- include PDB file by default to enable line numbers in stack traces -->
<ModFolderName Condition="'$(ModFolderName)' == '' AND '$(DeployModFolderName)' != ''">$(DeployModFolderName)</ModFolderName> <DebugType>pdbonly</DebugType>
<ModZipPath Condition="'$(ModZipPath)' == '' AND '$(DeployModZipTo)' != ''">$(DeployModZipTo)</ModZipPath> <DebugSymbols>true</DebugSymbols>
<!-- set default settings --> <!-- recognise XNA Framework DLLs in the GAC (only affects mods using new csproj format) -->
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
<!-- set default package options -->
<ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName> <ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName>
<ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath> <ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath>
<EnableModDeploy Condition="'$(EnableModDeploy)' == ''">True</EnableModDeploy> <EnableModDeploy Condition="'$(EnableModDeploy)' == ''">true</EnableModDeploy>
<EnableModZip Condition="'$(EnableModZip)' == ''">True</EnableModZip> <EnableModZip Condition="'$(EnableModZip)' == ''">true</EnableModZip>
<CopyModReferencesToBuildOutput Condition="'$(CopyModReferencesToBuildOutput)' == ''">False</CopyModReferencesToBuildOutput> <EnableHarmony Condition="'$(EnableModZip)' == ''">false</EnableHarmony>
<EnableGameDebugging Condition="$(EnableGameDebugging) == ''">true</EnableGameDebugging>
<CopyModReferencesToBuildOutput Condition="'$(CopyModReferencesToBuildOutput)' == '' OR ('$(CopyModReferencesToBuildOutput)' != 'true' AND '$(CopyModReferencesToBuildOutput)' != 'false')">false</CopyModReferencesToBuildOutput>
</PropertyGroup> </PropertyGroup>
<!-- find platform + game path --> <PropertyGroup Condition="$(OS) == 'Windows_NT' AND $(EnableGameDebugging) == 'true'">
<Choose> <!-- enable game debugging -->
<When Condition="$(OS) == 'Unix' OR $(OS) == 'OSX'"> <StartAction>Program</StartAction>
<PropertyGroup> <StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram>
<!-- Linux --> <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/GOG Games/Stardew Valley/game</GamePath> </PropertyGroup>
<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>
<!-- Mac (may be 'Unix' or 'OSX') -->
<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>
</PropertyGroup>
</When>
<When Condition="$(OS) == 'Windows_NT'">
<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 -->
<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>
<!-- derive from Steam library path -->
<_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>
</PropertyGroup>
</When>
</Choose>
<!--********************************************* <!--*********************************************
** Inject the assembly references and debugging configuration ** Add assembly references
**********************************************--> **********************************************-->
<Choose> <!-- common -->
<When Condition="$(OS) == 'Windows_NT'"> <ItemGroup>
<!-- references --> <Reference Include="$(GameExecutableName)">
<ItemGroup> <HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Private>false</Private> </Reference>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <Reference Include="StardewValley.GameData">
</Reference> <HintPath>$(GamePath)\StardewValley.GameData.dll</HintPath>
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Private>false</Private> </Reference>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <Reference Include="StardewModdingAPI">
</Reference> <HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Private>false</Private> </Reference>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <Reference Include="SMAPI.Toolkit.CoreInterfaces">
</Reference> <HintPath>$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll</HintPath>
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Private>false</Private> </Reference>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <Reference Include="xTile">
</Reference> <HintPath>$(GamePath)\xTile.dll</HintPath>
<Reference Include="Netcode"> <Private>$(CopyModReferencesToBuildOutput)</Private>
<HintPath>$(GamePath)\Netcode.dll</HintPath> </Reference>
<Private>False</Private> <Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'">
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <HintPath>$(GamePath)\smapi-internal\0Harmony.dll</HintPath>
</Reference> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Reference Include="Stardew Valley"> </Reference>
<HintPath>$(GamePath)\Stardew Valley.exe</HintPath> </ItemGroup>
<Private>false</Private>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference>
<Reference Include="StardewModdingAPI">
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
<Private>false</Private>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference>
<Reference Include="StardewModdingAPI.Toolkit.CoreInterfaces">
<HintPath>$(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll</HintPath>
<HintPath Condition="!Exists('$(GamePath)\smapi-internal')">$(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll</HintPath>
<Private>false</Private>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference>
<Reference Include="xTile, Version=2.0.4.0, Culture=neutral, processorArchitecture=x86">
<HintPath>$(GamePath)\xTile.dll</HintPath>
<Private>false</Private>
<SpecificVersion>False</SpecificVersion>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference>
</ItemGroup>
<!-- launch game for debugging --> <!-- Windows -->
<PropertyGroup> <ItemGroup Condition="$(OS) == 'Windows_NT'">
<StartAction>Program</StartAction> <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram> <Private>$(CopyModReferencesToBuildOutput)</Private>
<StartWorkingDirectory>$(GamePath)</StartWorkingDirectory> </Reference>
</PropertyGroup> <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
</When> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Otherwise> </Reference>
<!-- references --> <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<ItemGroup> <Private>$(CopyModReferencesToBuildOutput)</Private>
<Reference Include="MonoGame.Framework"> </Reference>
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath> <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>false</Private> <Private>$(CopyModReferencesToBuildOutput)</Private>
<SpecificVersion>False</SpecificVersion> </Reference>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <Reference Include="Netcode">
</Reference> <HintPath>$(GamePath)\Netcode.dll</HintPath>
<Reference Include="StardewValley"> <Private>$(CopyModReferencesToBuildOutput)</Private>
<HintPath>$(GamePath)\StardewValley.exe</HintPath> </Reference>
<Private>false</Private> </ItemGroup>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference> <!-- Linux/Mac -->
<Reference Include="StardewModdingAPI"> <ItemGroup Condition="$(OS) != 'Windows_NT'">
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath> <Reference Include="MonoGame.Framework">
<Private>false</Private> <HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private> <Private>$(CopyModReferencesToBuildOutput)</Private>
</Reference> </Reference>
<Reference Include="StardewModdingAPI.Toolkit.CoreInterfaces"> </ItemGroup>
<HintPath>$(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll</HintPath>
<HintPath Condition="!Exists('$(GamePath)\smapi-internal')">$(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll</HintPath>
<Private>false</Private>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference>
<Reference Include="xTile">
<HintPath>$(GamePath)\xTile.dll</HintPath>
<Private>false</Private>
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
</Reference>
</ItemGroup>
</Otherwise>
</Choose>
<!--********************************************* <!--*********************************************
** Deploy mod files & create release zip after build ** Show friendly error for invalid OS or game path
**********************************************--> **********************************************-->
<!-- if game path or OS is invalid, show one user-friendly error instead of a slew of reference errors -->
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">
<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/buildmsg/game-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." />
<Error Condition="'$(OS)' == 'Windows_NT' AND !Exists('$(GamePath)\Stardew Valley.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the Stardew Valley.exe file. If this folder is invalid, delete it and the package will autodetect another game install 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="'$(OS)' != 'Windows_NT' AND !Exists('$(GamePath)\StardewValley.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the StardewValley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." />
<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)\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." />
</Target> </Target>
<!-- deploy mod files & create release zip -->
<!--*********************************************
** Deploy mod files & create release zip
**********************************************-->
<Target Name="AfterBuild"> <Target Name="AfterBuild">
<DeployModTask <DeployModTask
ModFolderName="$(ModFolderName)" ModFolderName="$(ModFolderName)"

View File

@ -2,20 +2,35 @@
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata> <metadata>
<id>Pathoschild.Stardew.ModBuildConfig</id> <id>Pathoschild.Stardew.ModBuildConfig</id>
<version>2.2</version> <version>3.0.0</version>
<title>Build package for SMAPI mods</title> <title>Build package for SMAPI mods</title>
<authors>Pathoschild</authors> <authors>Pathoschild</authors>
<owners>Pathoschild</owners> <owners>Pathoschild</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<licenseUrl>https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt</licenseUrl> <license type="expression">MIT</license>
<projectUrl>https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme</projectUrl> <repository type="git" url="https://github.com/Pathoschild/SMAPI" />
<projectUrl>https://smapi.io/package/readme</projectUrl>
<icon>images\icon.png</icon>
<iconUrl>https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png</iconUrl> <iconUrl>https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png</iconUrl>
<description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later.</description> <description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later.</description>
<releaseNotes> <releaseNotes>
2.2: 3.0.0:
- Added support for SMAPI 2.8+ (still compatible with earlier versions). - Updated for SMAPI 3.0 and Stardew Valley 1.4.
- Added default game paths for 32-bit Windows. - Added automatic support for 'assets' folders.
- Fixed valid manifests marked invalid in some cases. - Added $(GameExecutableName) MSBuild variable.
- Added support for projects using the simplified .csproj format.
- Added option to disable game debugging config.
- Added .pdb files to builds by default (to enable line numbers in error stack traces).
- Added optional Harmony reference.
- Fixed Newtonsoft.Json.pdb included in release zips when Json.NET is referenced directly.
- Fixed &lt;IgnoreModFilePatterns&gt; not working for i18n files.
- Dropped support for older versions of SMAPI and Visual Studio.
- Migrated package icon to NuGet's new format.
</releaseNotes> </releaseNotes>
</metadata> </metadata>
<files>
<file src="analyzers\**" target="analyzers" />
<file src="build\**" target="build" />
<file src="images\**" target="images" />
</files>
</package> </package>

View File

@ -15,7 +15,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <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 ItemRepository();
/// <summary>The type names recognised 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();

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
@ -58,7 +58,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <param name="searchWords">The search string to find.</param> /// <param name="searchWords">The search string to find.</param>
private IEnumerable<SearchableItem> GetItems(string[] searchWords) private IEnumerable<SearchableItem> GetItems(string[] searchWords)
{ {
// normalise 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) if (searchWords?.Any() == false)
searchWords = null; searchWords = null;

View File

@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
@ -65,7 +65,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Update(IMonitor monitor) public override void Update(IMonitor monitor)
{ {
if (this.InfiniteMoney) if (this.InfiniteMoney)
Game1.player.money = 999999; Game1.player.Money = 999999;
} }
} }
} }

View File

@ -60,7 +60,7 @@ 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 mins so game updates to next interval Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 minutes so game updates to next interval
Game1.performTenMinuteClockUpdate(); Game1.performTenMinuteClockUpdate();
} }
} }

View File

@ -6,28 +6,31 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
/// <summary>A big craftable object in <see cref="StardewValley.Game1.bigCraftablesInformation"/></summary> /// <summary>A big craftable object in <see cref="StardewValley.Game1.bigCraftablesInformation"/></summary>
BigCraftable, BigCraftable,
/// <summary>A <see cref="Boots"/> item.</summary> /// <summary>A <see cref="StardewValley.Objects.Boots"/> item.</summary>
Boots, Boots,
/// <summary>A <see cref="Wallpaper"/> flooring item.</summary> /// <summary>A <see cref="StardewValley.Objects.Clothing"/> item.</summary>
Clothing,
/// <summary>A <see cref="StardewValley.Objects.Wallpaper"/> flooring item.</summary>
Flooring, Flooring,
/// <summary>A <see cref="Furniture"/> item.</summary> /// <summary>A <see cref="StardewValley.Objects.Furniture"/> item.</summary>
Furniture, Furniture,
/// <summary>A <see cref="Hat"/> item.</summary> /// <summary>A <see cref="StardewValley.Objects.Hat"/> item.</summary>
Hat, Hat,
/// <summary>Any object in <see cref="StardewValley.Game1.objectInformation"/> (except rings).</summary> /// <summary>Any object in <see cref="StardewValley.Game1.objectInformation"/> (except rings).</summary>
Object, Object,
/// <summary>A <see cref="Ring"/> item.</summary> /// <summary>A <see cref="StardewValley.Objects.Ring"/> item.</summary>
Ring, Ring,
/// <summary>A <see cref="Tool"/> tool.</summary> /// <summary>A <see cref="StardewValley.Tool"/> tool.</summary>
Tool, Tool,
/// <summary>A <see cref="Wallpaper"/> wall item.</summary> /// <summary>A <see cref="StardewValley.Objects.Wallpaper"/> wall item.</summary>
Wallpaper, Wallpaper,
/// <summary>A <see cref="StardewValley.Tools.MeleeWeapon"/> or <see cref="StardewValley.Tools.Slingshot"/> item.</summary> /// <summary>A <see cref="StardewValley.Tools.MeleeWeapon"/> or <see cref="StardewValley.Tools.Slingshot"/> item.</summary>

View File

@ -1,7 +1,11 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
using StardewValley; using StardewValley;
using StardewValley.Menus;
using StardewValley.Objects; using StardewValley.Objects;
using StardewValley.Tools; using StardewValley.Tools;
using SObject = StardewValley.Object; using SObject = StardewValley.Object;
@ -22,172 +26,227 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
** Public methods ** Public methods
*********/ *********/
/// <summary>Get all spawnable items.</summary> /// <summary>Get all spawnable items.</summary>
[SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")]
public IEnumerable<SearchableItem> GetAll() public IEnumerable<SearchableItem> GetAll()
{ {
// get tools IEnumerable<SearchableItem> GetAllRaw()
yield return new SearchableItem(ItemType.Tool, ToolFactory.axe, ToolFactory.getToolFromDescription(ToolFactory.axe, 0));
yield return new SearchableItem(ItemType.Tool, ToolFactory.hoe, ToolFactory.getToolFromDescription(ToolFactory.hoe, 0));
yield return new SearchableItem(ItemType.Tool, ToolFactory.pickAxe, ToolFactory.getToolFromDescription(ToolFactory.pickAxe, 0));
yield return new SearchableItem(ItemType.Tool, ToolFactory.wateringCan, ToolFactory.getToolFromDescription(ToolFactory.wateringCan, 0));
yield return new SearchableItem(ItemType.Tool, ToolFactory.fishingRod, ToolFactory.getToolFromDescription(ToolFactory.fishingRod, 0));
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset, new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 1, new Shears());
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 2, new Pan());
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 3, new Wand());
// wallpapers
for (int id = 0; id < 112; id++)
yield return new SearchableItem(ItemType.Wallpaper, id, new Wallpaper(id) { Category = SObject.furnitureCategory });
// flooring
for (int id = 0; id < 40; id++)
yield return new SearchableItem(ItemType.Flooring, id, new Wallpaper(id, isFloor: true) { Category = SObject.furnitureCategory });
// equipment
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\Boots").Keys)
yield return new SearchableItem(ItemType.Boots, id, new Boots(id));
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\hats").Keys)
yield return new SearchableItem(ItemType.Hat, id, new Hat(id));
foreach (int id in Game1.objectInformation.Keys)
{ {
if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange) // get tools
yield return new SearchableItem(ItemType.Ring, id, new Ring(id)); for (int quality = Tool.stone; quality <= Tool.iridium; quality++)
}
// weapons
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\weapons").Keys)
{
Item weapon = (id >= 32 && id <= 34)
? (Item)new Slingshot(id)
: new MeleeWeapon(id);
yield return new SearchableItem(ItemType.Weapon, id, weapon);
}
// furniture
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\Furniture").Keys)
{
if (id == 1466 || id == 1468)
yield return new SearchableItem(ItemType.Furniture, id, new TV(id, Vector2.Zero));
else
yield return new SearchableItem(ItemType.Furniture, id, new Furniture(id, Vector2.Zero));
}
// craftables
foreach (int id in Game1.bigCraftablesInformation.Keys)
yield return new SearchableItem(ItemType.BigCraftable, id, new SObject(Vector2.Zero, id));
// secret notes
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\SecretNotes").Keys)
{
SObject note = new SObject(79, 1);
note.name = $"{note.name} #{id}";
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, note);
}
// objects
foreach (int id in Game1.objectInformation.Keys)
{
if (id == 79)
continue; // secret note handled above
if (id >= Ring.ringLowerIndexRange && id <= Ring.ringUpperIndexRange)
continue; // handled separated
SObject item = new SObject(id, 1);
yield return new SearchableItem(ItemType.Object, id, item);
// fruit products
if (item.Category == SObject.FruitsCategory)
{ {
// wine yield return this.TryCreate(ItemType.Tool, ToolFactory.axe, () => ToolFactory.getToolFromDescription(ToolFactory.axe, quality));
SObject wine = new SObject(348, 1) yield return this.TryCreate(ItemType.Tool, ToolFactory.hoe, () => ToolFactory.getToolFromDescription(ToolFactory.hoe, quality));
{ yield return this.TryCreate(ItemType.Tool, ToolFactory.pickAxe, () => ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality));
Name = $"{item.Name} Wine", yield return this.TryCreate(ItemType.Tool, ToolFactory.wateringCan, () => ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality));
Price = item.Price * 3 if (quality != Tool.iridium)
}; yield return this.TryCreate(ItemType.Tool, ToolFactory.fishingRod, () => ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality));
wine.preserve.Value = SObject.PreserveType.Wine; }
wine.preservedParentSheetIndex.Value = item.ParentSheetIndex; yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset, () => new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, wine); yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 1, () => new Shears());
yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 2, () => new Pan());
yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 3, () => new Wand());
// jelly // clothing
SObject jelly = new SObject(344, 1) foreach (int id in Game1.clothingInformation.Keys)
{ yield return this.TryCreate(ItemType.Clothing, id, () => new Clothing(id));
Name = $"{item.Name} Jelly",
Price = 50 + item.Price * 2 // wallpapers
}; for (int id = 0; id < 112; id++)
jelly.preserve.Value = SObject.PreserveType.Jelly; yield return this.TryCreate(ItemType.Wallpaper, id, () => new Wallpaper(id) { Category = SObject.furnitureCategory });
jelly.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, jelly); // flooring
for (int id = 0; id < 56; id++)
yield return this.TryCreate(ItemType.Flooring, id, () => new Wallpaper(id, isFloor: true) { Category = SObject.furnitureCategory });
// equipment
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\Boots").Keys)
yield return this.TryCreate(ItemType.Boots, id, () => new Boots(id));
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\hats").Keys)
yield return this.TryCreate(ItemType.Hat, id, () => new Hat(id));
// weapons
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\weapons").Keys)
{
yield return this.TryCreate(ItemType.Weapon, id, () => (id >= 32 && id <= 34)
? (Item)new Slingshot(id)
: new MeleeWeapon(id)
);
} }
// vegetable products // furniture
else if (item.Category == SObject.VegetableCategory) foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\Furniture").Keys)
{ {
// juice if (id == 1466 || id == 1468)
SObject juice = new SObject(350, 1) yield return this.TryCreate(ItemType.Furniture, id, () => new TV(id, Vector2.Zero));
{ else
Name = $"{item.Name} Juice", yield return this.TryCreate(ItemType.Furniture, id, () => new Furniture(id, Vector2.Zero));
Price = (int)(item.Price * 2.25d)
};
juice.preserve.Value = SObject.PreserveType.Juice;
juice.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, juice);
// pickled
SObject pickled = new SObject(342, 1)
{
Name = $"Pickled {item.Name}",
Price = 50 + item.Price * 2
};
pickled.preserve.Value = SObject.PreserveType.Pickle;
pickled.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, pickled);
} }
// flower honey // craftables
else if (item.Category == SObject.flowersCategory) foreach (int id in Game1.bigCraftablesInformation.Keys)
yield return this.TryCreate(ItemType.BigCraftable, id, () => new SObject(Vector2.Zero, id));
// objects
foreach (int id in Game1.objectInformation.Keys)
{ {
// get honey type string[] fields = Game1.objectInformation[id]?.Split('/');
SObject.HoneyType? type = null;
switch (item.ParentSheetIndex)
{
case 376:
type = SObject.HoneyType.Poppy;
break;
case 591:
type = SObject.HoneyType.Tulip;
break;
case 593:
type = SObject.HoneyType.SummerSpangle;
break;
case 595:
type = SObject.HoneyType.FairyRose;
break;
case 597:
type = SObject.HoneyType.BlueJazz;
break;
case 421: // sunflower standing in for all other flowers
type = SObject.HoneyType.Wild;
break;
}
// yield honey // secret notes
if (type != null) if (id == 79)
{ {
SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) foreach (int secretNoteId in Game1.content.Load<Dictionary<int, string>>("Data\\SecretNotes").Keys)
{ {
Name = "Wild Honey" yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, () =>
}; {
honey.honeyType.Value = type; SObject note = new SObject(79, 1);
note.name = $"{note.name} #{secretNoteId}";
if (type != SObject.HoneyType.Wild) return note;
{ });
honey.Name = $"{item.Name} Honey"; }
honey.Price += item.Price * 2; }
// ring
else if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
yield return this.TryCreate(ItemType.Ring, id, () => new Ring(id));
// item
else
{
// spawn main item
SObject item = null;
yield return this.TryCreate(ItemType.Object, id, () =>
{
return item = (id == 812 // roe
? new ColoredObject(id, 1, Color.White)
: new SObject(id, 1)
);
});
if (item == null)
continue;
// flavored items
switch (item.Category)
{
// fruit products
case SObject.FruitsCategory:
// wine
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + id, () => new SObject(348, 1)
{
Name = $"{item.Name} Wine",
Price = item.Price * 3,
preserve = { SObject.PreserveType.Wine },
preservedParentSheetIndex = { item.ParentSheetIndex }
});
// jelly
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + id, () => new SObject(344, 1)
{
Name = $"{item.Name} Jelly",
Price = 50 + item.Price * 2,
preserve = { SObject.PreserveType.Jelly },
preservedParentSheetIndex = { item.ParentSheetIndex }
});
break;
// vegetable products
case SObject.VegetableCategory:
// juice
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + id, () => new SObject(350, 1)
{
Name = $"{item.Name} Juice",
Price = (int)(item.Price * 2.25d),
preserve = { SObject.PreserveType.Juice },
preservedParentSheetIndex = { item.ParentSheetIndex }
});
// pickled
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, () => new SObject(342, 1)
{
Name = $"Pickled {item.Name}",
Price = 50 + item.Price * 2,
preserve = { SObject.PreserveType.Pickle },
preservedParentSheetIndex = { item.ParentSheetIndex }
});
break;
// flower honey
case SObject.flowersCategory:
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, () =>
{
SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false)
{
Name = $"{item.Name} Honey",
preservedParentSheetIndex = { item.ParentSheetIndex }
};
honey.Price += item.Price * 2;
return honey;
});
break;
// roe and aged roe (derived from FishPond.GetFishProduce)
case SObject.sellAtFishShopCategory when id == 812:
foreach (var pair in Game1.objectInformation)
{
// get input
SObject input = this.TryCreate(ItemType.Object, -1, () => new SObject(pair.Key, 1))?.Item as SObject;
if (input == null || input.Category != SObject.FishCategory)
continue;
Color color = TailoringMenu.GetDyeColor(input) ?? Color.Orange;
// yield roe
SObject roe = null;
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, () =>
{
roe = new ColoredObject(812, 1, color)
{
name = $"{input.Name} Roe",
preserve = { Value = SObject.PreserveType.Roe },
preservedParentSheetIndex = { Value = input.ParentSheetIndex }
};
roe.Price += input.Price / 2;
return roe;
});
// aged roe
if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
{
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, () => new ColoredObject(447, 1, color)
{
name = $"Aged {input.Name} Roe",
Category = -27,
preserve = { Value = SObject.PreserveType.AgedRoe },
preservedParentSheetIndex = { Value = input.ParentSheetIndex },
Price = roe.Price * 2
});
}
}
break;
} }
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, honey);
} }
} }
} }
return GetAllRaw().Where(p => p != null);
}
/*********
** Private methods
*********/
/// <summary>Create a searchable item if valid.</summary>
/// <param name="type">The item type.</param>
/// <param name="id">The unique ID (if different from the item's parent sheet index).</param>
/// <param name="createItem">Create an item instance.</param>
private SearchableItem TryCreate(ItemType type, int id, Func<Item> createItem)
{
try
{
return new SearchableItem(type, id, createItem());
}
catch
{
return null; // if some item data is invalid, just don't include it
}
} }
} }
} }

View File

@ -1,4 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("SMAPI.Mods.ConsoleCommands")]
[assembly: AssemblyDescription("")]

View File

@ -0,0 +1,73 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>ConsoleCommands</AssemblyName>
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
<TargetFramework>net45</TargetFramework>
<LangVersion>latest</LangVersion>
<OutputPath>$(SolutionDir)\..\bin\$(Configuration)\Mods\ConsoleCommands</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SMAPI\SMAPI.csproj">
<Private>False</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Reference Include="$(GameExecutableName)">
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="StardewValley.GameData">
<HintPath>$(GamePath)\StardewValley.GameData.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<Choose>
<!-- Windows -->
<When Condition="$(OS) == 'Windows_NT'">
<ItemGroup>
<Reference Include="Netcode">
<HintPath>$(GamePath)\Netcode.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>False</Private>
</Reference>
</ItemGroup>
</When>
<!-- Linux/Mac -->
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework">
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<None Update="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
<Import Project="..\..\build\common.targets" />
</Project>

View File

@ -1,44 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
<AssemblyName>ConsoleCommands</AssemblyName>
<TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion>
<OutputPath>$(SolutionDir)\..\bin\$(Configuration)\Mods\ConsoleCommands</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="MonoGame.Framework">
<HintPath>..\..\..\..\..\Downloads\com.chucklefish.stardewvalley_1.322\assemblies\MonoGame.Framework.dll</HintPath>
</Reference>
<Reference Include="StardewModdingAPI">
<HintPath>..\SMAPI\bin\Debug\StardewModdingAPI.dll</HintPath>
</Reference>
<Reference Include="StardewValley">
<HintPath>..\..\..\..\..\Downloads\com.chucklefish.stardewvalley_1.322\assemblies\StardewValley.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
</Project>

View File

@ -1,9 +1,9 @@
{ {
"Name": "Console Commands", "Name": "Console Commands",
"Author": "SMAPI", "Author": "SMAPI",
"Version": "2.11.3", "Version": "3.1.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.", "Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands", "UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll", "EntryDll": "ConsoleCommands.dll",
"MinimumApiVersion": "2.11.3" "MinimumApiVersion": "3.1.0"
} }

View File

@ -4,6 +4,7 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Mods.SaveBackup namespace StardewModdingAPI.Mods.SaveBackup
@ -40,9 +41,10 @@ namespace StardewModdingAPI.Mods.SaveBackup
DirectoryInfo backupFolder = new DirectoryInfo(this.BackupFolder); DirectoryInfo backupFolder = new DirectoryInfo(this.BackupFolder);
backupFolder.Create(); backupFolder.Create();
// back up saves // back up & prune saves
this.CreateBackup(backupFolder); Task
this.PruneBackups(backupFolder, this.BackupsToKeep); .Run(() => this.CreateBackup(backupFolder))
.ContinueWith(backupTask => this.PruneBackups(backupFolder, this.BackupsToKeep));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -66,49 +68,23 @@ namespace StardewModdingAPI.Mods.SaveBackup
if (targetFile.Exists || fallbackDir.Exists) if (targetFile.Exists || fallbackDir.Exists)
return; return;
// create zip // back up saves
// due to limitations with the bundled Mono on Mac, we can't reference System.IO.Compression. this.Monitor.Log($"Backing up saves to {targetFile.FullName}...", LogLevel.Trace);
this.Monitor.Log($"Adding {targetFile.Name}...", LogLevel.Trace); if (!this.TryCompress(Constants.SavesPath, targetFile, out Exception compressError))
switch (Constants.TargetPlatform)
{ {
case GamePlatform.Android: // log error (expected on Android due to missing compression DLLs)
case GamePlatform.Linux: if (Constants.TargetPlatform == GamePlatform.Android)
case GamePlatform.Windows: this.Monitor.VerboseLog($"Compression isn't supported on Android:\n{compressError}");
{ else
try {
{ this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead.", LogLevel.Debug);
// create compressed backup this.Monitor.Log(compressError.ToString(), LogLevel.Trace);
Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly."); }
Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type.");
Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type.");
MethodInfo createMethod = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method.");
createMethod.Invoke(null, new object[] { Constants.SavesPath, targetFile.FullName, CompressionLevel.Fastest, false });
}
catch (Exception ex) when (ex is TypeLoadException || ex.InnerException is TypeLoadException)
{
// create uncompressed backup if compression fails
this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead.", LogLevel.Debug);
this.Monitor.Log(ex.ToString(), LogLevel.Trace);
this.RecursiveCopy(new DirectoryInfo(Constants.SavesPath), fallbackDir, copyRoot: false);
}
}
break;
case GamePlatform.Mac: // fallback to uncompressed
{ this.RecursiveCopy(new DirectoryInfo(Constants.SavesPath), fallbackDir, copyRoot: false);
DirectoryInfo saveFolder = new DirectoryInfo(Constants.SavesPath);
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "zip",
Arguments = $"-rq \"{targetFile.FullName}\" \"{saveFolder.Name}\" -x \"*.DS_Store\" -x \"__MACOSX\"",
WorkingDirectory = $"{Constants.SavesPath}/../",
CreateNoWindow = true
};
new Process { StartInfo = startInfo }.Start();
}
break;
} }
this.Monitor.Log("Backup done!", LogLevel.Trace);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -125,20 +101,23 @@ namespace StardewModdingAPI.Mods.SaveBackup
try try
{ {
var oldBackups = backupFolder var oldBackups = backupFolder
.GetFiles() .GetFileSystemInfos()
.OrderByDescending(p => p.CreationTimeUtc) .OrderByDescending(p => p.CreationTimeUtc)
.Skip(backupsToKeep); .Skip(backupsToKeep);
foreach (FileInfo file in oldBackups) foreach (FileSystemInfo entry in oldBackups)
{ {
try try
{ {
this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); this.Monitor.Log($"Deleting {entry.Name}...", LogLevel.Trace);
file.Delete(); if (entry is DirectoryInfo folder)
folder.Delete(recursive: true);
else
entry.Delete();
} }
catch (Exception ex) catch (Exception ex)
{ {
this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}", LogLevel.Error); this.Monitor.Log($"Error deleting old save backup '{entry.Name}': {ex}", LogLevel.Error);
} }
} }
} }
@ -149,6 +128,72 @@ namespace StardewModdingAPI.Mods.SaveBackup
} }
} }
/// <summary>Create a zip using the best available method.</summary>
/// <param name="sourcePath">The file or directory path to zip.</param>
/// <param name="destination">The destination file to create.</param>
/// <param name="error">The error which occurred trying to compress, if applicable. This is <see cref="NotSupportedException"/> if compression isn't supported on this platform.</param>
/// <returns>Returns whether compression succeeded.</returns>
private bool TryCompress(string sourcePath, FileInfo destination, out Exception error)
{
try
{
if (Constants.TargetPlatform == GamePlatform.Mac)
this.CompressUsingMacProcess(sourcePath, destination); // due to limitations with the bundled Mono on Mac, we can't reference System.IO.Compression
else
this.CompressUsingNetFramework(sourcePath, destination);
error = null;
return true;
}
catch (Exception ex)
{
error = ex;
return false;
}
}
/// <summary>Create a zip using the .NET compression library.</summary>
/// <param name="sourcePath">The file or directory path to zip.</param>
/// <param name="destination">The destination file to create.</param>
/// <exception cref="NotSupportedException">The compression libraries aren't available on this system.</exception>
private void CompressUsingNetFramework(string sourcePath, FileInfo destination)
{
// get compress method
MethodInfo createFromDirectory;
try
{
// create compressed backup
Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type.");
Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type.");
createFromDirectory = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method.");
}
catch (Exception ex)
{
throw new NotSupportedException("Couldn't load the .NET compression libraries on this system.", ex);
}
// compress file
createFromDirectory.Invoke(null, new object[] { sourcePath, destination.FullName, CompressionLevel.Fastest, false });
}
/// <summary>Create a zip using a process command on MacOS.</summary>
/// <param name="sourcePath">The file or directory path to zip.</param>
/// <param name="destination">The destination file to create.</param>
private void CompressUsingMacProcess(string sourcePath, FileInfo destination)
{
DirectoryInfo saveFolder = new DirectoryInfo(sourcePath);
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "zip",
Arguments = $"-rq \"{destination.FullName}\" \"{saveFolder.Name}\" -x \"*.DS_Store\" -x \"__MACOSX\"",
WorkingDirectory = $"{saveFolder.FullName}/../",
CreateNoWindow = true
};
new Process { StartInfo = startInfo }.Start();
}
/// <summary>Recursively copy a directory or file.</summary> /// <summary>Recursively copy a directory or file.</summary>
/// <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>

View File

@ -1,4 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("StardewModdingAPI.Mods.SaveBackup")]
[assembly: AssemblyDescription("")]

View File

@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
<AssemblyName>SaveBackup</AssemblyName> <AssemblyName>SaveBackup</AssemblyName>
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
<TargetFramework>net45</TargetFramework> <TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<OutputPath>C:\Users\Chris\source\repos\SMAPI\bin\Debug\Mods\SaveBackup\</OutputPath> <OutputPath>C:\Users\Chris\source\repos\SMAPI\bin\Debug\Mods\SaveBackup\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@ -12,21 +11,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <ProjectReference Include="..\SMAPI\SMAPI.csproj">
<Link>Properties\GlobalAssemblyInfo.cs</Link> <Private>False</Private>
</Compile> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj" /> <Reference Include="$(GameExecutableName)">
</ItemGroup> <HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
<Private>False</Private>
<ItemGroup>
<Reference Include="StardewModdingAPI">
<HintPath>..\SMAPI\bin\Debug\StardewModdingAPI.dll</HintPath>
</Reference>
<Reference Include="StardewValley">
<HintPath>..\..\..\..\..\Downloads\com.chucklefish.stardewvalley_1.322\assemblies\StardewValley.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>

View File

@ -1,9 +1,9 @@
{ {
"Name": "Save Backup", "Name": "Save Backup",
"Author": "SMAPI", "Author": "SMAPI",
"Version": "2.11.3", "Version": "3.1.0",
"Description": "Automatically backs up all your saves once per day into its folder.", "Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup", "UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll", "EntryDll": "SaveBackup.dll",
"MinimumApiVersion": "2.11.3" "MinimumApiVersion": "3.1.0"
} }

View File

@ -5,13 +5,15 @@ using System.Linq;
using Moq; using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Framework; using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Serialization.Models;
using SemanticVersion = StardewModdingAPI.SemanticVersion;
namespace StardewModdingAPI.Tests.Core namespace SMAPI.Tests.Core
{ {
/// <summary>Unit tests for <see cref="ModResolver"/>.</summary> /// <summary>Unit tests for <see cref="ModResolver"/>.</summary>
[TestFixture] [TestFixture]
@ -27,7 +29,7 @@ namespace StardewModdingAPI.Tests.Core
public void ReadBasicManifest_NoMods_ReturnsEmptyList() public void ReadBasicManifest_NoMods_ReturnsEmptyList()
{ {
// arrange // arrange
string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string rootFolder = this.GetTempFolderPath();
Directory.CreateDirectory(rootFolder); Directory.CreateDirectory(rootFolder);
// act // act
@ -41,7 +43,7 @@ namespace StardewModdingAPI.Tests.Core
public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest() public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest()
{ {
// arrange // arrange
string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string rootFolder = this.GetTempFolderPath();
string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(modFolder); Directory.CreateDirectory(modFolder);
@ -55,7 +57,7 @@ namespace StardewModdingAPI.Tests.Core
Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set.");
} }
[Test(Description = "Assert that the resolver correctly reads manifest data from a randomised file.")] [Test(Description = "Assert that the resolver correctly reads manifest data from a randomized file.")]
public void ReadBasicManifest_CanReadFile() public void ReadBasicManifest_CanReadFile()
{ {
// create manifest data // create manifest data
@ -78,7 +80,7 @@ namespace StardewModdingAPI.Tests.Core
}; };
// write to filesystem // write to filesystem
string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string rootFolder = this.GetTempFolderPath();
string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N"));
string filename = Path.Combine(modFolder, "manifest.json"); string filename = Path.Combine(modFolder, "manifest.json");
Directory.CreateDirectory(modFolder); Directory.CreateDirectory(modFolder);
@ -209,7 +211,7 @@ namespace StardewModdingAPI.Tests.Core
IManifest manifest = this.GetManifest(); IManifest manifest = this.GetManifest();
// create DLL // create DLL
string modFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(this.GetTempFolderPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(modFolder); Directory.CreateDirectory(modFolder);
File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), ""); File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), "");
@ -462,7 +464,13 @@ namespace StardewModdingAPI.Tests.Core
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>Get a randomised basic manifest.</summary> /// <summary>Get a generated folder path in the temp folder. This folder isn't created automatically.</summary>
private string GetTempFolderPath()
{
return Path.Combine(Path.GetTempPath(), "smapi-unit-tests", Guid.NewGuid().ToString("N"));
}
/// <summary>Get a randomized basic manifest.</summary>
/// <param name="id">The <see cref="IManifest.UniqueID"/> value, or <c>null</c> for a generated value.</param> /// <param name="id">The <see cref="IManifest.UniqueID"/> value, or <c>null</c> for a generated value.</param>
/// <param name="name">The <see cref="IManifest.Name"/> value, or <c>null</c> for a generated value.</param> /// <param name="name">The <see cref="IManifest.Name"/> value, or <c>null</c> for a generated value.</param>
/// <param name="version">The <see cref="IManifest.Version"/> value, or <c>null</c> for a generated value.</param> /// <param name="version">The <see cref="IManifest.Version"/> value, or <c>null</c> for a generated value.</param>
@ -486,14 +494,14 @@ namespace StardewModdingAPI.Tests.Core
}; };
} }
/// <summary>Get a randomised basic manifest.</summary> /// <summary>Get a randomized basic manifest.</summary>
/// <param name="uniqueID">The mod's name and unique ID.</param> /// <param name="uniqueID">The mod's name and unique ID.</param>
private Mock<IModMetadata> GetMetadata(string uniqueID) private Mock<IModMetadata> GetMetadata(string uniqueID)
{ {
return this.GetMetadata(this.GetManifest(uniqueID, "1.0")); return this.GetMetadata(this.GetManifest(uniqueID, "1.0"));
} }
/// <summary>Get a randomised basic manifest.</summary> /// <summary>Get a randomized basic manifest.</summary>
/// <param name="uniqueID">The mod's name and unique ID.</param> /// <param name="uniqueID">The mod's name and unique ID.</param>
/// <param name="dependencies">The dependencies this mod requires.</param> /// <param name="dependencies">The dependencies this mod requires.</param>
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param> /// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
@ -503,7 +511,7 @@ namespace StardewModdingAPI.Tests.Core
return this.GetMetadata(manifest, allowStatusChange); return this.GetMetadata(manifest, allowStatusChange);
} }
/// <summary>Get a randomised basic manifest.</summary> /// <summary>Get a randomized basic manifest.</summary>
/// <param name="manifest">The mod manifest.</param> /// <param name="manifest">The mod manifest.</param>
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param> /// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false) private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false)

View File

@ -2,10 +2,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModHelpers;
using StardewValley; using StardewValley;
namespace StardewModdingAPI.Tests.Core namespace SMAPI.Tests.Core
{ {
/// <summary>Unit tests for <see cref="TranslationHelper"/> and <see cref="Translation"/>.</summary> /// <summary>Unit tests for <see cref="TranslationHelper"/> and <see cref="Translation"/>.</summary>
[TestFixture] [TestFixture]
@ -31,7 +32,7 @@ namespace StardewModdingAPI.Tests.Core
var data = new Dictionary<string, IDictionary<string, string>>(); var data = new Dictionary<string, IDictionary<string, string>>();
// act // act
ITranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
Translation translation = helper.Get("key"); Translation translation = helper.Get("key");
Translation[] translationList = helper.GetTranslations()?.ToArray(); Translation[] translationList = helper.GetTranslations()?.ToArray();
@ -54,7 +55,7 @@ namespace StardewModdingAPI.Tests.Core
// act // act
var actual = new Dictionary<string, Translation[]>(); var actual = new Dictionary<string, Translation[]>();
TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys) foreach (string locale in expected.Keys)
{ {
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
@ -78,7 +79,7 @@ namespace StardewModdingAPI.Tests.Core
// act // act
var actual = new Dictionary<string, Translation[]>(); var actual = new Dictionary<string, Translation[]>();
TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys) foreach (string locale in expected.Keys)
{ {
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
@ -108,14 +109,14 @@ namespace StardewModdingAPI.Tests.Core
[TestCase(" boop ", ExpectedResult = true)] [TestCase(" boop ", ExpectedResult = true)]
public bool Translation_HasValue(string text) public bool Translation_HasValue(string text)
{ {
return new Translation("ModName", "pt-BR", "key", text).HasValue(); return new Translation("pt-BR", "key", text).HasValue();
} }
[Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")]
public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text)
{ {
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", text); Translation translation = new Translation("pt-BR", "key", text);
// assert // assert
if (translation.HasValue()) if (translation.HasValue())
@ -128,7 +129,7 @@ namespace StardewModdingAPI.Tests.Core
public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text)
{ {
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", text); Translation translation = new Translation("pt-BR", "key", text);
// assert // assert
if (translation.HasValue()) if (translation.HasValue())
@ -141,7 +142,7 @@ namespace StardewModdingAPI.Tests.Core
public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text)
{ {
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", text).UsePlaceholder(value); Translation translation = new Translation("pt-BR", "key", text).UsePlaceholder(value);
// assert // assert
if (translation.HasValue()) if (translation.HasValue())
@ -152,24 +153,11 @@ namespace StardewModdingAPI.Tests.Core
Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled."); Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled.");
} }
[Test(Description = "Assert that the translation's Assert method throws the expected exception.")]
public void Translation_Assert([ValueSource(nameof(TranslationTests.Samples))] string text)
{
// act
Translation translation = new Translation("ModName", "pt-BR", "key", text);
// assert
if (translation.HasValue())
Assert.That(() => translation.Assert(), Throws.Nothing, "The assert unexpected threw an exception for a valid input.");
else
Assert.That(() => translation.Assert(), Throws.Exception.TypeOf<KeyNotFoundException>(), "The assert didn't throw an exception for invalid input.");
}
[Test(Description = "Assert that the translation returns the expected text after setting the default.")] [Test(Description = "Assert that the translation returns the expected text after setting the default.")]
public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default)
{ {
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", text).Default(@default); Translation translation = new Translation("pt-BR", "key", text).Default(@default);
// assert // assert
if (!string.IsNullOrEmpty(text)) if (!string.IsNullOrEmpty(text))
@ -194,7 +182,7 @@ namespace StardewModdingAPI.Tests.Core
string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}";
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", input); Translation translation = new Translation("pt-BR", "key", input);
switch (structure) switch (structure)
{ {
case "anonymous object": case "anonymous object":
@ -235,7 +223,7 @@ namespace StardewModdingAPI.Tests.Core
string value = Guid.NewGuid().ToString("N"); string value = Guid.NewGuid().ToString("N");
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary<string, object> { [key] = value }); Translation translation = new Translation("pt-BR", "key", text).Tokens(new Dictionary<string, object> { [key] = value });
// assert // assert
Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text.");
@ -245,13 +233,13 @@ namespace StardewModdingAPI.Tests.Core
[TestCase("{{value}}", "value")] [TestCase("{{value}}", "value")]
[TestCase("{{VaLuE}}", "vAlUe")] [TestCase("{{VaLuE}}", "vAlUe")]
[TestCase("{{VaLuE }}", " vAlUe")] [TestCase("{{VaLuE }}", " vAlUe")]
public void Translation_Tokens_KeysAreNormalised(string text, string key) public void Translation_Tokens_KeysAreNormalized(string text, string key)
{ {
// arrange // arrange
string value = Guid.NewGuid().ToString("N"); string value = Guid.NewGuid().ToString("N");
// act // act
Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary<string, object> { [key] = value }); Translation translation = new Translation("pt-BR", "key", text).Tokens(new Dictionary<string, object> { [key] = value });
// assert // assert
Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text.");
@ -302,19 +290,19 @@ namespace StardewModdingAPI.Tests.Core
{ {
["default"] = new[] ["default"] = new[]
{ {
new Translation(string.Empty, "default", "key A", "default A"), new Translation("default", "key A", "default A"),
new Translation(string.Empty, "default", "key C", "default C") new Translation("default", "key C", "default C")
}, },
["en"] = new[] ["en"] = new[]
{ {
new Translation(string.Empty, "en", "key A", "en A"), new Translation("en", "key A", "en A"),
new Translation(string.Empty, "en", "key B", "en B"), new Translation("en", "key B", "en B"),
new Translation(string.Empty, "en", "key C", "default C") new Translation("en", "key C", "default C")
}, },
["zzz"] = new[] ["zzz"] = new[]
{ {
new Translation(string.Empty, "zzz", "key A", "zzz A"), new Translation("zzz", "key A", "zzz A"),
new Translation(string.Empty, "zzz", "key C", "default C") new Translation("zzz", "key C", "default C")
} }
}; };
expected["en-us"] = expected["en"].ToArray(); expected["en-us"] = expected["en"].ToArray();

View File

@ -1,4 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("SMAPI.Tests")]
[assembly: AssemblyDescription("")]

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SMAPI.Tests</AssemblyName>
<RootNamespace>SMAPI.Tests</RootNamespace>
<TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
<ProjectReference Include="..\SMAPI\SMAPI.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NUnit" Version="3.12.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="$(GameExecutableName)">
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="..\..\build\common.targets" />
</Project>

View File

@ -1,6 +1,6 @@
using System; using System;
namespace StardewModdingAPI.Tests namespace SMAPI.Tests
{ {
/// <summary>Provides sample values for unit testing.</summary> /// <summary>Provides sample values for unit testing.</summary>
internal static class Sample internal static class Sample

View File

@ -1,37 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>StardewModdingAPI.Tests</RootNamespace>
<AssemblyName>StardewModdingAPI.Tests</AssemblyName>
<TargetFramework>net45</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.3.1" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.2" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
</Project>

View File

@ -1,7 +1,7 @@
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Tests.Toolkit namespace SMAPI.Tests.Toolkit
{ {
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary> /// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
[TestFixture] [TestFixture]
@ -25,7 +25,7 @@ namespace StardewModdingAPI.Tests.Toolkit
return string.Join("|", PathUtilities.GetSegments(path)); return string.Join("|", PathUtilities.GetSegments(path));
} }
[Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] [Test(Description = "Assert that NormalizePathSeparators returns the expected values.")]
#if SMAPI_FOR_WINDOWS #if SMAPI_FOR_WINDOWS
[TestCase("", ExpectedResult = "")] [TestCase("", ExpectedResult = "")]
[TestCase("/", ExpectedResult = "")] [TestCase("/", ExpectedResult = "")]
@ -47,9 +47,9 @@ namespace StardewModdingAPI.Tests.Toolkit
[TestCase("C:/boop", ExpectedResult = "C:/boop")] [TestCase("C:/boop", ExpectedResult = "C:/boop")]
[TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")]
#endif #endif
public string NormalisePathSeparators(string path) public string NormalizePathSeparators(string path)
{ {
return PathUtilities.NormalisePathSeparators(path); return PathUtilities.NormalizePathSeparators(path);
} }
[Test(Description = "Assert that GetRelativePath returns the expected values.")] [Test(Description = "Assert that GetRelativePath returns the expected values.")]

View File

@ -6,7 +6,7 @@ using System.Text.RegularExpressions;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI.Utilities; using StardewModdingAPI.Utilities;
namespace StardewModdingAPI.Tests.Utilities namespace SMAPI.Tests.Utilities
{ {
/// <summary>Unit tests for <see cref="SDate"/>.</summary> /// <summary>Unit tests for <see cref="SDate"/>.</summary>
[TestFixture] [TestFixture]
@ -159,7 +159,7 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("15 summer Y1", -28, ExpectedResult = "15 spring Y1")] // negative season transition [TestCase("15 summer Y1", -28, ExpectedResult = "15 spring Y1")] // negative season transition
[TestCase("15 summer Y2", -28 * 4, ExpectedResult = "15 summer Y1")] // negative year transition [TestCase("15 summer Y2", -28 * 4, ExpectedResult = "15 summer Y1")] // negative year transition
[TestCase("01 spring Y3", -(28 * 7 + 17), ExpectedResult = "12 spring Y1")] // negative year transition [TestCase("01 spring Y3", -(28 * 7 + 17), ExpectedResult = "12 spring Y1")] // negative year transition
[TestCase("06 fall Y2", 50, ExpectedResult = "28 winter Y3")] // test for zero-index errors [TestCase("06 fall Y2", 50, ExpectedResult = "28 winter Y2")] // test for zero-index errors
[TestCase("06 fall Y2", 51, ExpectedResult = "01 spring Y3")] // test for zero-index errors [TestCase("06 fall Y2", 51, ExpectedResult = "01 spring Y3")] // test for zero-index errors
public string AddDays(string dateStr, int addDays) public string AddDays(string dateStr, int addDays)
{ {

View File

@ -2,9 +2,10 @@ using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json; using Newtonsoft.Json;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Framework; using StardewModdingAPI.Framework;
namespace StardewModdingAPI.Tests.Utilities namespace SMAPI.Tests.Utilities
{ {
/// <summary>Unit tests for <see cref="SemanticVersion"/>.</summary> /// <summary>Unit tests for <see cref="SemanticVersion"/>.</summary>
[TestFixture] [TestFixture]
@ -17,55 +18,61 @@ namespace StardewModdingAPI.Tests.Utilities
** Constructor ** Constructor
****/ ****/
[Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from a string.")] [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from a string.")]
[TestCase("1.0", ExpectedResult = "1.0")] [TestCase("1.0", ExpectedResult = "1.0.0")]
[TestCase("1.0.0", ExpectedResult = "1.0")] [TestCase("1.0.0", ExpectedResult = "1.0.0")]
[TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")] [TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")]
[TestCase("1.2-some-tag.4", ExpectedResult = "1.2-some-tag.4")] [TestCase("1.2-some-tag.4", ExpectedResult = "1.2.0-some-tag.4")]
[TestCase("1.2.3-some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] [TestCase("1.2.3-some-tag.4", ExpectedResult = "1.2.3-some-tag.4")]
[TestCase("1.2.3-SoME-tAg.4", ExpectedResult = "1.2.3-SoME-tAg.4")] [TestCase("1.2.3-SoME-tAg.4", ExpectedResult = "1.2.3-SoME-tAg.4")]
[TestCase("1.2.3-some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] [TestCase("1.2.3-some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")]
[TestCase("1.2.3-some-tag.4+build.004", ExpectedResult = "1.2.3-some-tag.4+build.004")]
[TestCase("1.2+3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")]
public string Constructor_FromString(string input) public string Constructor_FromString(string input)
{ {
return new SemanticVersion(input).ToString(); return new SemanticVersion(input).ToString();
} }
[Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from the individual numbers.")] [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from the individual numbers.")]
[TestCase(1, 0, 0, null, ExpectedResult = "1.0")] [TestCase(1, 0, 0, null, null, ExpectedResult = "1.0.0")]
[TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] [TestCase(3000, 4000, 5000, null, null, ExpectedResult = "3000.4000.5000")]
[TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] [TestCase(1, 2, 3, "", null, ExpectedResult = "1.2.3")]
[TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] [TestCase(1, 2, 3, " ", null, ExpectedResult = "1.2.3")]
[TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3-0")] [TestCase(1, 2, 3, "0", null, ExpectedResult = "1.2.3-0")]
[TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] [TestCase(1, 2, 3, "some-tag.4", null, ExpectedResult = "1.2.3-some-tag.4")]
[TestCase(1, 2, 3, "sOMe-TaG.4", ExpectedResult = "1.2.3-sOMe-TaG.4")] [TestCase(1, 2, 3, "sOMe-TaG.4", null, ExpectedResult = "1.2.3-sOMe-TaG.4")]
[TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] [TestCase(1, 2, 3, "some-tag.4 ", null, ExpectedResult = "1.2.3-some-tag.4")]
public string Constructor_FromParts(int major, int minor, int patch, string tag) [TestCase(1, 2, 3, "some-tag.4 ", "build.004", ExpectedResult = "1.2.3-some-tag.4+build.004")]
[TestCase(1, 2, 0, null, "3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")]
public string Constructor_FromParts(int major, int minor, int patch, string prerelease, string build)
{ {
// act // act
ISemanticVersion version = new SemanticVersion(major, minor, patch, tag); ISemanticVersion version = new SemanticVersion(major, minor, patch, prerelease, build);
// assert // assert
Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value.");
Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value.");
Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value.");
Assert.AreEqual(string.IsNullOrWhiteSpace(tag) ? null : tag.Trim(), version.PrereleaseTag, "The tag doesn't match the given value."); Assert.AreEqual(string.IsNullOrWhiteSpace(prerelease) ? null : prerelease.Trim(), version.PrereleaseTag, "The prerelease tag doesn't match the given value.");
Assert.AreEqual(string.IsNullOrWhiteSpace(build) ? null : build.Trim(), version.BuildMetadata, "The build metadata doesn't match the given value.");
return version.ToString(); return version.ToString();
} }
[Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")] [Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")]
[TestCase(0, 0, 0, null)] [TestCase(0, 0, 0, null, null)]
[TestCase(-1, 0, 0, null)] [TestCase(-1, 0, 0, null, null)]
[TestCase(0, -1, 0, null)] [TestCase(0, -1, 0, null, null)]
[TestCase(0, 0, -1, null)] [TestCase(0, 0, -1, null, null)]
[TestCase(1, 0, 0, "-tag")] [TestCase(1, 0, 0, "-tag", null)]
[TestCase(1, 0, 0, "tag spaces")] [TestCase(1, 0, 0, "tag spaces", null)]
[TestCase(1, 0, 0, "tag~")] [TestCase(1, 0, 0, "tag~", null)]
public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string tag) [TestCase(1, 0, 0, null, "build~")]
public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string prerelease, string build)
{ {
this.AssertAndLogException<FormatException>(() => new SemanticVersion(major, minor, patch, tag)); this.AssertAndLogException<FormatException>(() => new SemanticVersion(major, minor, patch, prerelease, build));
} }
[Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")] [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")]
[TestCase(1, 0, 0, ExpectedResult = "1.0")] [TestCase(1, 0, 0, ExpectedResult = "1.0.0")]
[TestCase(1, 2, 3, ExpectedResult = "1.2.3")] [TestCase(1, 2, 3, ExpectedResult = "1.2.3")]
[TestCase(3000, 4000, 5000, ExpectedResult = "3000.4000.5000")] [TestCase(3000, 4000, 5000, ExpectedResult = "3000.4000.5000")]
public string Constructor_FromAssemblyVersion(int major, int minor, int patch) public string Constructor_FromAssemblyVersion(int major, int minor, int patch)
@ -97,6 +104,7 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("1.2.3--some-tag")] [TestCase("1.2.3--some-tag")]
[TestCase("1.2.3-some-tag...")] [TestCase("1.2.3-some-tag...")]
[TestCase("1.2.3-some-tag...4")] [TestCase("1.2.3-some-tag...4")]
[TestCase("1.2.3-some-tag.4+build...4")]
[TestCase("apple")] [TestCase("apple")]
[TestCase("-apple")] [TestCase("-apple")]
[TestCase("-5")] [TestCase("-5")]
@ -118,6 +126,8 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("1.0-beta", "1.0-beta", ExpectedResult = 0)] [TestCase("1.0-beta", "1.0-beta", ExpectedResult = 0)]
[TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = 0)] [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = 0)]
[TestCase("1.0-beta", "1.0-beta ", ExpectedResult = 0)] [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = 0)]
[TestCase("1.0-beta+build.001", "1.0-beta+build.001", ExpectedResult = 0)]
[TestCase("1.0-beta+build.001", "1.0-beta+build.006", ExpectedResult = 0)] // build metadata must not affect precedence
// less than // less than
[TestCase("0.5.7", "0.5.8", ExpectedResult = -1)] [TestCase("0.5.7", "0.5.8", ExpectedResult = -1)]
@ -155,6 +165,8 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)] [TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)]
[TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)] [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)]
[TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)] [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)]
[TestCase("1.0-beta+build.001", "1.0-beta+build.001", ExpectedResult = false)] // build metadata must not affect precedence
[TestCase("1.0-beta+build.001", "1.0-beta+build.006", ExpectedResult = false)] // build metadata must not affect precedence
// less than // less than
[TestCase("0.5.7", "0.5.8", ExpectedResult = true)] [TestCase("0.5.7", "0.5.8", ExpectedResult = true)]
@ -191,6 +203,8 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)] [TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)]
[TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)] [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)]
[TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)] [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)]
[TestCase("1.0-beta+build.001", "1.0-beta+build.001", ExpectedResult = false)] // build metadata must not affect precedence
[TestCase("1.0-beta+build.001", "1.0-beta+build.006", ExpectedResult = false)] // build metadata must not affect precedence
// less than // less than
[TestCase("0.5.7", "0.5.8", ExpectedResult = false)] [TestCase("0.5.7", "0.5.8", ExpectedResult = false)]
@ -243,19 +257,19 @@ namespace StardewModdingAPI.Tests.Utilities
} }
/**** /****
** Serialisable ** Serializable
****/ ****/
[Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")]
[TestCase("1.0")] [TestCase("1.0.0")]
public void Serialisable(string versionStr) public void Serializable(string versionStr)
{ {
// act // act
string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr)); string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr));
SemanticVersion after = JsonConvert.DeserializeObject<SemanticVersion>(json); SemanticVersion after = JsonConvert.DeserializeObject<SemanticVersion>(json);
// assert // assert
Assert.IsNotNull(after, "The semantic version after deserialisation is unexpectedly null."); Assert.IsNotNull(after, "The semantic version after deserialization is unexpectedly null.");
Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialisation doesn't match the input version."); Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialization doesn't match the input version.");
} }
/**** /****
@ -278,6 +292,8 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("1.11")] [TestCase("1.11")]
[TestCase("1.2")] [TestCase("1.2")]
[TestCase("1.2.15")] [TestCase("1.2.15")]
[TestCase("1.4.0.1")]
[TestCase("1.4.0.6")]
public void GameVersion(string versionStr) public void GameVersion(string versionStr)
{ {
// act // act
@ -285,7 +301,6 @@ namespace StardewModdingAPI.Tests.Utilities
// assert // assert
Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value."); Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value.");
Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions.");
} }

View File

@ -17,20 +17,17 @@ namespace StardewModdingAPI
/// <summary>The patch version for backwards-compatible bug fixes.</summary> /// <summary>The patch version for backwards-compatible bug fixes.</summary>
int PatchVersion { get; } int PatchVersion { get; }
#if !SMAPI_3_0_STRICT
/// <summary>An optional build tag.</summary>
[Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")]
string Build { get; }
#endif
/// <summary>An optional prerelease tag.</summary> /// <summary>An optional prerelease tag.</summary>
string PrereleaseTag { get; } string PrereleaseTag { get; }
/// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
string BuildMetadata { get; }
/********* /*********
** Accessors ** Accessors
*********/ *********/
/// <summary>Whether this is a pre-release version.</summary> /// <summary>Whether this is a prerelease version.</summary>
bool IsPrerelease(); bool IsPrerelease();
/// <summary>Get whether this version is older than the specified version.</summary> /// <summary>Get whether this version is older than the specified version.</summary>

View File

@ -1,4 +0,0 @@
using System.Reflection;
[assembly: AssemblyTitle("SMAPI.Toolkit.CoreInterfaces")]
[assembly: AssemblyDescription("Provides toolkit interfaces which are available to SMAPI mods.")]

View File

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks> <AssemblyName>SMAPI.Toolkit.CoreInterfaces</AssemblyName>
<RootNamespace>StardewModdingAPI</RootNamespace> <RootNamespace>StardewModdingAPI</RootNamespace>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <Description>Provides toolkit interfaces which are available to SMAPI mods.</Description>
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
<OutputPath>..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces</OutputPath> <OutputPath>..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces</OutputPath>
<DocumentationFile>..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml</DocumentationFile> <DocumentationFile>..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml</DocumentationFile>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <Import Project="..\..\build\common.targets" />
<Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" />
</ItemGroup>
</Project> </Project>

View File

@ -1,3 +1,5 @@
using System;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{ {
/// <summary>Metadata about a mod.</summary> /// <summary>Metadata about a mod.</summary>
@ -9,23 +11,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The mod's unique ID (if known).</summary> /// <summary>The mod's unique ID (if known).</summary>
public string ID { get; set; } public string ID { get; set; }
/// <summary>The main version.</summary> /// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary>
public ModEntryVersionModel Main { get; set; } public ModEntryVersionModel SuggestedUpdate { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
public ModEntryVersionModel UnofficialForBeta { get; set; }
/// <summary>Optional extended data which isn't needed for update checks.</summary> /// <summary>Optional extended data which isn't needed for update checks.</summary>
public ModExtendedMetadataModel Metadata { get; set; } public ModExtendedMetadataModel Metadata { get; set; }
/// <summary>The main version.</summary>
[Obsolete]
public ModEntryVersionModel Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
[Obsolete]
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
[Obsolete]
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
[Obsolete]
public ModEntryVersionModel UnofficialForBeta { get; set; }
/// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary> /// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary>
public bool HasBetaInfo { get; set; } [Obsolete]
public bool? HasBetaInfo { get; set; }
/// <summary>The errors that occurred while fetching update data.</summary> /// <summary>The errors that occurred while fetching update data.</summary>
public string[] Errors { get; set; } = new string[0]; public string[] Errors { get; set; } = new string[0];

View File

@ -28,6 +28,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The mod ID in the Chucklefish mod repo.</summary> /// <summary>The mod ID in the Chucklefish mod repo.</summary>
public int? ChucklefishID { get; set; } public int? ChucklefishID { get; set; }
/// <summary>The mod ID in the CurseForge mod repo.</summary>
public int? CurseForgeID { get; set; }
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
public string CurseForgeKey { get; set; }
/// <summary>The mod ID in the ModDrop mod repo.</summary> /// <summary>The mod ID in the ModDrop mod repo.</summary>
public int? ModDropID { get; set; } public int? ModDropID { get; set; }
@ -40,6 +46,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The custom mod page URL (if applicable).</summary> /// <summary>The custom mod page URL (if applicable).</summary>
public string CustomUrl { get; set; } public string CustomUrl { get; set; }
/// <summary>The main version.</summary>
public ModEntryVersionModel Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
public ModEntryVersionModel UnofficialForBeta { get; set; }
/**** /****
** Stable compatibility ** Stable compatibility
@ -48,13 +65,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public WikiCompatibilityStatus? CompatibilityStatus { get; set; } public WikiCompatibilityStatus? CompatibilityStatus { get; set; }
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatitng.</summary> /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
public string CompatibilitySummary { get; set; } public string CompatibilitySummary { get; set; }
/// <summary>The game or SMAPI version which broke this mod, if applicable.</summary> /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
public string BrokeIn { get; set; } public string BrokeIn { get; set; }
/**** /****
** Beta compatibility ** Beta compatibility
****/ ****/
@ -62,7 +78,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; }
/// <summary>The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatitng.</summary> /// <summary>The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting.</summary>
public string BetaCompatibilitySummary { get; set; } public string BetaCompatibilitySummary { get; set; }
/// <summary>The beta game or SMAPI version which broke this mod, if applicable.</summary> /// <summary>The beta game or SMAPI version which broke this mod, if applicable.</summary>
@ -78,8 +94,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="wiki">The mod metadata from the wiki (if available).</param> /// <param name="wiki">The mod metadata from the wiki (if available).</param>
/// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param> /// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param>
public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) /// <param name="main">The main version.</param>
/// <param name="optional">The latest optional version, if newer than <paramref name="main"/>.</param>
/// <param name="unofficial">The latest unofficial version, if newer than <paramref name="main"/> and <paramref name="optional"/>.</param>
/// <param name="unofficialForBeta">The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</param>
public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta)
{ {
// versions
this.Main = main;
this.Optional = optional;
this.Unofficial = unofficial;
this.UnofficialForBeta = unofficialForBeta;
// wiki data // wiki data
if (wiki != null) if (wiki != null)
{ {
@ -87,6 +113,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
this.Name = wiki.Name.FirstOrDefault(); this.Name = wiki.Name.FirstOrDefault();
this.NexusID = wiki.NexusID; this.NexusID = wiki.NexusID;
this.ChucklefishID = wiki.ChucklefishID; this.ChucklefishID = wiki.ChucklefishID;
this.CurseForgeID = wiki.CurseForgeID;
this.CurseForgeKey = wiki.CurseForgeKey;
this.ModDropID = wiki.ModDropID; this.ModDropID = wiki.ModDropID;
this.GitHubRepo = wiki.GitHubRepo; this.GitHubRepo = wiki.GitHubRepo;
this.CustomSourceUrl = wiki.CustomSourceUrl; this.CustomSourceUrl = wiki.CustomSourceUrl;

View File

@ -1,36 +0,0 @@
using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
/// <summary>Specifies mods whose update-check info to fetch.</summary>
public class ModSearchModel
{
/*********
** Accessors
*********/
/// <summary>The mods for which to find data.</summary>
public ModSearchEntryModel[] Mods { get; set; }
/// <summary>Whether to include extended metadata for each mod.</summary>
public bool IncludeExtendedMetadata { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an empty instance.</summary>
public ModSearchModel()
{
// needed for JSON deserialising
}
/// <summary>Construct an instance.</summary>
/// <param name="mods">The mods to search.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata)
{
this.Mods = mods.ToArray();
this.IncludeExtendedMetadata = includeExtendedMetadata;
}
}
}

View File

@ -12,6 +12,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The namespaced mod update keys (if available).</summary> /// <summary>The namespaced mod update keys (if available).</summary>
public string[] UpdateKeys { get; set; } public string[] UpdateKeys { get; set; }
/// <summary>The mod version installed by the local player. This is used for version mapping in some cases.</summary>
public ISemanticVersion InstalledVersion { get; set; }
/// <summary>Whether the installed version is broken or could not be loaded.</summary>
public bool IsBroken { get; set; }
/********* /*********
** Public methods ** Public methods
@ -19,15 +25,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an empty instance.</summary> /// <summary>Construct an empty instance.</summary>
public ModSearchEntryModel() public ModSearchEntryModel()
{ {
// needed for JSON deserialising // needed for JSON deserializing
} }
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="id">The unique mod ID.</param> /// <param name="id">The unique mod ID.</param>
/// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param>
/// <param name="updateKeys">The namespaced mod update keys (if available).</param> /// <param name="updateKeys">The namespaced mod update keys (if available).</param>
public ModSearchEntryModel(string id, string[] updateKeys) /// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param>
public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false)
{ {
this.ID = id; this.ID = id;
this.InstalledVersion = installedVersion;
this.UpdateKeys = updateKeys ?? new string[0]; this.UpdateKeys = updateKeys ?? new string[0];
} }
} }

View File

@ -0,0 +1,52 @@
using System.Linq;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
/// <summary>Specifies mods whose update-check info to fetch.</summary>
public class ModSearchModel
{
/*********
** Accessors
*********/
/// <summary>The mods for which to find data.</summary>
public ModSearchEntryModel[] Mods { get; set; }
/// <summary>Whether to include extended metadata for each mod.</summary>
public bool IncludeExtendedMetadata { get; set; }
/// <summary>The SMAPI version installed by the player. This is used for version mapping in some cases.</summary>
public ISemanticVersion ApiVersion { get; set; }
/// <summary>The Stardew Valley version installed by the player.</summary>
public ISemanticVersion GameVersion { get; set; }
/// <summary>The OS on which the player plays.</summary>
public Platform? Platform { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an empty instance.</summary>
public ModSearchModel()
{
// needed for JSON deserializing
}
/// <summary>Construct an instance.</summary>
/// <param name="mods">The mods to search.</param>
/// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
/// <param name="gameVersion">The Stardew Valley version installed by the player.</param>
/// <param name="platform">The OS on which the player plays.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata)
{
this.Mods = mods.ToArray();
this.ApiVersion = apiVersion;
this.GameVersion = gameVersion;
this.Platform = platform;
this.IncludeExtendedMetadata = includeExtendedMetadata;
}
}
}

View File

@ -3,7 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using Newtonsoft.Json; using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{ {
@ -37,12 +38,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Get metadata about a set of mods from the web API.</summary> /// <summary>Get metadata about a set of mods from the web API.</summary>
/// <param name="mods">The mod keys for which to fetch the latest version.</param> /// <param name="mods">The mod keys for which to fetch the latest version.</param>
/// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
/// <param name="gameVersion">The Stardew Valley version installed by the player.</param>
/// <param name="platform">The OS on which the player plays.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false)
{ {
return this.Post<ModSearchModel, ModEntryModel[]>( return this.Post<ModSearchModel, ModEntryModel[]>(
$"v{this.Version}/mods", $"v{this.Version}/mods",
new ModSearchModel(mods, includeExtendedMetadata) new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata)
).ToDictionary(p => p.ID); ).ToDictionary(p => p.ID);
} }

View File

@ -93,12 +93,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
string[] warnings = this.GetAttributeAsCsv(node, "data-warnings"); string[] warnings = this.GetAttributeAsCsv(node, "data-warnings");
int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id");
int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id");
int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id");
string curseForgeKey = this.GetAttribute(node, "data-curseforge-key");
int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id"); int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id");
string githubRepo = this.GetAttribute(node, "data-github"); string githubRepo = this.GetAttribute(node, "data-github");
string customSourceUrl = this.GetAttribute(node, "data-custom-source"); string customSourceUrl = this.GetAttribute(node, "data-custom-source");
string customUrl = this.GetAttribute(node, "data-url"); string customUrl = this.GetAttribute(node, "data-url");
string anchor = this.GetAttribute(node, "id"); string anchor = this.GetAttribute(node, "id");
string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for");
string devNote = this.GetAttribute(node, "data-dev-note");
IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions");
IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions");
// parse stable compatibility // parse stable compatibility
WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
@ -127,6 +132,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
} }
} }
// parse links
List<Tuple<Uri, string>> metadataLinks = new List<Tuple<Uri, string>>();
foreach (HtmlNode linkElement in node.Descendants("td").Last().Descendants("a").Skip(1)) // skip anchor link
{
string text = linkElement.InnerText.Trim();
Uri url = new Uri(linkElement.GetAttributeValue("href", ""));
metadataLinks.Add(Tuple.Create(url, text));
}
// yield model // yield model
yield return new WikiModEntry yield return new WikiModEntry
{ {
@ -135,6 +149,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
Author = authors, Author = authors,
NexusID = nexusID, NexusID = nexusID,
ChucklefishID = chucklefishID, ChucklefishID = chucklefishID,
CurseForgeID = curseForgeID,
CurseForgeKey = curseForgeKey,
ModDropID = modDropID, ModDropID = modDropID,
GitHubRepo = githubRepo, GitHubRepo = githubRepo,
CustomSourceUrl = customSourceUrl, CustomSourceUrl = customSourceUrl,
@ -143,6 +159,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
Compatibility = compatibility, Compatibility = compatibility,
BetaCompatibility = betaCompatibility, BetaCompatibility = betaCompatibility,
Warnings = warnings, Warnings = warnings,
MetadataLinks = metadataLinks.ToArray(),
DevNote = devNote,
MapLocalVersions = mapLocalVersions,
MapRemoteVersions = mapRemoteVersions,
Anchor = anchor Anchor = anchor
}; };
} }
@ -207,6 +227,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
return null; return null;
} }
/// <summary>Get an attribute value and parse it as a version mapping.</summary>
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
private IDictionary<string, string> GetAttributeAsVersionMapping(HtmlNode element, string name)
{
// get raw value
string raw = this.GetAttribute(element, name);
if (raw?.Contains("→") != true)
return null;
// parse
// Specified on the wiki in the form "remote version → mapped version; another remote version → mapped version"
IDictionary<string, string> map = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (string pair in raw.Split(';'))
{
string[] versions = pair.Split('→');
if (versions.Length == 2 && !string.IsNullOrWhiteSpace(versions[0]) && !string.IsNullOrWhiteSpace(versions[1]))
map[versions[0].Trim()] = versions[1].Trim();
}
return map;
}
/// <summary>Get the text of an element with the given class name.</summary> /// <summary>Get the text of an element with the given class name.</summary>
/// <param name="container">The metadata container.</param> /// <param name="container">The metadata container.</param>
/// <param name="className">The field name.</param> /// <param name="className">The field name.</param>

View File

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{ {
/// <summary>A mod entry in the wiki list.</summary> /// <summary>A mod entry in the wiki list.</summary>
@ -21,6 +24,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>The mod ID in the Chucklefish mod repo.</summary> /// <summary>The mod ID in the Chucklefish mod repo.</summary>
public int? ChucklefishID { get; set; } public int? ChucklefishID { get; set; }
/// <summary>The mod ID in the CurseForge mod repo.</summary>
public int? CurseForgeID { get; set; }
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
public string CurseForgeKey { get; set; }
/// <summary>The mod ID in the ModDrop mod repo.</summary> /// <summary>The mod ID in the ModDrop mod repo.</summary>
public int? ModDropID { get; set; } public int? ModDropID { get; set; }
@ -48,6 +57,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>The human-readable warnings for players about this mod.</summary> /// <summary>The human-readable warnings for players about this mod.</summary>
public string[] Warnings { get; set; } public string[] Warnings { get; set; }
/// <summary>Extra metadata links (usually for open pull requests).</summary>
public Tuple<Uri, string>[] MetadataLinks { get; set; }
/// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
public string DevNote { get; set; }
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapLocalVersions { get; set; }
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapRemoteVersions { get; set; }
/// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary> /// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
public string Anchor { get; set; } public string Anchor { get; set; }
} }

View File

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using StardewModdingAPI.Toolkit.Utilities;
#if SMAPI_FOR_WINDOWS
using Microsoft.Win32;
#endif
namespace StardewModdingAPI.Toolkit.Framework.GameScanning
{
/// <summary>Finds installed game folders.</summary>
public class GameScanner
{
/*********
** Public methods
*********/
/// <summary>Find all valid Stardew Valley install folders.</summary>
/// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS.</remarks>
public IEnumerable<DirectoryInfo> Scan()
{
// get OS info
Platform platform = EnvironmentUtility.DetectPlatform();
string executableFilename = EnvironmentUtility.GetExecutableName(platform);
// get install paths
IEnumerable<string> paths = this
.GetCustomInstallPaths(platform)
.Concat(this.GetDefaultInstallPaths(platform))
.Select(PathUtilities.NormalizePathSeparators)
.Distinct(StringComparer.InvariantCultureIgnoreCase);
// yield valid folders
foreach (string path in paths)
{
DirectoryInfo folder = new DirectoryInfo(path);
if (folder.Exists && folder.EnumerateFiles(executableFilename).Any())
yield return folder;
}
}
/*********
** Private methods
*********/
/// <summary>The default file paths where Stardew Valley can be installed.</summary>
/// <param name="platform">The target platform.</param>
/// <remarks>Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. </remarks>
private IEnumerable<string> GetDefaultInstallPaths(Platform platform)
{
switch (platform)
{
case Platform.Linux:
case Platform.Mac:
{
string home = Environment.GetEnvironmentVariable("HOME");
// Linux
yield return $"{home}/GOG Games/Stardew Valley/game";
yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley")
? $"{home}/.steam/steam/steamapps/common/Stardew Valley"
: $"{home}/.local/share/Steam/steamapps/common/Stardew Valley";
// Mac
yield return "/Applications/Stardew Valley.app/Contents/MacOS";
yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS";
}
break;
case Platform.Windows:
{
// Windows
foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
{
yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley";
yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
}
// Windows registry
#if SMAPI_FOR_WINDOWS
IDictionary<string, string> registryKeys = new Dictionary<string, string>
{
[@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam
[@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows
};
foreach (var pair in registryKeys)
{
string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value);
if (!string.IsNullOrWhiteSpace(path))
yield return path;
}
// via Steam library path
string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
if (steampath != null)
yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
#endif
}
break;
default:
throw new InvalidOperationException($"Unknown platform '{platform}'.");
}
}
/// <summary>Get the custom install path from the <c>stardewvalley.targets</c> file in the home directory, if any.</summary>
/// <param name="platform">The target platform.</param>
private IEnumerable<string> GetCustomInstallPaths(Platform platform)
{
// get home path
string homePath = Environment.GetEnvironmentVariable(platform == Platform.Windows ? "USERPROFILE" : "HOME");
if (string.IsNullOrWhiteSpace(homePath))
yield break;
// get targets file
FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets"));
if (!file.Exists)
yield break;
// parse file
XElement root;
try
{
using (FileStream stream = file.OpenRead())
root = XElement.Load(stream);
}
catch
{
yield break;
}
// get install path
XElement element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace
if (!string.IsNullOrWhiteSpace(element?.Value))
yield return element.Value.Trim();
}
#if SMAPI_FOR_WINDOWS
/// <summary>Get the value of a key in the Windows HKLM registry.</summary>
/// <param name="key">The full path of the registry key relative to HKLM.</param>
/// <param name="name">The name of the value.</param>
private string GetLocalMachineRegistryValue(string key, string name)
{
RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine;
RegistryKey openKey = localMachine.OpenSubKey(key);
if (openKey == null)
return null;
using (openKey)
return (string)openKey.GetValue(name);
}
/// <summary>Get the value of a key in the Windows HKCU registry.</summary>
/// <param name="key">The full path of the registry key relative to HKCU.</param>
/// <param name="name">The name of the value.</param>
private string GetCurrentUserRegistryValue(string key, string name)
{
RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser;
RegistryKey openKey = currentuser.OpenSubKey(key);
if (openKey == null)
return null;
using (openKey)
return (string)openKey.GetValue(name);
}
#endif
}
}

View File

@ -25,12 +25,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// </remarks> /// </remarks>
public string FormerIDs { get; set; } public string FormerIDs { get; set; }
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>();
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>();
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
public ModWarning SuppressWarnings { get; set; } public ModWarning SuppressWarnings { get; set; }
@ -112,8 +106,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>The method invoked after JSON deserialisation.</summary> /// <summary>The method invoked after JSON deserialization.</summary>
/// <param name="context">The deserialisation context.</param> /// <param name="context">The deserialization context.</param>
[OnDeserialized] [OnDeserialized]
private void OnDeserialized(StreamingContext context) private void OnDeserialized(StreamingContext context)
{ {

View File

@ -22,12 +22,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
public ModWarning SuppressWarnings { get; set; } public ModWarning SuppressWarnings { get; set; }
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapLocalVersions { get; }
/// <summary>Maps remote versions to a semantic version for update checks.</summary>
public IDictionary<string, string> MapRemoteVersions { get; }
/// <summary>The versioned field data.</summary> /// <summary>The versioned field data.</summary>
public ModDataField[] Fields { get; } public ModDataField[] Fields { get; }
@ -44,8 +38,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
this.ID = model.ID; this.ID = model.ID;
this.FormerIDs = model.GetFormerIDs().ToArray(); this.FormerIDs = model.GetFormerIDs().ToArray();
this.SuppressWarnings = model.SuppressWarnings; this.SuppressWarnings = model.SuppressWarnings;
this.MapLocalVersions = new Dictionary<string, string>(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase);
this.MapRemoteVersions = new Dictionary<string, string>(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase);
this.Fields = model.GetFields().ToArray(); this.Fields = model.GetFields().ToArray();
} }
@ -67,29 +59,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
return false; return false;
} }
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The remote version to normalise.</param>
public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
{
return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion)
? new SemanticVersion(newVersion)
: version;
}
/// <summary>Get a semantic remote version for update checks.</summary>
/// <param name="version">The remote version to normalise.</param>
public string GetRemoteVersionForUpdateChecks(string version)
{
// normalise version if possible
if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
version = parsed.ToString();
// fetch remote version
return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion)
? newVersion
: version;
}
/// <summary>Get the possible mod IDs.</summary> /// <summary>Get the possible mod IDs.</summary>
public IEnumerable<string> GetIDs() public IEnumerable<string> GetIDs()
{ {

View File

@ -26,29 +26,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary>
public ISemanticVersion StatusUpperVersion { get; set; } public ISemanticVersion StatusUpperVersion { get; set; }
/*********
** Public methods
*********/
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The remote version to normalise.</param>
public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
{
return this.DataRecord.GetLocalVersionForUpdateChecks(version);
}
/// <summary>Get a semantic remote version for update checks.</summary>
/// <param name="version">The remote version to normalise.</param>
public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version)
{
if (version == null)
return null;
string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString());
return rawVersion != null
? new SemanticVersion(rawVersion)
: version;
}
} }
} }

View File

@ -13,7 +13,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
BrokenCodeLoaded = 1, BrokenCodeLoaded = 1,
/// <summary>The mod affects the save serializer in a way that may make saves unloadable without the mod.</summary> /// <summary>The mod affects the save serializer in a way that may make saves unloadable without the mod.</summary>
ChangesSaveSerialiser = 2, ChangesSaveSerializer = 2,
/// <summary>The mod patches the game in a way that may impact stability.</summary> /// <summary>The mod patches the game in a way that may impact stability.</summary>
PatchesGame = 4, PatchesGame = 4,
@ -21,16 +21,19 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary> /// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
UsesDynamic = 8, UsesDynamic = 8,
/// <summary>The mod references specialised 'unvalided update tick' events which may impact stability.</summary> /// <summary>The mod references specialized 'unvalidated update tick' events which may impact stability.</summary>
UsesUnvalidatedUpdateTick = 16, UsesUnvalidatedUpdateTick = 16,
/// <summary>The mod has no update keys set.</summary> /// <summary>The mod has no update keys set.</summary>
NoUpdateKeys = 32, NoUpdateKeys = 32,
/// <summary>Uses .NET APIs for reading and writing to the console.</summary>
AccessesConsole = 64,
/// <summary>Uses .NET APIs for filesystem access.</summary> /// <summary>Uses .NET APIs for filesystem access.</summary>
AccessesFilesystem = 64, AccessesFilesystem = 128,
/// <summary>Uses .NET APIs for shell or process access.</summary> /// <summary>Uses .NET APIs for shell or process access.</summary>
AccessesShell = 128 AccessesShell = 256
} }
} }

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.ModScanning namespace StardewModdingAPI.Toolkit.Framework.ModScanning
@ -18,14 +18,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>The folder containing the mod's manifest.json.</summary> /// <summary>The folder containing the mod's manifest.json.</summary>
public DirectoryInfo Directory { get; } public DirectoryInfo Directory { get; }
/// <summary>The mod type.</summary>
public ModType Type { get; }
/// <summary>The mod manifest.</summary> /// <summary>The mod manifest.</summary>
public Manifest Manifest { get; } public Manifest Manifest { get; }
/// <summary>The error which occurred parsing the manifest, if any.</summary> /// <summary>The error which occurred parsing the manifest, if any.</summary>
public string ManifestParseError { get; } public ModParseError ManifestParseError { get; set; }
/// <summary>Whether the mod should be loaded by default. This is <c>false</c> if it was found within a folder whose name starts with a dot.</summary> /// <summary>A human-readable message for the <see cref="ManifestParseError"/>, if any.</summary>
public bool ShouldBeLoaded { get; } public string ManifestParseErrorText { get; set; }
/********* /*********
@ -34,16 +37,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="root">The root folder containing mods.</param> /// <param name="root">The root folder containing mods.</param>
/// <param name="directory">The folder containing the mod's manifest.json.</param> /// <param name="directory">The folder containing the mod's manifest.json.</param>
/// <param name="type">The mod type.</param>
/// <param name="manifest">The mod manifest.</param>
public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest)
: this(root, directory, type, manifest, ModParseError.None, null) { }
/// <summary>Construct an instance.</summary>
/// <param name="root">The root folder containing mods.</param>
/// <param name="directory">The folder containing the mod's manifest.json.</param>
/// <param name="type">The mod type.</param>
/// <param name="manifest">The mod manifest.</param> /// <param name="manifest">The mod manifest.</param>
/// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param> /// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param>
/// <param name="shouldBeLoaded">Whether the mod should be loaded by default. This should be <c>false</c> if it was found within a folder whose name starts with a dot.</param> /// <param name="manifestParseErrorText">A human-readable message for the <paramref name="manifestParseError"/>, if any.</param>
public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, ModParseError manifestParseError, string manifestParseErrorText)
{ {
// save info // save info
this.Directory = directory; this.Directory = directory;
this.Type = type;
this.Manifest = manifest; this.Manifest = manifest;
this.ManifestParseError = manifestParseError; this.ManifestParseError = manifestParseError;
this.ShouldBeLoaded = shouldBeLoaded; this.ManifestParseErrorText = manifestParseErrorText;
// set display name // set display name
this.DisplayName = manifest?.Name; this.DisplayName = manifest?.Name;

View File

@ -0,0 +1,24 @@
namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{
/// <summary>Indicates why a mod could not be parsed.</summary>
public enum ModParseError
{
/// <summary>No parse error.</summary>
None,
/// <summary>The folder is empty or contains only ignored files.</summary>
EmptyFolder,
/// <summary>The folder is ignored by convention.</summary>
IgnoredFolder,
/// <summary>The mod's <c>manifest.json</c> could not be parsed.</summary>
ManifestInvalid,
/// <summary>The folder contains non-ignored and non-XNB files, but none of them are <c>manifest.json</c>.</summary>
ManifestMissing,
/// <summary>The folder is an XNB mod, which can't be loaded through SMAPI.</summary>
XnbMod
}
}

View File

@ -2,8 +2,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using StardewModdingAPI.Toolkit.Serialisation; using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialization.Models;
namespace StardewModdingAPI.Toolkit.Framework.ModScanning namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{ {
@ -17,20 +18,32 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
private readonly JsonHelper JsonHelper; private readonly JsonHelper JsonHelper;
/// <summary>A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod.</summary> /// <summary>A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod.</summary>
private readonly HashSet<string> IgnoreFilesystemEntries = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) private readonly HashSet<Regex> IgnoreFilesystemEntries = new HashSet<Regex>
{ {
".DS_Store", // OS metadata files
"mcs", new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager
"Thumbs.db" new Regex(@"^(?:__MACOSX|\._\.DS_Store|\.DS_Store|mcs)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS
new Regex(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows
new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files
// other
new Regex(@"\.(?:bmp|gif|jpeg|jpg|png|psd|tif)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // image files
new Regex(@"\.(?:md|rtf|txt)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // text files
new Regex(@"\.(?:backup|bak|old)$", RegexOptions.Compiled | RegexOptions.IgnoreCase) // backup file
}; };
/// <summary>The extensions for files which an XNB mod may contain. If a mod contains *only* these file extensions, it should be considered an XNB mod.</summary> /// <summary>The extensions for files which an XNB mod may contain. If a mod doesn't have a <c>manifest.json</c> and contains *only* these file extensions, it should be considered an XNB mod.</summary>
private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{ {
".md", // XNB files
".png", ".xgs",
".txt", ".xnb",
".xnb" ".xsb",
".xwb",
// unpacking artifacts
".json",
".yaml"
}; };
@ -52,6 +65,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return this.GetModFolders(root, root); return this.GetModFolders(root, root);
} }
/// <summary>Extract information about all mods in the given folder.</summary>
/// <param name="rootPath">The root folder containing mods. Only the <paramref name="modPath"/> will be searched, but this field allows it to be treated as a potential mod folder of its own.</param>
/// <param name="modPath">The mod path to search.</param>
// /// <param name="tryConsolidateMod">If the folder contains multiple XNB mods, treat them as subfolders of a single mod. This is useful when reading a single mod archive, as opposed to a mods folder.</param>
public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath)
{
return this.GetModFolders(root: new DirectoryInfo(rootPath), folder: new DirectoryInfo(modPath));
}
/// <summary>Extract information from a mod folder.</summary> /// <summary>Extract information from a mod folder.</summary>
/// <param name="root">The root folder containing mods.</param> /// <param name="root">The root folder containing mods.</param>
/// <param name="searchFolder">The folder to search for a mod.</param> /// <param name="searchFolder">The folder to search for a mod.</param>
@ -63,34 +85,40 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
// set appropriate invalid-mod error // set appropriate invalid-mod error
if (manifestFile == null) if (manifestFile == null)
{ {
FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).ToArray(); FileInfo[] files = this.RecursivelyGetRelevantFiles(searchFolder).ToArray();
if (!files.Any()) if (!files.Any())
return new ModFolder(root, searchFolder, null, "it's an empty folder."); return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder.");
if (files.All(file => this.PotentialXnbModExtensions.Contains(file.Extension))) if (files.All(this.IsPotentialXnbFile))
return new ModFolder(root, searchFolder, null, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json."); return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "it contains files, but none of them are manifest.json.");
} }
// read mod info // read mod info
Manifest manifest = null; Manifest manifest = null;
string manifestError = null; ModParseError error = ModParseError.None;
string errorText = null;
{ {
try try
{ {
if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null) if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null)
manifestError = "its manifest is invalid."; {
error = ModParseError.ManifestInvalid;
errorText = "its manifest is invalid.";
}
} }
catch (SParseException ex) catch (SParseException ex)
{ {
manifestError = $"parsing its manifest failed: {ex.Message}"; error = ModParseError.ManifestInvalid;
errorText = $"parsing its manifest failed: {ex.Message}";
} }
catch (Exception ex) catch (Exception ex)
{ {
manifestError = $"parsing its manifest failed:\n{ex}"; error = ModParseError.ManifestInvalid;
errorText = $"parsing its manifest failed:\n{ex}";
} }
} }
// normalise display fields // normalize display fields
if (manifest != null) if (manifest != null)
{ {
manifest.Name = this.StripNewlines(manifest.Name); manifest.Name = this.StripNewlines(manifest.Name);
@ -98,7 +126,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
manifest.Author = this.StripNewlines(manifest.Author); manifest.Author = this.StripNewlines(manifest.Author);
} }
return new ModFolder(root, manifestFile.Directory, manifest, manifestError); // get mod type
ModType type = ModType.Invalid;
if (manifest != null)
{
type = !string.IsNullOrWhiteSpace(manifest.ContentPackFor?.UniqueID)
? ModType.ContentPack
: ModType.Smapi;
}
// build result
return new ModFolder(root, manifestFile.Directory, type, manifest, error, errorText);
} }
@ -108,20 +146,30 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Recursively extract information about all mods in the given folder.</summary> /// <summary>Recursively extract information about all mods in the given folder.</summary>
/// <param name="root">The root mod folder.</param> /// <param name="root">The root mod folder.</param>
/// <param name="folder">The folder to search for mods.</param> /// <param name="folder">The folder to search for mods.</param>
public IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder) private IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder)
{ {
// skip bool isRoot = folder.FullName == root.FullName;
if (folder.FullName != root.FullName && folder.Name.StartsWith("."))
yield return new ModFolder(root, folder, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false);
// recurse into subfolders // skip
else if (this.IsModSearchFolder(root, folder)) if (!isRoot)
{ {
foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) if (folder.Name.StartsWith("."))
{ {
foreach (ModFolder match in this.GetModFolders(root, subfolder)) yield return new ModFolder(root, folder, ModType.Ignored, null, ModParseError.IgnoredFolder, "ignored folder because its name starts with a dot.");
yield return match; yield break;
} }
if (!this.IsRelevant(folder))
yield break;
}
// find mods in subfolders
if (this.IsModSearchFolder(root, folder))
{
IEnumerable<ModFolder> subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub));
if (!isRoot)
subfolders = this.TryConsolidate(root, folder, subfolders.ToArray());
foreach (ModFolder subfolder in subfolders)
yield return subfolder;
} }
// treat as mod folder // treat as mod folder
@ -129,6 +177,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
yield return this.ReadFolder(root, folder); yield return this.ReadFolder(root, folder);
} }
/// <summary>Consolidate adjacent folders into one mod folder, if possible.</summary>
/// <param name="root">The folder containing both parent and subfolders.</param>
/// <param name="parentFolder">The parent folder to consolidate, if possible.</param>
/// <param name="subfolders">The subfolders to consolidate, if possible.</param>
private IEnumerable<ModFolder> TryConsolidate(DirectoryInfo root, DirectoryInfo parentFolder, ModFolder[] subfolders)
{
if (subfolders.Length > 1)
{
// a collection of empty folders
if (subfolders.All(p => p.ManifestParseError == ModParseError.EmptyFolder))
return new[] { new ModFolder(root, parentFolder, ModType.Invalid, null, ModParseError.EmptyFolder, subfolders[0].ManifestParseErrorText) };
// an XNB mod
if (subfolders.All(p => p.Type == ModType.Xnb || p.ManifestParseError == ModParseError.EmptyFolder))
return new[] { new ModFolder(root, parentFolder, ModType.Xnb, null, ModParseError.XnbMod, subfolders[0].ManifestParseErrorText) };
}
return subfolders;
}
/// <summary>Find the manifest for a mod folder.</summary> /// <summary>Find the manifest for a mod folder.</summary>
/// <param name="folder">The folder to search.</param> /// <param name="folder">The folder to search.</param>
private FileInfo FindManifest(DirectoryInfo folder) private FileInfo FindManifest(DirectoryInfo folder)
@ -166,11 +234,41 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return subfolders.Any() && !files.Any(); return subfolders.Any() && !files.Any();
} }
/// <summary>Recursively get all relevant files in a folder based on the result of <see cref="IsRelevant"/>.</summary>
/// <param name="folder">The root folder to search.</param>
private IEnumerable<FileInfo> RecursivelyGetRelevantFiles(DirectoryInfo folder)
{
foreach (FileSystemInfo entry in folder.GetFileSystemInfos())
{
if (!this.IsRelevant(entry))
continue;
if (entry is FileInfo file)
yield return file;
if (entry is DirectoryInfo subfolder)
{
foreach (FileInfo subfolderFile in this.RecursivelyGetRelevantFiles(subfolder))
yield return subfolderFile;
}
}
}
/// <summary>Get whether a file or folder is relevant when deciding how to process a mod folder.</summary> /// <summary>Get whether a file or folder is relevant when deciding how to process a mod folder.</summary>
/// <param name="entry">The file or folder.</param> /// <param name="entry">The file or folder.</param>
private bool IsRelevant(FileSystemInfo entry) private bool IsRelevant(FileSystemInfo entry)
{ {
return !this.IgnoreFilesystemEntries.Contains(entry.Name); return !this.IgnoreFilesystemEntries.Any(p => p.IsMatch(entry.Name));
}
/// <summary>Get whether a file is potentially part of an XNB mod.</summary>
/// <param name="entry">The file.</param>
private bool IsPotentialXnbFile(FileInfo entry)
{
if (!this.IsRelevant(entry))
return true;
return this.PotentialXnbModExtensions.Contains(entry.Extension); // use EndsWith to handle cases like image..png
} }
/// <summary>Strip newlines from a string.</summary> /// <summary>Strip newlines from a string.</summary>

View File

@ -0,0 +1,21 @@
namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{
/// <summary>A general mod type.</summary>
public enum ModType
{
/// <summary>The mod is invalid and its type could not be determined.</summary>
Invalid,
/// <summary>The folder is ignored by convention.</summary>
Ignored,
/// <summary>A mod which uses SMAPI directly.</summary>
Smapi,
/// <summary>A mod which contains files loaded by a SMAPI mod.</summary>
ContentPack,
/// <summary>A legacy mod which replaces game files directly.</summary>
Xnb
}
}

View File

@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <summary>The Chucklefish mod repository.</summary> /// <summary>The Chucklefish mod repository.</summary>
Chucklefish, Chucklefish,
/// <summary>The CurseForge mod repository.</summary>
CurseForge,
/// <summary>A GitHub project containing releases.</summary> /// <summary>A GitHub project containing releases.</summary>
GitHub, GitHub,

View File

@ -3,7 +3,7 @@ using System;
namespace StardewModdingAPI.Toolkit.Framework.UpdateData namespace StardewModdingAPI.Toolkit.Framework.UpdateData
{ {
/// <summary>A namespaced mod ID which uniquely identifies a mod within a mod repository.</summary> /// <summary>A namespaced mod ID which uniquely identifies a mod within a mod repository.</summary>
public class UpdateKey public class UpdateKey : IEquatable<UpdateKey>
{ {
/********* /*********
** Accessors ** Accessors
@ -38,6 +38,12 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
&& !string.IsNullOrWhiteSpace(id); && !string.IsNullOrWhiteSpace(id);
} }
/// <summary>Construct an instance.</summary>
/// <param name="repository">The mod repository containing the mod.</param>
/// <param name="id">The mod ID within the repository.</param>
public UpdateKey(ModRepositoryKey repository, string id)
: this($"{repository}:{id}", repository, id) { }
/// <summary>Parse a raw update key.</summary> /// <summary>Parse a raw update key.</summary>
/// <param name="raw">The raw update key to parse.</param> /// <param name="raw">The raw update key to parse.</param>
public static UpdateKey Parse(string raw) public static UpdateKey Parse(string raw)
@ -69,5 +75,29 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
? $"{this.Repository}:{this.ID}" ? $"{this.Repository}:{this.ID}"
: this.RawText; : this.RawText;
} }
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
public bool Equals(UpdateKey other)
{
return
other != null
&& this.Repository == other.Repository
&& string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object.</param>
public override bool Equals(object obj)
{
return obj is UpdateKey other && this.Equals(other);
}
/// <summary>Serves as the default hash function. </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
return $"{this.Repository}:{this.ID}".ToLower().GetHashCode();
}
} }
} }

View File

@ -2,13 +2,17 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
using StardewModdingAPI.Toolkit.Framework.GameScanning;
using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialization;
[assembly: InternalsVisibleTo("StardewModdingAPI")]
[assembly: InternalsVisibleTo("SMAPI.Web")]
namespace StardewModdingAPI.Toolkit namespace StardewModdingAPI.Toolkit
{ {
/// <summary>A convenience wrapper for the various tools.</summary> /// <summary>A convenience wrapper for the various tools.</summary>
@ -46,6 +50,13 @@ namespace StardewModdingAPI.Toolkit
this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}";
} }
/// <summary>Find valid Stardew Valley install folders.</summary>
/// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS.</remarks>
public IEnumerable<DirectoryInfo> GetGameFolders()
{
return new GameScanner().Scan();
}
/// <summary>Extract mod metadata from the wiki compatibility list.</summary> /// <summary>Extract mod metadata from the wiki compatibility list.</summary>
public async Task<WikiModList> GetWikiCompatibilityListAsync() public async Task<WikiModList> GetWikiCompatibilityListAsync()
{ {
@ -69,6 +80,14 @@ namespace StardewModdingAPI.Toolkit
return new ModScanner(this.JsonHelper).GetModFolders(rootPath); return new ModScanner(this.JsonHelper).GetModFolders(rootPath);
} }
/// <summary>Extract information about all mods in the given folder.</summary>
/// <param name="rootPath">The root folder containing mods. Only the <paramref name="modPath"/> will be searched, but this field allows it to be treated as a potential mod folder of its own.</param>
/// <param name="modPath">The mod path to search.</param>
public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath)
{
return new ModScanner(this.JsonHelper).GetModFolders(rootPath, modPath);
}
/// <summary>Get an update URL for an update key (if valid).</summary> /// <summary>Get an update URL for an update key (if valid).</summary>
/// <param name="updateKey">The update key.</param> /// <param name="updateKey">The update key.</param>
public string GetUpdateUrl(string updateKey) public string GetUpdateUrl(string updateKey)

View File

@ -1,7 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("SMAPI.Toolkit")]
[assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")]
[assembly: InternalsVisibleTo("StardewModdingAPI")]
[assembly: InternalsVisibleTo("StardewModdingAPI.Web")]

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>SMAPI.Toolkit</AssemblyName>
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
<Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description>
<TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
<OutputPath>..\..\bin\$(Configuration)\SMAPI.Toolkit</OutputPath>
<DocumentationFile>..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\SMAPI.Toolkit.xml</DocumentationFile>
<LangVersion>latest</LangVersion>
<PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.16" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
<PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
</ItemGroup>
<Import Project="..\..\build\common.targets" />
</Project>

View File

@ -7,8 +7,7 @@ namespace StardewModdingAPI.Toolkit
/// <remarks> /// <remarks>
/// The implementation is defined by Semantic Version 2.0 (https://semver.org/), with a few deviations: /// The implementation is defined by Semantic Version 2.0 (https://semver.org/), with a few deviations:
/// - short-form "x.y" versions are supported (equivalent to "x.y.0"); /// - short-form "x.y" versions are supported (equivalent to "x.y.0");
/// - hyphens are synonymous with dots in prerelease tags (like "-unofficial.3-pathoschild"); /// - hyphens are synonymous with dots in prerelease tags and build metadata (like "-unofficial.3-pathoschild");
/// - +build suffixes are not supported;
/// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial"). /// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial").
/// </remarks> /// </remarks>
public class SemanticVersion : ISemanticVersion public class SemanticVersion : ISemanticVersion
@ -16,11 +15,11 @@ namespace StardewModdingAPI.Toolkit
/********* /*********
** Fields ** Fields
*********/ *********/
/// <summary>A regex pattern matching a valid prerelease tag.</summary> /// <summary>A regex pattern matching a valid prerelease or build metadata tag.</summary>
internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+";
/// <summary>A regex pattern matching a version within a larger string.</summary> /// <summary>A regex pattern matching a version within a larger string.</summary>
internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>" + SemanticVersion.TagPattern + "))?"; internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>" + SemanticVersion.TagPattern + "))?(?:\\+(?<buildmetadata>" + SemanticVersion.TagPattern + "))?";
/// <summary>A regular expression matching a semantic version string.</summary> /// <summary>A regular expression matching a semantic version string.</summary>
/// <remarks>This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, with deviations to support the Stardew Valley mod conventions (see remarks on <see cref="SemanticVersion"/>).</remarks> /// <remarks>This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, with deviations to support the Stardew Valley mod conventions (see remarks on <see cref="SemanticVersion"/>).</remarks>
@ -42,14 +41,8 @@ namespace StardewModdingAPI.Toolkit
/// <summary>An optional prerelease tag.</summary> /// <summary>An optional prerelease tag.</summary>
public string PrereleaseTag { get; } public string PrereleaseTag { get; }
#if !SMAPI_3_0_STRICT /// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
/// <summary>An optional prerelease tag.</summary> public string BuildMetadata { get; }
[Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")]
public string Build => this.PrereleaseTag;
/// <summary>Whether the version was parsed from the legacy object format.</summary>
public bool IsLegacyFormat { get; }
#endif
/********* /*********
@ -60,20 +53,14 @@ namespace StardewModdingAPI.Toolkit
/// <param name="minor">The minor version incremented for backwards-compatible changes.</param> /// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
/// <param name="patch">The patch version for backwards-compatible fixes.</param> /// <param name="patch">The patch version for backwards-compatible fixes.</param>
/// <param name="prereleaseTag">An optional prerelease tag.</param> /// <param name="prereleaseTag">An optional prerelease tag.</param>
/// <param name="isLegacyFormat">Whether the version was parsed from the legacy object format.</param> /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null, string buildMetadata = null)
#if !SMAPI_3_0_STRICT
, bool isLegacyFormat = false
#endif
)
{ {
this.MajorVersion = major; this.MajorVersion = major;
this.MinorVersion = minor; this.MinorVersion = minor;
this.PatchVersion = patch; this.PatchVersion = patch;
this.PrereleaseTag = this.GetNormalisedTag(prereleaseTag); this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag);
#if !SMAPI_3_0_STRICT this.BuildMetadata = this.GetNormalizedTag(buildMetadata);
this.IsLegacyFormat = isLegacyFormat;
#endif
this.AssertValid(); this.AssertValid();
} }
@ -106,16 +93,17 @@ namespace StardewModdingAPI.Toolkit
if (!match.Success) if (!match.Success)
throw new FormatException($"The input '{version}' isn't a valid semantic version."); throw new FormatException($"The input '{version}' isn't a valid semantic version.");
// initialise // initialize
this.MajorVersion = int.Parse(match.Groups["major"].Value); this.MajorVersion = int.Parse(match.Groups["major"].Value);
this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null;
this.BuildMetadata = match.Groups["buildmetadata"].Success ? this.GetNormalizedTag(match.Groups["buildmetadata"].Value) : null;
this.AssertValid(); this.AssertValid();
} }
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> /// <summary>Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param> /// <param name="other">The version to compare with this instance.</param>
/// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception> /// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception>
public int CompareTo(ISemanticVersion other) public int CompareTo(ISemanticVersion other)
@ -133,7 +121,7 @@ namespace StardewModdingAPI.Toolkit
return other != null && this.CompareTo(other) == 0; return other != null && this.CompareTo(other) == 0;
} }
/// <summary>Whether this is a pre-release version.</summary> /// <summary>Whether this is a prerelease version.</summary>
public bool IsPrerelease() public bool IsPrerelease()
{ {
return !string.IsNullOrWhiteSpace(this.PrereleaseTag); return !string.IsNullOrWhiteSpace(this.PrereleaseTag);
@ -189,16 +177,12 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Get a string representation of the version.</summary> /// <summary>Get a string representation of the version.</summary>
public override string ToString() public override string ToString()
{ {
// version string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}";
string result = this.PatchVersion != 0 if (this.PrereleaseTag != null)
? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" version += $"-{this.PrereleaseTag}";
: $"{this.MajorVersion}.{this.MinorVersion}"; if (this.BuildMetadata != null)
version += $"+{this.BuildMetadata}";
// tag return version;
string tag = this.PrereleaseTag;
if (tag != null)
result += $"-{tag}";
return result;
} }
/// <summary>Parse a version string without throwing an exception if it fails.</summary> /// <summary>Parse a version string without throwing an exception if it fails.</summary>
@ -223,15 +207,15 @@ namespace StardewModdingAPI.Toolkit
/********* /*********
** Private methods ** Private methods
*********/ *********/
/// <summary>Get a normalised build tag.</summary> /// <summary>Get a normalized prerelease or build tag.</summary>
/// <param name="tag">The tag to normalise.</param> /// <param name="tag">The tag to normalize.</param>
private string GetNormalisedTag(string tag) private string GetNormalizedTag(string tag)
{ {
tag = tag?.Trim(); tag = tag?.Trim();
return !string.IsNullOrWhiteSpace(tag) ? tag : null; return !string.IsNullOrWhiteSpace(tag) ? tag : null;
} }
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> /// <summary>Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="otherMajor">The major version to compare with this instance.</param> /// <param name="otherMajor">The major version to compare with this instance.</param>
/// <param name="otherMinor">The minor version to compare with this instance.</param> /// <param name="otherMinor">The minor version to compare with this instance.</param>
/// <param name="otherPatch">The patch version to compare with this instance.</param> /// <param name="otherPatch">The patch version to compare with this instance.</param>
@ -252,7 +236,7 @@ namespace StardewModdingAPI.Toolkit
if (this.PrereleaseTag == otherTag) if (this.PrereleaseTag == otherTag)
return same; return same;
// stable supercedes pre-release // stable supersedes prerelease
bool curIsStable = string.IsNullOrWhiteSpace(this.PrereleaseTag); bool curIsStable = string.IsNullOrWhiteSpace(this.PrereleaseTag);
bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); bool otherIsStable = string.IsNullOrWhiteSpace(otherTag);
if (curIsStable) if (curIsStable)
@ -260,12 +244,12 @@ namespace StardewModdingAPI.Toolkit
if (otherIsStable) if (otherIsStable)
return curOlder; return curOlder;
// compare two pre-release tag values // compare two prerelease tag values
string[] curParts = this.PrereleaseTag.Split('.', '-'); string[] curParts = this.PrereleaseTag.Split('.', '-');
string[] otherParts = otherTag.Split('.', '-'); string[] otherParts = otherTag.Split('.', '-');
for (int i = 0; i < curParts.Length; i++) for (int i = 0; i < curParts.Length; i++)
{ {
// longer prerelease tag supercedes if otherwise equal // longer prerelease tag supersedes if otherwise equal
if (otherParts.Length <= i) if (otherParts.Length <= i)
return curNewer; return curNewer;
@ -300,12 +284,21 @@ namespace StardewModdingAPI.Toolkit
throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative.");
if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0) if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0)
throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero.");
if (this.PrereleaseTag != null) if (this.PrereleaseTag != null)
{ {
if (this.PrereleaseTag.Trim() == "") if (this.PrereleaseTag.Trim() == "")
throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); throw new FormatException($"{this} isn't a valid semantic version. The prerelease tag cannot be a blank string (but may be omitted).");
if (!Regex.IsMatch(this.PrereleaseTag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) if (!Regex.IsMatch(this.PrereleaseTag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase))
throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); throw new FormatException($"{this} isn't a valid semantic version. The prerelease tag is invalid.");
}
if (this.BuildMetadata != null)
{
if (this.BuildMetadata.Trim() == "")
throw new FormatException($"{this} isn't a valid semantic version. The build metadata cannot be a blank string (but may be omitted).");
if (!Regex.IsMatch(this.BuildMetadata, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase))
throw new FormatException($"{this} isn't a valid semantic version. The build metadata is invalid.");
} }
} }
} }

View File

@ -1,10 +1,10 @@
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Serialization.Models;
namespace StardewModdingAPI.Toolkit.Serialisation.Converters namespace StardewModdingAPI.Toolkit.Serialization.Converters
{ {
/// <summary>Handles deserialisation of <see cref="ManifestContentPackFor"/> arrays.</summary> /// <summary>Handles deserialization of <see cref="ManifestContentPackFor"/> arrays.</summary>
public class ManifestContentPackForConverter : JsonConverter public class ManifestContentPackForConverter : JsonConverter
{ {
/********* /*********

View File

@ -2,11 +2,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Serialization.Models;
namespace StardewModdingAPI.Toolkit.Serialisation.Converters namespace StardewModdingAPI.Toolkit.Serialization.Converters
{ {
/// <summary>Handles deserialisation of <see cref="ManifestDependency"/> arrays.</summary> /// <summary>Handles deserialization of <see cref="ManifestDependency"/> arrays.</summary>
internal class ManifestDependencyArrayConverter : JsonConverter internal class ManifestDependencyArrayConverter : JsonConverter
{ {
/********* /*********

View File

@ -2,9 +2,9 @@ using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace StardewModdingAPI.Toolkit.Serialisation.Converters namespace StardewModdingAPI.Toolkit.Serialization.Converters
{ {
/// <summary>Handles deserialisation of <see cref="ISemanticVersion"/>.</summary> /// <summary>Handles deserialization of <see cref="ISemanticVersion"/>.</summary>
internal class SemanticVersionConverter : JsonConverter internal class SemanticVersionConverter : JsonConverter
{ {
/********* /*********
@ -67,20 +67,8 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters
int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion)); int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion));
int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion)); int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion));
string prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag)); string prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag));
#if !SMAPI_3_0_STRICT
if (string.IsNullOrWhiteSpace(prereleaseTag))
{
prereleaseTag = obj.ValueIgnoreCase<string>("Build");
if (prereleaseTag == "0")
prereleaseTag = null; // '0' from incorrect examples in old SMAPI documentation
}
#endif
return new SemanticVersion(major, minor, patch, prereleaseTag return new SemanticVersion(major, minor, patch, prereleaseTag);
#if !SMAPI_3_0_STRICT
, isLegacyFormat: true
#endif
);
} }
/// <summary>Read a JSON string.</summary> /// <summary>Read a JSON string.</summary>

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