Merge branch 'develop' into stable
This commit is contained in:
commit
60b4119577
|
@ -12,12 +12,11 @@ insert_final_newline = true
|
|||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
|
||||
[*.csproj]
|
||||
[*.{csproj,json,nuspec,targets}]
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
[*.csproj]
|
||||
insert_final_newline = false
|
||||
|
||||
##########
|
||||
## C# formatting
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
Do you want to...
|
||||
|
||||
* **Ask for help using SMAPI?**
|
||||
Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375),
|
||||
don't create a GitHub issue.
|
||||
Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375)
|
||||
or [ask on Discord](https://stardewvalleywiki.com/Modding:Community#Discord), don't create a
|
||||
GitHub issue.
|
||||
|
||||
* **Report a bug?**
|
||||
Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375)
|
||||
instead, unless you're sure it's a bug in SMAPI itself.
|
||||
or [ask on Discord](https://stardewvalleywiki.com/Modding:Community#Discord) instead, unless
|
||||
you're sure it's a bug in SMAPI itself.
|
||||
|
||||
* **Submit a pull request?**
|
||||
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://discord.gg/kH55QXP)
|
||||
sure it'll be accepted. Feel free to come chat in [#modding on Discord](https://stardewvalleywiki.com/Modding:Community#Discord)
|
||||
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
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
name: Bug report
|
||||
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:
|
||||
- #modding on Discord: https://stardewvalleywiki.com/Modding:Community#Discord
|
||||
- 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.
|
||||
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. Provide any other details you think might be relevant here.
|
||||
|
||||
**To Reproduce**
|
||||
Exact steps which reproduce the bug, if possible. For example:
|
||||
1. Load save '...'.
|
||||
2. Walk to '....'.
|
||||
3. Click '....'.
|
||||
4. Error occurs.
|
||||
|
||||
**Log file**
|
||||
Upload your SMAPI log to https://log.smapi.io and post a link here.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for SMAPI.
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
GitHub issues are only used for development tasks. Please don't submit feature requests here! Instead, see...
|
||||
|
||||
- #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
|
||||
|
||||
-->
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: General
|
||||
about: Create a ticket about something else.
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
GitHub issues are only used for development tasks. For support and questions, see...
|
||||
|
||||
- #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
|
||||
|
||||
-->
|
|
@ -0,0 +1,5 @@
|
|||
GitHub issues are only used for SMAPI development tasks.
|
||||
|
||||
To get help with SMAPI problems, you can...
|
||||
* [ask on Discord](https://stardewvalleywiki.com/Modding:Community#Discord);
|
||||
* or post in the [SMAPI support thread](https://community.playstarbound.com/threads/108375).
|
|
@ -1,5 +1,5 @@
|
|||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyProduct("SMAPI")]
|
||||
[assembly: AssemblyVersion("2.5.5")]
|
||||
[assembly: AssemblyFileVersion("2.5.5")]
|
||||
[assembly: AssemblyVersion("2.6.0")]
|
||||
[assembly: AssemblyFileVersion("2.6.0")]
|
||||
|
|
|
@ -14,65 +14,78 @@
|
|||
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS</GamePath>
|
||||
<!-- Windows paths -->
|
||||
<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>
|
||||
<GamePath Condition="!Exists('$(GamePath)') AND '$(OS)' == 'Windows_NT'">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath>
|
||||
<GamePath Condition="!Exists('$(GamePath)') AND '$(OS)' == 'Windows_NT'">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath>
|
||||
|
||||
<!--compile constants -->
|
||||
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- add references-->
|
||||
<!-- add common references -->
|
||||
<ItemGroup>
|
||||
<Reference Condition="'$(OS)' == 'Windows_NT' AND '$(MSBuildProjectName)' != 'StardewModdingAPI.Toolkit' AND '$(MSBuildProjectName)' != 'StardewModdingAPI.Toolkit.CoreInterfaces'" Include="System.Management" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- add game references-->
|
||||
<Choose>
|
||||
<When Condition="$(OS) == 'Windows_NT'">
|
||||
<PropertyGroup>
|
||||
<DefineConstants>$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<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>
|
||||
<Reference Include="Netcode" Condition="Exists('$(GamePath)\Netcode.dll')">
|
||||
<HintPath>$(GamePath)\Netcode.dll</HintPath>
|
||||
<Private Condition="'$(MSBuildProjectName)' != 'StardewModdingAPI.Tests'">False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Stardew Valley">
|
||||
<HintPath>$(GamePath)\Stardew Valley.exe</HintPath>
|
||||
<Private Condition="'$(MSBuildProjectName)' != 'StardewModdingAPI.Tests'">False</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>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<When Condition="'$(MSBuildProjectName)' == 'StardewModdingAPI' OR '$(MSBuildProjectName)' == 'StardewModdingAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'StardewModdingAPI.Mods.SaveBackup' OR '$(MSBuildProjectName)' == 'StardewModdingAPI.Tests'">
|
||||
<Choose>
|
||||
<When Condition="$(OS) == 'Windows_NT'">
|
||||
<ItemGroup>
|
||||
<!--XNA framework-->
|
||||
<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>
|
||||
|
||||
<!-- game DLLs -->
|
||||
<Reference Include="Netcode">
|
||||
<HintPath>$(GamePath)\Netcode.dll</HintPath>
|
||||
<Private Condition="'$(MSBuildProjectName)' != 'StardewModdingAPI.Tests'">False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Stardew Valley">
|
||||
<HintPath>$(GamePath)\Stardew Valley.exe</HintPath>
|
||||
<Private Condition="'$(MSBuildProjectName)' != 'StardewModdingAPI.Tests'">False</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>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<ItemGroup>
|
||||
<!-- MonoGame -->
|
||||
<Reference Include="MonoGame.Framework">
|
||||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
</Reference>
|
||||
|
||||
<!-- game DLLs -->
|
||||
<Reference Include="StardewValley">
|
||||
<HintPath>$(GamePath)\StardewValley.exe</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="xTile">
|
||||
<HintPath>$(GamePath)\xTile.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>$(DefineConstants);SMAPI_FOR_UNIX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="MonoGame.Framework">
|
||||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
</Reference>
|
||||
<Reference Include="StardewValley">
|
||||
<HintPath>$(GamePath)\StardewValley.exe</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="xTile">
|
||||
<HintPath>$(GamePath)\xTile.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors -->
|
||||
|
@ -82,21 +95,31 @@
|
|||
|
||||
<!-- copy files into game directory and enable debugging (only in debug mode) -->
|
||||
<Target Name="AfterBuild">
|
||||
<CallTarget Targets="CopySMAPI;CopyDefaultMod" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<CallTarget Targets="CopySMAPI;CopyDefaultMods" Condition="'$(Configuration)' == 'Debug'" />
|
||||
</Target>
|
||||
<Target Name="CopySMAPI" Condition="'$(MSBuildProjectName)' == 'StardewModdingAPI'">
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).metadata.json" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\0Harmony.pdb" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" />
|
||||
</Target>
|
||||
<Target Name="CopyDefaultMod" Condition="'$(MSBuildProjectName)' == 'StardewModdingAPI.Mods.ConsoleCommands'">
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\ConsoleCommands" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\ConsoleCommands" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" />
|
||||
<Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\ConsoleCommands" />
|
||||
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'StardewModdingAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'StardewModdingAPI.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)' == 'StardewModdingAPI.Toolkit' AND '$(Configuration)' == 'Debug' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
|
||||
</Target>
|
||||
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'StardewModdingAPI.Toolkit.CoreInterfaces' AND '$(Configuration)' == 'Debug' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)" />
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
|
||||
</Target>
|
||||
|
||||
<!-- launch SMAPI on debug -->
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
-->
|
||||
<Target Name="AfterBuild">
|
||||
<PropertyGroup>
|
||||
<CompiledSmapiPath>$(SolutionDir)\..\bin\$(Configuration)\SMAPI</CompiledSmapiPath>
|
||||
<CompiledRootPath>$(SolutionDir)\..\bin\$(Configuration)</CompiledRootPath>
|
||||
<CompiledSmapiPath>$(CompiledRootPath)\SMAPI</CompiledSmapiPath>
|
||||
<CompiledToolkitPath>$(CompiledRootPath)\SMAPI.Toolkit\net4.5</CompiledToolkitPath>
|
||||
<PackagePath>$(SolutionDir)\..\bin\Packaged</PackagePath>
|
||||
<PackageInternalPath>$(PackagePath)\internal</PackageInternalPath>
|
||||
</PropertyGroup>
|
||||
|
@ -18,33 +20,50 @@
|
|||
<RemoveDir Directories="$(PackagePath)" />
|
||||
|
||||
<!-- copy installer files -->
|
||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackagePath)\install.exe" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackagePath)\install on Windows.exe" />
|
||||
<Copy SourceFiles="$(TargetDir)\readme.txt" DestinationFiles="$(PackagePath)\README.txt" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(TargetDir)\unix-launcher.sh" DestinationFiles="$(PackageInternalPath)\Mono\StardewModdingAPI" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(TargetDir)\unix-install.sh" DestinationFiles="$(PackagePath)\install.sh" />
|
||||
|
||||
<!-- copy SMAPI files for Mono -->
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackageInternalPath)\Mono\install.exe" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\0Harmony.pdb" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.pdb" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.xml" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.metadata.json" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\steam_appid.txt" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.pdb" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.xml" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackageInternalPath)\Mono" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="@(CompiledMods)" DestinationFolder="$(PackageInternalPath)\Mono\Mods\%(RecursiveDir)" />
|
||||
|
||||
<!-- copy SMAPI files for Windows -->
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\0Harmony.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\0Harmony.pdb" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.pdb" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.xml" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.metadata.json" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\steam_appid.txt" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.pdb" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.xml" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledToolkitPath)\StardewModdingAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackageInternalPath)\Windows" />
|
||||
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="@(CompiledMods)" DestinationFolder="$(PackageInternalPath)\Windows\Mods\%(RecursiveDir)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
<Copy SourceFiles="$(ProjectDir)/package.nuspec" DestinationFolder="$(PackagePath)" />
|
||||
<Copy SourceFiles="$(ProjectDir)/build/smapi.targets" DestinationFiles="$(PackagePath)/build/Pathoschild.Stardew.ModBuildConfig.targets" />
|
||||
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.ModBuildConfig.dll" DestinationFiles="$(PackagePath)/build/StardewModdingAPI.ModBuildConfig.dll" />
|
||||
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.Toolkit.dll" DestinationFiles="$(PackagePath)/build/StardewModdingAPI.Toolkit.dll" />
|
||||
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.Toolkit.CoreInterfaces.dll" DestinationFiles="$(PackagePath)/build/StardewModdingAPI.Toolkit.CoreInterfaces.dll" />
|
||||
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/bin/netstandard1.3/StardewModdingAPI.ModBuildConfig.Analyzer.dll" DestinationFiles="$(PackagePath)/analyzers/dotnet/cs/StardewModdingAPI.ModBuildConfig.Analyzer.dll" />
|
||||
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1" DestinationFiles="$(PackagePath)/tools/install.ps1" />
|
||||
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1" DestinationFiles="$(PackagePath)/tools/uninstall.ps1" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
**SMAPI** is an open-source modding API for [Stardew Valley](http://stardewvalley.net/) that lets
|
||||
you play the game with mods. It's safely installed alongside the game's executable, and doesn't
|
||||
change any of your game files. It serves six main purposes:
|
||||
change any of your game files. It serves eight main purposes:
|
||||
|
||||
1. **Load mods into the game.**
|
||||
_SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't
|
||||
|
@ -25,16 +25,24 @@ change any of your game files. It serves six main purposes:
|
|||
crashing the game, and makes it possible to troubleshoot errors in the game itself that would
|
||||
otherwise show a generic 'program has stopped working' type of message._
|
||||
|
||||
6. **Provide update checks.**
|
||||
6. **Provide update checks.**
|
||||
_SMAPI automatically checks for new versions of your installed mods, and notifies you when any
|
||||
are available._
|
||||
|
||||
7. **Provide compatibility checks.**
|
||||
_SMAPI automatically detects outdated or broken code in mods, and safely disables them before
|
||||
they cause problems._
|
||||
|
||||
8. **Back up your save files.**
|
||||
_SMAPI automatically creates a daily backup of your saves and keeps ten backups, in case
|
||||
something goes wrong. (Via the bundled SaveBackup mod.)_
|
||||
|
||||
## Documentation
|
||||
Have questions? Come [chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other
|
||||
modders!
|
||||
|
||||
### For players
|
||||
* [Modding guides](https://stardewvalleywiki.com/Modding:Index#For_players)
|
||||
* [Player guide](https://stardewvalleywiki.com/Modding:Player_Guide)
|
||||
|
||||
### For modders
|
||||
* [Modding documentation](https://stardewvalleywiki.com/Modding:Index)
|
||||
|
|
|
@ -121,18 +121,30 @@ or you have multiple installs, you can specify the path yourself. There's two wa
|
|||
The configuration will check your custom path first, then fall back to the default paths (so it'll
|
||||
still compile on a different computer).
|
||||
|
||||
### Unit test projects
|
||||
### 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
|
||||
**(upcoming in 2.1)**
|
||||
|
||||
You can use the package in unit test projects too. Its optional unit test mode...
|
||||
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:
|
||||
|
||||
1. disables deploying the project as a mod;
|
||||
2. disables creating a release zip;
|
||||
2. and copies the referenced DLLs into the build output for unit test frameworks.
|
||||
|
||||
To enable it, add this above the first `</PropertyGroup>` in your `.csproj`:
|
||||
```xml
|
||||
<ModUnitTests>True</ModUnitTests>
|
||||
<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
|
||||
|
@ -140,11 +152,11 @@ To enable it, add this above the first `</PropertyGroup>` in your `.csproj`:
|
|||
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...
|
||||
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", "SMAPI001")] // implicit net field conversion
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")]
|
||||
```
|
||||
* for an entire project:
|
||||
1. Expand the _References_ node for the project in Visual Studio.
|
||||
|
@ -153,8 +165,8 @@ You can hide the warnings...
|
|||
|
||||
See below for help with each specific warning.
|
||||
|
||||
### SMAPI001
|
||||
**Implicit net field conversion:**
|
||||
### 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.
|
||||
|
@ -163,11 +175,11 @@ Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplaye
|
|||
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` will be true for a null value in some cases.
|
||||
`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 [SMAPI002](#smapi002) warning for
|
||||
`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#
|
||||
|
@ -185,17 +197,17 @@ Suggested fix:
|
|||
if (item != null && item.category.Value == 0)
|
||||
```
|
||||
|
||||
### SMAPI002
|
||||
**Avoid net fields when possible:**
|
||||
### 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 [SMAPI001](#smapi001)). This
|
||||
field has an equivalent non-net property that avoids those issues.
|
||||
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.
|
||||
|
||||
### SMAPI003
|
||||
**Avoid obsolete fields:**
|
||||
### 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.
|
||||
|
|
|
@ -1,21 +1,103 @@
|
|||
# Release notes
|
||||
<!--
|
||||
## 2.6 alpha
|
||||
## 2.6
|
||||
* For players:
|
||||
* Added support for Stardew Valley 1.3+; no longer compatible with earlier versions.
|
||||
* Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags.
|
||||
* Fixed SMAPI update checks not showing newer beta versions when using a beta version.
|
||||
* Updated for Stardew Valley 1.3.
|
||||
* Added automatic save backups.
|
||||
* Improved update checks:
|
||||
* added beta update channel;
|
||||
* added update alerts for incompatible mods with an unofficial update on the wiki;
|
||||
* added update alerts for optional files on Nexus;
|
||||
* added console warning for mods which don't have update checks configured;
|
||||
* added more visible prompt in beta channel for SMAPI updates;
|
||||
* fixed mod update checks failing if a mod only has prerelease versions on GitHub;
|
||||
* fixed Nexus mod update alerts not showing HTTPS links.
|
||||
* Improved mod warnings in the console.
|
||||
* Improved error when game can't start audio.
|
||||
* Improved the Console Commands mod:
|
||||
* 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 normalising the season value.
|
||||
* 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.
|
||||
* Fixed `SEHException` errors for some players.
|
||||
* Fixed performance issues for some players.
|
||||
* Fixed default color scheme on Mac or in PowerShell (configurable via `StardewModdingAPI.config.json`).
|
||||
* Fixed installer error on Linux/Mac in some cases.
|
||||
* Fixed installer not finding some game paths or showing duplicate paths.
|
||||
* Fixed installer not removing some SMAPI files.
|
||||
* Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!)
|
||||
* Fixed abort-retry loop if a mod crashed when intercepting assets during startup.
|
||||
* Fixed some mods failing if the player name is blank.
|
||||
* Fixed errors when a mod references a missing assembly.
|
||||
* Fixed `AssemblyResolutionException` errors in rare cases.
|
||||
* Renamed `install.exe` to `install on Windows.exe` to avoid confusion.
|
||||
* Updated compatibility list.
|
||||
|
||||
* For the web UI:
|
||||
* Added option to download SMAPI from Nexus.
|
||||
* Added log parser redesign that should be more intuitive.
|
||||
* Added log parser option to view raw log.
|
||||
* Changed log parser filters to show `DEBUG` messages by default.
|
||||
* Fixed design on smaller screens.
|
||||
* Fixed log parser issue when content packs have no description.
|
||||
* Fixed log parser mangling crossplatform paths in some cases.
|
||||
* Fixed `smapi.io/install` not linking to a useful page.
|
||||
|
||||
* For modders:
|
||||
* Added code analysis to mod build config package to flag common issues as warnings.
|
||||
* Dropped some deprecated APIs.
|
||||
* Fixed assets loaded by temporary content managers not being editable.
|
||||
* Fixed issue where assets didn't reload correctly when the player switches language.
|
||||
* Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input.
|
||||
* Added code analysis in the NuGet package to flag common issues as warnings.
|
||||
* Replaced `LocationEvents` to support multiplayer:
|
||||
* now raised for all locations;
|
||||
* now includes added/removed building interiors;
|
||||
* each event now provides a list of added/removed values;
|
||||
* added buildings-changed event.
|
||||
* Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags.
|
||||
* Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows.
|
||||
* Added `semanticVersion.IsPrerelease()` method.
|
||||
* Added support for launching multiple instances transparently. This removes the former `--log-path` command-line argument.
|
||||
* Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map.
|
||||
* Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.)
|
||||
* Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`.
|
||||
* Added absolute pixels to `ICursorPosition`.
|
||||
* Added support for reading/writing `ISemanticVersion` to JSON.
|
||||
* Added support for reloading NPC schedules through the content API.
|
||||
* Reimplemented the content API so it works more reliably in many edge cases.
|
||||
* Reimplemented input suppression to work more consistently in many cases.
|
||||
* The order of update keys now affects which URL players see in update alerts.
|
||||
* Fixed assets loaded by temporary content managers not being editable by mods.
|
||||
* Fixed assets not reloaded consistently when the player switches language.
|
||||
* Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`).
|
||||
* Fixed error if a mod translation file is empty.
|
||||
* Fixed input suppression not working consistently for clicks.
|
||||
* Fixed console input not saved to the log.
|
||||
* Fixed `Context.IsPlayerFree` being false during festivals.
|
||||
* Fixed `helper.ModRegistry.GetApi` errors not always mentioning which interface caused the issue.
|
||||
* Fixed console commands being invoked asynchronously.
|
||||
* Fixed mods able to intercept other mods' assets via the internal asset keys.
|
||||
* Fixed mods able to indirectly change other mods' data through shared content caches.
|
||||
* Fixed `SemanticVersion` allowing invalid versions in some cases.
|
||||
* **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)):
|
||||
* Dropped some deprecated APIs.
|
||||
* `LocationEvents` have been rewritten.
|
||||
* Mods can't intercept chatbox input.
|
||||
* Mod IDs should only contain letters, numbers, hyphens, dots, and underscores. That allows their use in many contexts like URLs. This restriction is now enforced. (In regex form: `^[a-zA-Z0-9_.-]+$`.)
|
||||
|
||||
* For SMAPI developers:
|
||||
* Added prerelease versions to the mod update-check API response where available (GitHub only).
|
||||
* Added support for beta releases on the home page.
|
||||
-->
|
||||
* Added more consistent crossplatform handling, including MacOS detection.
|
||||
* Added beta update channel.
|
||||
* Added optional mod metadata to the web API (including Nexus info, wiki metadata, etc).
|
||||
* Added early prototype of SMAPI 3.0 events via `helper.Events`.
|
||||
* Added early prototype of mod handler toolkit.
|
||||
* Added Harmony for SMAPI's internal use.
|
||||
* Added metadata dump option in `StardewModdingAPI.config.json` for troubleshooting some cases.
|
||||
* Added more stylish pufferchick on the home page.
|
||||
* Rewrote update checks:
|
||||
* Moved most logic into the web API.
|
||||
* Changed web API to require mod IDs.
|
||||
* Changed web API to also fetch metadata from SMAPI's internal mod DB and the wiki.
|
||||
* Rewrote world/player state tracking. The new implementation is much more efficient than previous method, uses net field events where available, and lays the groundwork for more advanced events in SMAPI 3.0.
|
||||
* Split mod DB out of `StardewModdingAPI.config.json` into its own file.
|
||||
* Updated to Mono.Cecil 0.10.
|
||||
|
||||
## 2.5.5
|
||||
* For players:
|
||||
|
@ -215,7 +297,7 @@
|
|||
|
||||
* **New features for modders**
|
||||
SMAPI 2.0 adds several features to enable new kinds of mods (see
|
||||
[API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)).
|
||||
[API documentation](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs)).
|
||||
|
||||
The **content API** lets you edit, inject, and reload XNB data loaded by the game at any time. This lets SMAPI mods do
|
||||
anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g.
|
||||
|
@ -338,8 +420,8 @@ For players:
|
|||
* Updated mod compatibility list.
|
||||
|
||||
For modders:
|
||||
* Added `SDate` utility for in-game date calculations (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)).
|
||||
* Added support for minimum dependency versions in `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)).
|
||||
* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Utilities#Dates)).
|
||||
* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)).
|
||||
* Added more useful logging when loading mods.
|
||||
* Added a `ModID` property to all mod helpers for extension methods.
|
||||
* Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code.
|
||||
|
@ -371,8 +453,8 @@ For players:
|
|||
* Updated mod compatibility list.
|
||||
|
||||
For modders:
|
||||
* You can now add dependencies to `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)).
|
||||
* You can now translate your mod (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)).
|
||||
* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)).
|
||||
* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation)).
|
||||
* You can now load unpacked `.tbin` files from your mod folder through the content API.
|
||||
* 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>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
@ -44,7 +44,7 @@ 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](http://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms)
|
||||
[crossplatforming info](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_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
|
||||
|
@ -78,8 +78,9 @@ on the wiki for the first-time setup.
|
|||
Mono.Cecil.dll
|
||||
Newtonsoft.Json.dll
|
||||
StardewModdingAPI
|
||||
StardewModdingAPI.AssemblyRewriters.dll
|
||||
StardewModdingAPI.config.json
|
||||
StardewModdingAPI.Internal.dll
|
||||
StardewModdingAPI.metadata.json
|
||||
StardewModdingAPI.exe
|
||||
StardewModdingAPI.pdb
|
||||
StardewModdingAPI.xml
|
||||
|
@ -91,8 +92,9 @@ on the wiki for the first-time setup.
|
|||
Mods/*
|
||||
Mono.Cecil.dll
|
||||
Newtonsoft.Json.dll
|
||||
StardewModdingAPI.AssemblyRewriters.dll
|
||||
StardewModdingAPI.config.json
|
||||
StardewModdingAPI.Internal.dll
|
||||
StardewModdingAPI.metadata.json
|
||||
StardewModdingAPI.exe
|
||||
StardewModdingAPI.pdb
|
||||
StardewModdingAPI.xml
|
||||
|
@ -135,7 +137,6 @@ change without warning.
|
|||
|
||||
argument | purpose
|
||||
-------- | -------
|
||||
`--log-path "path"` | The relative or absolute path of the log file SMAPI should write.
|
||||
`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.)
|
||||
|
||||
### Compile flags
|
||||
|
@ -157,48 +158,66 @@ persisted in a compressed form to Pastebin.
|
|||
|
||||
The log parser lives at https://log.smapi.io.
|
||||
|
||||
### Mods API
|
||||
The mods API provides version info for mods hosted by Chucklefish, GitHub, or Nexus Mods. It's used
|
||||
by SMAPI to perform update checks. The `{version}` URL token is the version of SMAPI making the
|
||||
request; it doesn't do anything currently, but lets us version breaking changes if needed.
|
||||
### 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.
|
||||
|
||||
Each mod is identified by a repository key and unique identifier (like `nexus:541`). The following
|
||||
repositories are supported:
|
||||
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.
|
||||
|
||||
key | repository
|
||||
------------- | ----------
|
||||
`chucklefish` | A mod page on the [Chucklefish mod site](https://community.playstarbound.com/resources/categories/22), identified by the mod ID in the page URL.
|
||||
`github` | A repository on [GitHub](https://github.com), identified by its owner and repository name (like `Zoryn4163/SMAPI-Mods`). This checks the version of the latest repository release.
|
||||
`nexus` | A mod page on [Nexus Mods](https://www.nexusmods.com/stardewvalley), identified by the mod ID in the page URL.
|
||||
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 accepts either `GET` or `POST` for convenience:
|
||||
> ```
|
||||
>GET https://api.smapi.io/v2.0/mods?modKeys=nexus:541,chucklefish:4228
|
||||
>```
|
||||
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.
|
||||
|
||||
>```
|
||||
>POST https://api.smapi.io/v2.0/mods
|
||||
>{
|
||||
> "ModKeys": [ "nexus:541", "chucklefish:4228" ]
|
||||
>}
|
||||
>```
|
||||
|
||||
It returns a response like this:
|
||||
>```
|
||||
>{
|
||||
> "chucklefish:4228": {
|
||||
> "name": "Entoarox Framework",
|
||||
> "version": "1.8.0",
|
||||
> "url": "https://community.playstarbound.com/resources/4228"
|
||||
> },
|
||||
> "nexus:541": {
|
||||
> "name": "Lookup Anything",
|
||||
> "version": "1.16",
|
||||
> "url": "http://www.nexusmods.com/stardewvalley/mods/541"
|
||||
> }
|
||||
>}
|
||||
>```
|
||||
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
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyTitle("SMAPI.AssemblyRewriters")]
|
||||
[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")]
|
|
@ -1,37 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StardewModdingAPI.Common.Models
|
||||
{
|
||||
/// <summary>Specifies mods whose update-check info to fetch.</summary>
|
||||
internal class ModSearchModel
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The namespaced mod keys to search.</summary>
|
||||
public string[] ModKeys { get; set; }
|
||||
|
||||
/// <summary>Whether to allow non-semantic versions, instead of returning an error for those.</summary>
|
||||
public bool AllowInvalidVersions { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an empty instance.</summary>
|
||||
public ModSearchModel()
|
||||
{
|
||||
// needed for JSON deserialising
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="modKeys">The namespaced mod keys to search.</param>
|
||||
/// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param>
|
||||
public ModSearchModel(IEnumerable<string> modKeys, bool allowInvalidVersions)
|
||||
{
|
||||
this.ModKeys = modKeys.ToArray();
|
||||
this.AllowInvalidVersions = allowInvalidVersions;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StardewModdingAPI.Common
|
||||
{
|
||||
/// <summary>A low-level implementation of a semantic version with an optional release tag.</summary>
|
||||
/// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks>
|
||||
internal class SemanticVersionImpl
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The major version incremented for major API changes.</summary>
|
||||
public int Major { get; }
|
||||
|
||||
/// <summary>The minor version incremented for backwards-compatible changes.</summary>
|
||||
public int Minor { get; }
|
||||
|
||||
/// <summary>The patch version for backwards-compatible bug fixes.</summary>
|
||||
public int Patch { get; }
|
||||
|
||||
/// <summary>An optional prerelease tag.</summary>
|
||||
public string Tag { get; }
|
||||
|
||||
/// <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>(?>[a-z0-9]+[\-\.]?)+))?";
|
||||
|
||||
/// <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 three important deviations intended to support Stardew Valley mod conventions:
|
||||
/// - allows short-form "x.y" versions;
|
||||
/// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3");
|
||||
/// - doesn't allow '+build' suffixes.
|
||||
/// </remarks>
|
||||
internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture);
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="major">The major version incremented for major API changes.</param>
|
||||
/// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
|
||||
/// <param name="patch">The patch version for backwards-compatible bug fixes.</param>
|
||||
/// <param name="tag">An optional prerelease tag.</param>
|
||||
public SemanticVersionImpl(int major, int minor, int patch, string tag = null)
|
||||
{
|
||||
this.Major = major;
|
||||
this.Minor = minor;
|
||||
this.Patch = patch;
|
||||
this.Tag = this.GetNormalisedTag(tag);
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="version">The assembly version.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception>
|
||||
public SemanticVersionImpl(Version version)
|
||||
{
|
||||
if (version == null)
|
||||
throw new ArgumentNullException(nameof(version), "The input version can't be null.");
|
||||
|
||||
this.Major = version.Major;
|
||||
this.Minor = version.Minor;
|
||||
this.Patch = version.Build;
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="version">The semantic version string.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception>
|
||||
/// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception>
|
||||
public SemanticVersionImpl(string version)
|
||||
{
|
||||
// parse
|
||||
if (version == null)
|
||||
throw new ArgumentNullException(nameof(version), "The input version string can't be null.");
|
||||
var match = SemanticVersionImpl.Regex.Match(version.Trim());
|
||||
if (!match.Success)
|
||||
throw new FormatException($"The input '{version}' isn't a valid semantic version.");
|
||||
|
||||
// initialise
|
||||
this.Major = int.Parse(match.Groups["major"].Value);
|
||||
this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
|
||||
this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
|
||||
this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : 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>
|
||||
/// <param name="other">The version to compare with this instance.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception>
|
||||
public int CompareTo(SemanticVersionImpl other)
|
||||
{
|
||||
if (other == null)
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag);
|
||||
}
|
||||
|
||||
|
||||
/// <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>
|
||||
/// <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="otherPatch">The patch version to compare with this instance.</param>
|
||||
/// <param name="otherTag">The prerelease tag to compare with this instance.</param>
|
||||
public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag)
|
||||
{
|
||||
const int same = 0;
|
||||
const int curNewer = 1;
|
||||
const int curOlder = -1;
|
||||
|
||||
// compare stable versions
|
||||
if (this.Major != otherMajor)
|
||||
return this.Major.CompareTo(otherMajor);
|
||||
if (this.Minor != otherMinor)
|
||||
return this.Minor.CompareTo(otherMinor);
|
||||
if (this.Patch != otherPatch)
|
||||
return this.Patch.CompareTo(otherPatch);
|
||||
if (this.Tag == otherTag)
|
||||
return same;
|
||||
|
||||
// stable supercedes pre-release
|
||||
bool curIsStable = string.IsNullOrWhiteSpace(this.Tag);
|
||||
bool otherIsStable = string.IsNullOrWhiteSpace(otherTag);
|
||||
if (curIsStable)
|
||||
return curNewer;
|
||||
if (otherIsStable)
|
||||
return curOlder;
|
||||
|
||||
// compare two pre-release tag values
|
||||
string[] curParts = this.Tag.Split('.', '-');
|
||||
string[] otherParts = otherTag.Split('.', '-');
|
||||
for (int i = 0; i < curParts.Length; i++)
|
||||
{
|
||||
// longer prerelease tag supercedes if otherwise equal
|
||||
if (otherParts.Length <= i)
|
||||
return curNewer;
|
||||
|
||||
// compare if different
|
||||
if (curParts[i] != otherParts[i])
|
||||
{
|
||||
// compare numerically if possible
|
||||
{
|
||||
if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum))
|
||||
return curNum.CompareTo(otherNum);
|
||||
}
|
||||
|
||||
// else compare lexically
|
||||
return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback (this should never happen)
|
||||
return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Get a string representation of the version.</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
// version
|
||||
string result = this.Patch != 0
|
||||
? $"{this.Major}.{this.Minor}.{this.Patch}"
|
||||
: $"{this.Major}.{this.Minor}";
|
||||
|
||||
// tag
|
||||
string tag = this.Tag;
|
||||
if (tag != null)
|
||||
result += $"-{tag}";
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Parse a version string without throwing an exception if it fails.</summary>
|
||||
/// <param name="version">The version string.</param>
|
||||
/// <param name="parsed">The parsed representation.</param>
|
||||
/// <returns>Returns whether parsing the version succeeded.</returns>
|
||||
internal static bool TryParse(string version, out SemanticVersionImpl parsed)
|
||||
{
|
||||
try
|
||||
{
|
||||
parsed = new SemanticVersionImpl(version);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
parsed = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get a normalised build tag.</summary>
|
||||
/// <param name="tag">The tag to normalise.</param>
|
||||
private string GetNormalisedTag(string tag)
|
||||
{
|
||||
tag = tag?.Trim();
|
||||
return !string.IsNullOrWhiteSpace(tag) ? tag : null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
<HasSharedItems>true</HasSharedItems>
|
||||
<SharedGUID>2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc</SharedGUID>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<Import_RootNamespace>StardewModdingAPI.Common</Import_RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\ModSeachModel.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\ModInfoModel.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)SemanticVersionImpl.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,12 +0,0 @@
|
|||
namespace StardewModdingApi.Installer.Enums
|
||||
{
|
||||
/// <summary>The game's platform version.</summary>
|
||||
internal enum Platform
|
||||
{
|
||||
/// <summary>The Linux/Mac version of the game.</summary>
|
||||
Mono,
|
||||
|
||||
/// <summary>The Windows version of the game.</summary>
|
||||
Windows
|
||||
}
|
||||
}
|
|
@ -7,7 +7,8 @@ using System.Reflection;
|
|||
using System.Threading;
|
||||
using Microsoft.Win32;
|
||||
using StardewModdingApi.Installer.Enums;
|
||||
using StardewModdingAPI.Common;
|
||||
using StardewModdingAPI.Internal;
|
||||
using StardewModdingAPI.Internal.ConsoleWriting;
|
||||
|
||||
namespace StardewModdingApi.Installer
|
||||
{
|
||||
|
@ -17,6 +18,15 @@ namespace StardewModdingApi.Installer
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The name of the installer file in the package.</summary>
|
||||
private readonly string InstallerFileName = "install.exe";
|
||||
|
||||
/// <summary>Mod files which shouldn't be deleted when deploying bundled mods (mod folder name => file names).</summary>
|
||||
private readonly IDictionary<string, HashSet<string>> ProtectBundledFiles = new Dictionary<string, HashSet<string>>(StringComparer.InvariantCultureIgnoreCase)
|
||||
{
|
||||
["SaveBackup"] = new HashSet<string>(new[] { "backups", "config.json" }, StringComparer.InvariantCultureIgnoreCase)
|
||||
};
|
||||
|
||||
/// <summary>The <see cref="Environment.OSVersion"/> value that represents Windows 7.</summary>
|
||||
private readonly Version Windows7Version = new Version(6, 1);
|
||||
|
||||
|
@ -27,7 +37,8 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
switch (platform)
|
||||
{
|
||||
case Platform.Mono:
|
||||
case Platform.Linux:
|
||||
case Platform.Mac:
|
||||
{
|
||||
string home = Environment.GetEnvironmentVariable("HOME");
|
||||
|
||||
|
@ -61,6 +72,11 @@ namespace StardewModdingApi.Installer
|
|||
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;
|
||||
|
||||
|
@ -77,12 +93,20 @@ namespace StardewModdingApi.Installer
|
|||
string GetInstallPath(string path) => Path.Combine(installDir.FullName, path);
|
||||
|
||||
// common
|
||||
yield return GetInstallPath("0Harmony.dll");
|
||||
yield return GetInstallPath("0Harmony.pdb");
|
||||
yield return GetInstallPath("Mono.Cecil.dll");
|
||||
yield return GetInstallPath("Newtonsoft.Json.dll");
|
||||
yield return GetInstallPath("StardewModdingAPI.exe");
|
||||
yield return GetInstallPath("StardewModdingAPI.config.json");
|
||||
yield return GetInstallPath("StardewModdingAPI.data.json");
|
||||
yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll");
|
||||
yield return GetInstallPath("StardewModdingAPI.metadata.json");
|
||||
yield return GetInstallPath("StardewModdingAPI.Toolkit.dll");
|
||||
yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb");
|
||||
yield return GetInstallPath("StardewModdingAPI.Toolkit.xml");
|
||||
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll");
|
||||
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb");
|
||||
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml");
|
||||
yield return GetInstallPath("StardewModdingAPI.xml");
|
||||
yield return GetInstallPath("System.ValueTuple.dll");
|
||||
yield return GetInstallPath("steam_appid.txt");
|
||||
|
||||
|
@ -101,6 +125,7 @@ namespace StardewModdingApi.Installer
|
|||
yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands)
|
||||
yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8
|
||||
yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4
|
||||
yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5
|
||||
if (modsDir.Exists)
|
||||
{
|
||||
foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories())
|
||||
|
@ -109,24 +134,30 @@ namespace StardewModdingApi.Installer
|
|||
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files
|
||||
}
|
||||
|
||||
/// <summary>Whether the current console supports color formatting.</summary>
|
||||
private static readonly bool ConsoleSupportsColor = InteractiveInstaller.GetConsoleSupportsColor();
|
||||
/// <summary>Handles writing color-coded text to the console.</summary>
|
||||
private readonly ColorfulConsoleWriter ConsoleWriter;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public InteractiveInstaller()
|
||||
{
|
||||
this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.AutoDetect);
|
||||
}
|
||||
|
||||
/// <summary>Run the install or uninstall script.</summary>
|
||||
/// <param name="args">The command line arguments.</param>
|
||||
/// <remarks>
|
||||
/// Initialisation flow:
|
||||
/// 1. Collect information (mainly OS and install path) and validate it.
|
||||
/// 2. Ask the user whether to install or uninstall.
|
||||
///
|
||||
///
|
||||
/// Uninstall logic:
|
||||
/// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup.
|
||||
/// 2. Delete all files and folders in the game directory matching one of the values returned by <see cref="GetUninstallPaths"/>.
|
||||
///
|
||||
///
|
||||
/// Install flow:
|
||||
/// 1. Run the uninstall flow.
|
||||
/// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory.
|
||||
|
@ -140,10 +171,19 @@ namespace StardewModdingApi.Installer
|
|||
/****
|
||||
** Get platform & set window title
|
||||
****/
|
||||
Platform platform = this.DetectPlatform();
|
||||
Console.Title = $"SMAPI {new SemanticVersionImpl(this.GetType().Assembly.GetName().Version)} installer on {platform}";
|
||||
Platform platform = EnvironmentUtility.DetectPlatform();
|
||||
Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}";
|
||||
Console.WriteLine();
|
||||
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
if (platform == Platform.Linux || platform == Platform.Mac)
|
||||
{
|
||||
this.PrintError($"This is the installer for Windows. Run the 'install on {platform}.{(platform == Platform.Linux ? "sh" : "command")}' file instead.");
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
/****
|
||||
** read command-line arguments
|
||||
****/
|
||||
|
@ -178,18 +218,20 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// get folders
|
||||
DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.ToString()));
|
||||
DirectoryInfo packageDir = platform.IsMono()
|
||||
? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono
|
||||
: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows"));
|
||||
DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods"));
|
||||
var paths = new
|
||||
{
|
||||
executable = Path.Combine(installDir.FullName, platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe"),
|
||||
executable = Path.Combine(installDir.FullName, EnvironmentUtility.GetExecutableName(platform)),
|
||||
unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"),
|
||||
unixLauncher = Path.Combine(installDir.FullName, "StardewValley"),
|
||||
unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original")
|
||||
};
|
||||
|
||||
// show output
|
||||
Console.WriteLine($"Your game folder: {installDir}.");
|
||||
this.PrintInfo($"Your game folder: {installDir}.");
|
||||
|
||||
/****
|
||||
** validate assumptions
|
||||
|
@ -198,7 +240,7 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip")
|
||||
? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder."
|
||||
: $"The 'internal/{platform}' package folder is missing (should be at {packageDir})."
|
||||
: $"The 'internal/{packageDir.Name}' package folder is missing (should be at {packageDir})."
|
||||
);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
|
@ -226,7 +268,7 @@ namespace StardewModdingApi.Installer
|
|||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
if (!this.HasXNA(platform))
|
||||
if (!this.HasXna(platform))
|
||||
{
|
||||
this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup.");
|
||||
Console.ReadLine();
|
||||
|
@ -247,9 +289,9 @@ namespace StardewModdingApi.Installer
|
|||
action = ScriptAction.Uninstall;
|
||||
else
|
||||
{
|
||||
Console.WriteLine("You can....");
|
||||
Console.WriteLine("[1] Install SMAPI.");
|
||||
Console.WriteLine("[2] Uninstall SMAPI.");
|
||||
this.PrintInfo("You can....");
|
||||
this.PrintInfo("[1] Install SMAPI.");
|
||||
this.PrintInfo("[2] Uninstall SMAPI.");
|
||||
Console.WriteLine();
|
||||
|
||||
string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2");
|
||||
|
@ -271,7 +313,7 @@ namespace StardewModdingApi.Installer
|
|||
** Always uninstall old files
|
||||
****/
|
||||
// restore game launcher
|
||||
if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup))
|
||||
if (platform.IsMono() && File.Exists(paths.unixLauncherBackup))
|
||||
{
|
||||
this.PrintDebug("Removing SMAPI launcher...");
|
||||
this.InteractivelyDelete(paths.unixLauncher);
|
||||
|
@ -298,19 +340,25 @@ namespace StardewModdingApi.Installer
|
|||
this.PrintDebug("Adding SMAPI files...");
|
||||
foreach (FileInfo sourceFile in packageDir.EnumerateFiles().Where(this.ShouldCopyFile))
|
||||
{
|
||||
if (sourceFile.Name == this.InstallerFileName)
|
||||
continue;
|
||||
|
||||
string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
|
||||
this.InteractivelyDelete(targetPath);
|
||||
sourceFile.CopyTo(targetPath);
|
||||
}
|
||||
|
||||
// replace mod launcher (if possible)
|
||||
if (platform == Platform.Mono)
|
||||
if (platform.IsMono())
|
||||
{
|
||||
this.PrintDebug("Safely replacing game launcher...");
|
||||
if (!File.Exists(paths.unixLauncherBackup))
|
||||
File.Move(paths.unixLauncher, paths.unixLauncherBackup);
|
||||
else if (File.Exists(paths.unixLauncher))
|
||||
this.InteractivelyDelete(paths.unixLauncher);
|
||||
if (File.Exists(paths.unixLauncher))
|
||||
{
|
||||
if (!File.Exists(paths.unixLauncherBackup))
|
||||
File.Move(paths.unixLauncher, paths.unixLauncherBackup);
|
||||
else
|
||||
this.InteractivelyDelete(paths.unixLauncher);
|
||||
}
|
||||
|
||||
File.Move(paths.unixSmapiLauncher, paths.unixLauncher);
|
||||
}
|
||||
|
@ -323,19 +371,42 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// add or replace bundled mods
|
||||
Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods"));
|
||||
modsDir.Create();
|
||||
DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods"));
|
||||
if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
|
||||
{
|
||||
this.PrintDebug("Adding bundled mods...");
|
||||
|
||||
// special case: rename Omegasis' SaveBackup mod
|
||||
{
|
||||
DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup"));
|
||||
DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "AdvancedSaveBackup"));
|
||||
FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json"));
|
||||
if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1))
|
||||
{
|
||||
this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}...");
|
||||
this.Move(oldFolder, newFolder.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
// add bundled mods
|
||||
foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
|
||||
{
|
||||
this.PrintDebug($" adding {sourceDir.Name}...");
|
||||
|
||||
// initialise target dir
|
||||
// init/clear target dir
|
||||
DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name));
|
||||
this.InteractivelyDelete(targetDir.FullName);
|
||||
targetDir.Create();
|
||||
if (targetDir.Exists)
|
||||
{
|
||||
this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles);
|
||||
foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos())
|
||||
{
|
||||
if (protectedFiles == null || !protectedFiles.Contains(entry.Name))
|
||||
this.InteractivelyDelete(entry.FullName);
|
||||
}
|
||||
}
|
||||
else
|
||||
targetDir.Create();
|
||||
|
||||
// copy files
|
||||
foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
|
||||
|
@ -344,7 +415,7 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// remove obsolete appdata mods
|
||||
this.InteractivelyRemoveAppDataMods(platform, modsDir, packagedModsDir);
|
||||
this.InteractivelyRemoveAppDataMods(modsDir, packagedModsDir);
|
||||
}
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
|
@ -356,20 +427,20 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
if (action == ScriptAction.Install)
|
||||
{
|
||||
this.PrintColor("SMAPI is installed! If you use Steam, set your launch options to enable achievements (see smapi.io/install):", ConsoleColor.DarkGreen);
|
||||
this.PrintColor($" \"{Path.Combine(installDir.FullName, "StardewModdingAPI.exe")}\" %command%", ConsoleColor.DarkGreen);
|
||||
this.PrintSuccess("SMAPI is installed! If you use Steam, set your launch options to enable achievements (see smapi.io/install):");
|
||||
this.PrintSuccess($" \"{Path.Combine(installDir.FullName, "StardewModdingAPI.exe")}\" %command%");
|
||||
Console.WriteLine();
|
||||
this.PrintColor("If you don't use Steam, launch StardewModdingAPI.exe in your game folder to play with mods.", ConsoleColor.DarkGreen);
|
||||
this.PrintSuccess("If you don't use Steam, launch StardewModdingAPI.exe in your game folder to play with mods.");
|
||||
}
|
||||
else
|
||||
this.PrintColor("SMAPI is removed! If you configured Steam to launch SMAPI, don't forget to clear your launch options.", ConsoleColor.DarkGreen);
|
||||
this.PrintSuccess("SMAPI is removed! If you configured Steam to launch SMAPI, don't forget to clear your launch options.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (action == ScriptAction.Install)
|
||||
this.PrintColor("SMAPI is installed! Launch the game the same way as before to play with mods.", ConsoleColor.DarkGreen);
|
||||
else
|
||||
this.PrintColor("SMAPI is removed! Launch the game the same way as before to play without mods.", ConsoleColor.DarkGreen);
|
||||
this.PrintSuccess(action == ScriptAction.Install
|
||||
? "SMAPI is installed! Launch the game the same way as before to play with mods."
|
||||
: "SMAPI is removed! Launch the game the same way as before to play without mods."
|
||||
);
|
||||
}
|
||||
|
||||
Console.ReadKey();
|
||||
|
@ -379,36 +450,17 @@ namespace StardewModdingApi.Installer
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Detect the game's platform.</summary>
|
||||
/// <exception cref="NotSupportedException">The platform is not supported.</exception>
|
||||
private Platform DetectPlatform()
|
||||
/// <summary>Get the display text for an assembly version.</summary>
|
||||
/// <param name="version">The assembly version.</param>
|
||||
private string GetDisplayVersion(Version version)
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
case PlatformID.MacOSX:
|
||||
case PlatformID.Unix:
|
||||
return Platform.Mono;
|
||||
|
||||
default:
|
||||
return Platform.Windows;
|
||||
}
|
||||
string str = $"{version.Major}.{version.Minor}";
|
||||
if (version.Build != 0)
|
||||
str += $".{version.Build}";
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>Test whether the current console supports color formatting.</summary>
|
||||
private static bool GetConsoleSupportsColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = Console.ForegroundColor;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false; // Mono bug
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the value of a key in the Windows registry.</summary>
|
||||
/// <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)
|
||||
|
@ -421,41 +473,38 @@ namespace StardewModdingApi.Installer
|
|||
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 debug message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintDebug(string text)
|
||||
{
|
||||
this.PrintColor(text, ConsoleColor.DarkGray);
|
||||
}
|
||||
private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
|
||||
|
||||
/// <summary>Print a debug message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintInfo(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info);
|
||||
|
||||
/// <summary>Print a warning message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintWarning(string text)
|
||||
{
|
||||
this.PrintColor(text, ConsoleColor.DarkYellow);
|
||||
}
|
||||
private void PrintWarning(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn);
|
||||
|
||||
/// <summary>Print a warning message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintError(string text)
|
||||
{
|
||||
this.PrintColor(text, ConsoleColor.Red);
|
||||
}
|
||||
private void PrintError(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error);
|
||||
|
||||
/// <summary>Print a message to the console.</summary>
|
||||
/// <param name="text">The message text.</param>
|
||||
/// <param name="color">The text foreground color.</param>
|
||||
private void PrintColor(string text, ConsoleColor color)
|
||||
{
|
||||
if (InteractiveInstaller.ConsoleSupportsColor)
|
||||
{
|
||||
Console.ForegroundColor = color;
|
||||
Console.WriteLine(text);
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
Console.WriteLine(text);
|
||||
}
|
||||
/// <summary>Print a success message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintSuccess(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success);
|
||||
|
||||
/// <summary>Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
|
@ -476,7 +525,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <summary>Get whether the current system has XNA Framework installed. This only applies on Windows.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
/// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
|
||||
private bool HasXNA(Platform platform)
|
||||
private bool HasXna(Platform platform)
|
||||
{
|
||||
switch (platform)
|
||||
{
|
||||
|
@ -511,6 +560,7 @@ namespace StardewModdingApi.Installer
|
|||
|
||||
/// <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>
|
||||
/// <remarks>This method is mirred from <c>FileUtilities.ForceDelete</c> in the toolkit.</remarks>
|
||||
private void ForceDelete(FileSystemInfo entry)
|
||||
{
|
||||
// ignore if already deleted
|
||||
|
@ -519,8 +569,7 @@ namespace StardewModdingApi.Installer
|
|||
return;
|
||||
|
||||
// delete children
|
||||
var folder = entry as DirectoryInfo;
|
||||
if (folder != null)
|
||||
if (entry is DirectoryInfo folder)
|
||||
{
|
||||
foreach (FileSystemInfo child in folder.GetFileSystemInfos())
|
||||
this.ForceDelete(child);
|
||||
|
@ -551,11 +600,11 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
this.PrintInfo(message);
|
||||
string input = Console.ReadLine()?.Trim().ToLowerInvariant();
|
||||
if (!options.Contains(input))
|
||||
{
|
||||
Console.WriteLine("That's not a valid option.");
|
||||
this.PrintInfo("That's not a valid option.");
|
||||
continue;
|
||||
}
|
||||
return input;
|
||||
|
@ -568,9 +617,7 @@ namespace StardewModdingApi.Installer
|
|||
private DirectoryInfo InteractivelyGetInstallPath(Platform platform, string specifiedPath)
|
||||
{
|
||||
// get executable name
|
||||
string executableFilename = platform == Platform.Windows
|
||||
? "Stardew Valley.exe"
|
||||
: "StardewValley.exe";
|
||||
string executableFilename = EnvironmentUtility.GetExecutableName(platform);
|
||||
|
||||
// validate specified path
|
||||
if (specifiedPath != null)
|
||||
|
@ -597,6 +644,8 @@ namespace StardewModdingApi.Installer
|
|||
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
|
||||
|
@ -608,9 +657,9 @@ namespace StardewModdingApi.Installer
|
|||
|
||||
// let user choose path
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Found multiple copies of the game:");
|
||||
this.PrintInfo("Found multiple copies of the game:");
|
||||
for (int i = 0; i < defaultPaths.Length; i++)
|
||||
Console.WriteLine($"[{i + 1}] {defaultPaths[i].FullName}");
|
||||
this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}");
|
||||
Console.WriteLine();
|
||||
|
||||
string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
|
@ -620,22 +669,22 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// ask user
|
||||
Console.WriteLine("Oops, couldn't find the game automatically.");
|
||||
this.PrintInfo("Oops, couldn't find the game automatically.");
|
||||
while (true)
|
||||
{
|
||||
// get path from user
|
||||
Console.WriteLine($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter.");
|
||||
this.PrintInfo($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter.");
|
||||
string path = Console.ReadLine()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
Console.WriteLine(" You must specify a directory path to continue.");
|
||||
this.PrintInfo(" You must specify a directory path to continue.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// normalise path
|
||||
if (platform == Platform.Windows)
|
||||
path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path
|
||||
if (platform == Platform.Mono)
|
||||
if (platform == Platform.Linux || platform == Platform.Mac)
|
||||
path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line
|
||||
if (path.StartsWith("~/"))
|
||||
{
|
||||
|
@ -651,35 +700,31 @@ namespace StardewModdingApi.Installer
|
|||
// validate path
|
||||
if (!directory.Exists)
|
||||
{
|
||||
Console.WriteLine(" That directory doesn't seem to exist.");
|
||||
this.PrintInfo(" That directory doesn't seem to exist.");
|
||||
continue;
|
||||
}
|
||||
if (!directory.EnumerateFiles(executableFilename).Any())
|
||||
{
|
||||
Console.WriteLine(" That directory doesn't contain a Stardew Valley executable.");
|
||||
this.PrintInfo(" That directory doesn't contain a Stardew Valley executable.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// looks OK
|
||||
Console.WriteLine(" OK!");
|
||||
this.PrintInfo(" OK!");
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Interactively move mods out of the appdata directory.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
/// <param name="properModsDir">The directory which should contain all mods.</param>
|
||||
/// <param name="packagedModsDir">The installer directory containing packaged mods.</param>
|
||||
private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir, DirectoryInfo packagedModsDir)
|
||||
private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir)
|
||||
{
|
||||
// get packaged mods to delete
|
||||
string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray();
|
||||
|
||||
// get path
|
||||
string homePath = platform == Platform.Windows
|
||||
? Environment.GetEnvironmentVariable("APPDATA")
|
||||
: Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".config");
|
||||
string appDataPath = Path.Combine(homePath, "StardewValley");
|
||||
string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
|
||||
DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods"));
|
||||
|
||||
// check if migration needed
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>StardewModdingAPI.Installer</RootNamespace>
|
||||
<AssemblyName>StardewModdingAPI.Installer</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
</PropertyGroup>
|
||||
|
@ -40,7 +40,6 @@
|
|||
<Link>Properties\GlobalAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Enums\ScriptAction.cs" />
|
||||
<Compile Include="Enums\Platform.cs" />
|
||||
<Compile Include="InteractiveInstaller.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
@ -58,7 +57,7 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
<Import Project="..\..\build\prepare-install-package.targets" />
|
||||
|
|
|
@ -14,15 +14,9 @@
|
|||
SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately.
|
||||
|
||||
|
||||
Install guide
|
||||
Player's guide
|
||||
--------------------------------
|
||||
See http://stardewvalleywiki.com/Modding:Installing_SMAPI.
|
||||
|
||||
|
||||
Need help?
|
||||
--------------------------------
|
||||
- FAQs: http://stardewvalleywiki.com/Modding:Player_FAQs
|
||||
- Ask for help: https://discord.gg/kH55QXP
|
||||
See https://stardewvalleywiki.com/Modding:Player_Guide
|
||||
|
||||
|
||||
Manual install
|
||||
|
@ -30,7 +24,7 @@ Manual install
|
|||
THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead.
|
||||
If you really want to install SMAPI manually, here's how.
|
||||
|
||||
1. Download the latest version of SMAPI: https://github.com/Pathoschild/SMAPI/releases.
|
||||
1. Download the latest version of SMAPI: https://github.com/Pathoschild/SMAPI/releases
|
||||
2. Unzip the .zip file somewhere (not in your game folder).
|
||||
3. Copy the files from the "internal/Windows" folder (on Windows) or "internal/Mono" folder (on
|
||||
Linux/Mac) into your game folder. The `StardewModdingAPI.exe` file should be right next to the
|
||||
|
|
|
@ -14,7 +14,7 @@ fi
|
|||
|
||||
# validate Mono & run installer
|
||||
if $COMMAND mono >/dev/null 2>&1; then
|
||||
mono install.exe
|
||||
mono internal/Mono/install.exe
|
||||
else
|
||||
echo "Oops! Looks like Mono isn't installed. Please install Mono from http://mono-project.com, reboot, and run this installer again."
|
||||
read
|
||||
|
|
|
@ -74,11 +74,11 @@ else
|
|||
elif $COMMAND xterm 2>/dev/null; then
|
||||
xterm -e "$LAUNCHER"
|
||||
elif $COMMAND xfce4-terminal 2>/dev/null; then
|
||||
xfce4-terminal -e "$LAUNCHER"
|
||||
xfce4-terminal -e "env TERM=xterm; $LAUNCHER"
|
||||
elif $COMMAND gnome-terminal 2>/dev/null; then
|
||||
gnome-terminal -e "$LAUNCHER"
|
||||
gnome-terminal -e "env TERM=xterm; $LAUNCHER"
|
||||
elif $COMMAND konsole 2>/dev/null; then
|
||||
konsole -e "$LAUNCHER"
|
||||
konsole -p Environment=TERM=xterm -e "$LAUNCHER"
|
||||
elif $COMMAND terminal 2>/dev/null; then
|
||||
terminal -e "$LAUNCHER"
|
||||
else
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewModdingAPI.Internal.ConsoleWriting
|
||||
{
|
||||
/// <summary>Provides a wrapper for writing color-coded text to the console.</summary>
|
||||
internal class ColorfulConsoleWriter
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The console text color for each log level.</summary>
|
||||
private readonly IDictionary<ConsoleLogLevel, ConsoleColor> Colors;
|
||||
|
||||
/// <summary>Whether the current console supports color formatting.</summary>
|
||||
private readonly bool SupportsColor;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="platform">The target platform.</param>
|
||||
/// <param name="colorScheme">The console color scheme to use.</param>
|
||||
public ColorfulConsoleWriter(Platform platform, MonitorColorScheme colorScheme)
|
||||
{
|
||||
this.SupportsColor = this.TestColorSupport();
|
||||
this.Colors = this.GetConsoleColorScheme(platform, colorScheme);
|
||||
}
|
||||
|
||||
/// <summary>Write a message line to the log.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="level">The log level.</param>
|
||||
public void WriteLine(string message, ConsoleLogLevel level)
|
||||
{
|
||||
if (this.SupportsColor)
|
||||
{
|
||||
if (level == ConsoleLogLevel.Critical)
|
||||
{
|
||||
Console.BackgroundColor = ConsoleColor.Red;
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.ForegroundColor = this.Colors[level];
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
else
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Test whether the current console supports color formatting.</summary>
|
||||
private bool TestColorSupport()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = Console.ForegroundColor;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false; // Mono bug
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the color scheme to use for the current console.</summary>
|
||||
/// <param name="platform">The target platform.</param>
|
||||
/// <param name="colorScheme">The console color scheme to use.</param>
|
||||
private IDictionary<ConsoleLogLevel, ConsoleColor> GetConsoleColorScheme(Platform platform, MonitorColorScheme colorScheme)
|
||||
{
|
||||
// auto detect color scheme
|
||||
if (colorScheme == MonitorColorScheme.AutoDetect)
|
||||
{
|
||||
colorScheme = platform == Platform.Mac
|
||||
? MonitorColorScheme.LightBackground // MacOS doesn't provide console background color info, but it's usually white.
|
||||
: ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground;
|
||||
}
|
||||
|
||||
// get colors for scheme
|
||||
switch (colorScheme)
|
||||
{
|
||||
case MonitorColorScheme.DarkBackground:
|
||||
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>
|
||||
/// <param name="color">The color to check.</param>
|
||||
private static bool IsDark(ConsoleColor color)
|
||||
{
|
||||
switch (color)
|
||||
{
|
||||
case ConsoleColor.Black:
|
||||
case ConsoleColor.Blue:
|
||||
case ConsoleColor.DarkBlue:
|
||||
case ConsoleColor.DarkMagenta: // Powershell
|
||||
case ConsoleColor.DarkRed:
|
||||
case ConsoleColor.Red:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
namespace StardewModdingAPI.Internal.ConsoleWriting
|
||||
{
|
||||
/// <summary>The log severity levels.</summary>
|
||||
internal enum ConsoleLogLevel
|
||||
{
|
||||
/// <summary>Tracing info intended for developers.</summary>
|
||||
Trace,
|
||||
|
||||
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
|
||||
Debug,
|
||||
|
||||
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
|
||||
Info,
|
||||
|
||||
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>A message indicating something went wrong.</summary>
|
||||
Error,
|
||||
|
||||
/// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary>
|
||||
Alert,
|
||||
|
||||
/// <summary>A critical issue that generally signals an immediate end to the application.</summary>
|
||||
Critical,
|
||||
|
||||
/// <summary>A success message that generally signals a successful end to a task.</summary>
|
||||
Success
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace StardewModdingAPI.Internal.ConsoleWriting
|
||||
{
|
||||
/// <summary>A monitor color scheme to use.</summary>
|
||||
internal enum MonitorColorScheme
|
||||
{
|
||||
/// <summary>Choose a color scheme automatically.</summary>
|
||||
AutoDetect,
|
||||
|
||||
/// <summary>Use lighter text colors that look better on a black or dark background.</summary>
|
||||
DarkBackground,
|
||||
|
||||
/// <summary>Use darker text colors that look better on a white or light background.</summary>
|
||||
LightBackground
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
using System.Management;
|
||||
#endif
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace StardewModdingAPI.Internal
|
||||
{
|
||||
/// <summary>Provides methods for fetching environment information.</summary>
|
||||
internal static class EnvironmentUtility
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>Get the OS name from the system uname command.</summary>
|
||||
/// <param name="buffer">The buffer to fill with the resulting string.</param>
|
||||
[DllImport("libc")]
|
||||
static extern int uname(IntPtr buffer);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Detect the current OS.</summary>
|
||||
public static Platform DetectPlatform()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
case PlatformID.MacOSX:
|
||||
return Platform.Mac;
|
||||
|
||||
case PlatformID.Unix:
|
||||
return EnvironmentUtility.IsRunningMac()
|
||||
? Platform.Mac
|
||||
: Platform.Linux;
|
||||
|
||||
default:
|
||||
return Platform.Windows;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Get the human-readable OS name and version.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
[SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
|
||||
public static string GetFriendlyPlatformName(Platform platform)
|
||||
{
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
try
|
||||
{
|
||||
return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
|
||||
.Get()
|
||||
.Cast<ManagementObject>()
|
||||
.Select(entry => entry.GetPropertyValue("Caption").ToString())
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch { }
|
||||
#endif
|
||||
return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion;
|
||||
}
|
||||
|
||||
/// <summary>Get the name of the Stardew Valley executable.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
public static string GetExecutableName(Platform platform)
|
||||
{
|
||||
return platform == Platform.Windows
|
||||
? "Stardew Valley.exe"
|
||||
: "StardewValley.exe";
|
||||
}
|
||||
|
||||
/// <summary>Get whether the platform uses Mono.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
public static bool IsMono(this Platform platform)
|
||||
{
|
||||
return platform == Platform.Linux || platform == Platform.Mac;
|
||||
}
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Detect whether the code is running on Mac.</summary>
|
||||
/// <remarks>
|
||||
/// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the
|
||||
/// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS.
|
||||
/// </remarks>
|
||||
private static bool IsRunningMac()
|
||||
{
|
||||
IntPtr buffer = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
buffer = Marshal.AllocHGlobal(8192);
|
||||
if (EnvironmentUtility.uname(buffer) == 0)
|
||||
{
|
||||
string os = Marshal.PtrToStringAnsi(buffer);
|
||||
return os == "Darwin";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false; // default to Linux
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (buffer != IntPtr.Zero)
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace StardewModdingAPI.Internal
|
||||
{
|
||||
/// <summary>The game's platform version.</summary>
|
||||
internal enum Platform
|
||||
{
|
||||
/// <summary>The Linux version of the game.</summary>
|
||||
Linux,
|
||||
|
||||
/// <summary>The Mac version of the game.</summary>
|
||||
Mac,
|
||||
|
||||
/// <summary>The Windows version of the game.</summary>
|
||||
Windows
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
<HasSharedItems>true</HasSharedItems>
|
||||
<SharedGUID>85208f8d-6fd1-4531-be05-7142490f59fe</SharedGUID>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<Import_RootNamespace>SMAPI.Internal</Import_RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorfulConsoleWriter.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)EnvironmentUtility.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\LogLevel.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Platform.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,13 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc</ProjectGuid>
|
||||
<ProjectGuid>85208f8d-6fd1-4531-be05-7142490f59fe</ProjectGuid>
|
||||
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
|
||||
<PropertyGroup />
|
||||
<Import Project="StardewModdingAPI.Common.projitems" Label="Shared" />
|
||||
<Import Project="SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
|
||||
</Project>
|
|
@ -0,0 +1,10 @@
|
|||
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Netcode
|
||||
{
|
||||
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary>
|
||||
public class NetCollection<T> : Collection<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { }
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Netcode
|
||||
{
|
||||
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary>
|
||||
public class NetList<T> : List<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { }
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
|
||||
namespace Netcode
|
||||
{
|
||||
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary>
|
||||
public class NetObjectList<T> : NetList<T> { }
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
|
||||
#pragma warning disable 649 // (never assigned) -- only used to test type conversions
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewValley
|
||||
|
@ -6,6 +7,7 @@ namespace StardewValley
|
|||
/// <summary>A simplified version of Stardew Valley's <c>StardewValley.Farmer</c> class for unit testing.</summary>
|
||||
internal class Farmer
|
||||
{
|
||||
public IDictionary<string, int[]> friendships;
|
||||
/// <summary>A sample field which should be replaced with a different property.</summary>
|
||||
public readonly IDictionary<string, int[]> friendships;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,5 +20,14 @@ namespace StardewValley
|
|||
|
||||
/// <summary>A generic net ref property with no equivalent non-net property.</summary>
|
||||
public NetRef<object> netRefProperty { get; } = new NetRef<object>();
|
||||
|
||||
/// <summary>A sample net list.</summary>
|
||||
public readonly NetList<int> netList = new NetList<int>();
|
||||
|
||||
/// <summary>A sample net object list.</summary>
|
||||
public readonly NetObjectList<int> netObjectList = new NetObjectList<int>();
|
||||
|
||||
/// <summary>A sample net collection.</summary>
|
||||
public readonly NetCollection<int> netCollection = new NetCollection<int>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
/// <param name="expression">The expression which should be reported.</param>
|
||||
/// <param name="fromType">The source type name which should be reported.</param>
|
||||
/// <param name="toType">The target type name which should be reported.</param>
|
||||
[TestCase("Item item = null; if (item.netIntField < 42);", 22, "item.netIntField", "NetInt", "int")]
|
||||
[TestCase("Item item = null; if (item.netIntField < 42);", 22, "item.netIntField", "NetInt", "int")] // ↓ implicit conversion
|
||||
[TestCase("Item item = null; if (item.netIntField <= 42);", 22, "item.netIntField", "NetInt", "int")]
|
||||
[TestCase("Item item = null; if (item.netIntField > 42);", 22, "item.netIntField", "NetInt", "int")]
|
||||
[TestCase("Item item = null; if (item.netIntField >= 42);", 22, "item.netIntField", "NetInt", "int")]
|
||||
|
@ -79,20 +79,24 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
[TestCase("Item item = null; if (item.netRefField != null);", 22, "item.netRefField", "NetRef", "object")]
|
||||
[TestCase("Item item = null; if (item.netRefProperty == null);", 22, "item.netRefProperty", "NetRef", "object")]
|
||||
[TestCase("Item item = null; if (item.netRefProperty != null);", 22, "item.netRefProperty", "NetRef", "object")]
|
||||
[TestCase("SObject obj = null; if (obj.netIntField != 42);", 24, "obj.netIntField", "NetInt", "int")] // ↓ same as above, but inherited from base class
|
||||
[TestCase("SObject obj = null; if (obj.netIntField != 42);", 24, "obj.netIntField", "NetInt", "int")] // ↓ implicit conversion for parent field
|
||||
[TestCase("SObject obj = null; if (obj.netIntProperty != 42);", 24, "obj.netIntProperty", "NetInt", "int")]
|
||||
[TestCase("SObject obj = null; if (obj.netRefField == null);", 24, "obj.netRefField", "NetRef", "object")]
|
||||
[TestCase("SObject obj = null; if (obj.netRefField != null);", 24, "obj.netRefField", "NetRef", "object")]
|
||||
[TestCase("SObject obj = null; if (obj.netRefProperty == null);", 24, "obj.netRefProperty", "NetRef", "object")]
|
||||
[TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")]
|
||||
[TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type
|
||||
[TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")]
|
||||
[TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetInt", "int")] // ↓ explicit conversion to invalid type
|
||||
[TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")]
|
||||
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType)
|
||||
{
|
||||
// arrange
|
||||
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
{
|
||||
Id = "SMAPI001",
|
||||
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/smapi001 for details.",
|
||||
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.",
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) }
|
||||
};
|
||||
|
@ -101,6 +105,22 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
this.VerifyCSharpDiagnostic(code, expected);
|
||||
}
|
||||
|
||||
/// <summary>Test that the net field analyzer doesn't raise any warnings for safe member access.</summary>
|
||||
/// <param name="codeText">The code line to test.</param>
|
||||
[TestCase("Item item = new Item(); System.Collections.IEnumerable list = farmer.eventsSeen;")]
|
||||
[TestCase("Item item = new Item(); System.Collections.Generic.IEnumerable<int> list = farmer.netList;")]
|
||||
[TestCase("Item item = new Item(); System.Collections.Generic.IList<int> list = farmer.netList;")]
|
||||
[TestCase("Item item = new Item(); System.Collections.Generic.ICollection<int> list = farmer.netCollection;")]
|
||||
[TestCase("Item item = new Item(); System.Collections.Generic.IList<int> list = farmer.netObjectList;")] // subclass of NetList
|
||||
public void AvoidImplicitNetFieldComparisons_AllowsSafeAccess(string codeText)
|
||||
{
|
||||
// arrange
|
||||
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
|
||||
// assert
|
||||
this.VerifyCSharpDiagnostic(code);
|
||||
}
|
||||
|
||||
/// <summary>Test that the expected diagnostic message is raised for avoidable net field references.</summary>
|
||||
/// <param name="codeText">The code line to test.</param>
|
||||
/// <param name="column">The column within the code line where the diagnostic message should be reported.</param>
|
||||
|
@ -117,8 +137,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
{
|
||||
Id = "SMAPI002",
|
||||
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/smapi002 for details.",
|
||||
Id = "AvoidNetField",
|
||||
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.",
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) }
|
||||
};
|
||||
|
|
|
@ -59,14 +59,15 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
/// <param name="oldName">The old field name which should be reported.</param>
|
||||
/// <param name="newName">The new field name which should be reported.</param>
|
||||
[TestCase("var x = new Farmer().friendships;", 8, "StardewValley.Farmer.friendships", "friendshipData")]
|
||||
[TestCase("var x = new Farmer()?.friendships;", 8, "StardewValley.Farmer.friendships", "friendshipData")]
|
||||
public void AvoidObsoleteField_RaisesDiagnostic(string codeText, int column, string oldName, string newName)
|
||||
{
|
||||
// arrange
|
||||
string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
{
|
||||
Id = "SMAPI003",
|
||||
Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/smapi003 for details.",
|
||||
Id = "AvoidObsoleteField",
|
||||
Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.",
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Locations = new[] { new DiagnosticResultLocation("Test0.cs", ObsoleteFieldAnalyzerTests.SampleCodeLine, ObsoleteFieldAnalyzerTests.SampleCodeColumn + column) }
|
||||
};
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.8.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.10.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
||||
{
|
||||
/// <summary>Provides generic utilities for SMAPI's Roslyn analyzers.</summary>
|
||||
internal static class AnalyzerUtilities
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the metadata for an explicit cast or 'x as y' expression.</summary>
|
||||
/// <param name="node">The member access expression.</param>
|
||||
/// <param name="semanticModel">provides methods for asking semantic questions about syntax nodes.</param>
|
||||
/// <param name="fromExpression">The expression whose value is being converted.</param>
|
||||
/// <param name="fromType">The type being converted from.</param>
|
||||
/// <param name="toType">The type being converted to.</param>
|
||||
/// <returns>Returns true if the node is a matched expression, else false.</returns>
|
||||
public static bool TryGetCastOrAsInfo(SyntaxNode node, SemanticModel semanticModel, out ExpressionSyntax fromExpression, out TypeInfo fromType, out TypeInfo toType)
|
||||
{
|
||||
// (type)x
|
||||
if (node is CastExpressionSyntax cast)
|
||||
{
|
||||
fromExpression = cast.Expression;
|
||||
fromType = semanticModel.GetTypeInfo(fromExpression);
|
||||
toType = semanticModel.GetTypeInfo(cast.Type);
|
||||
return true;
|
||||
}
|
||||
|
||||
// x as y
|
||||
if (node is BinaryExpressionSyntax binary && binary.Kind() == SyntaxKind.AsExpression)
|
||||
{
|
||||
fromExpression = binary.Left;
|
||||
fromType = semanticModel.GetTypeInfo(fromExpression);
|
||||
toType = semanticModel.GetTypeInfo(binary.Right);
|
||||
return true;
|
||||
}
|
||||
|
||||
// invalid
|
||||
fromExpression = null;
|
||||
fromType = default(TypeInfo);
|
||||
toType = default(TypeInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Get the metadata for a member access expression.</summary>
|
||||
/// <param name="node">The member access expression.</param>
|
||||
/// <param name="semanticModel">provides methods for asking semantic questions about syntax nodes.</param>
|
||||
/// <param name="declaringType">The object type which has the member.</param>
|
||||
/// <param name="memberType">The type of the accessed member.</param>
|
||||
/// <param name="memberName">The name of the accessed member.</param>
|
||||
/// <returns>Returns true if the node is a member access expression, else false.</returns>
|
||||
public static bool TryGetMemberInfo(SyntaxNode node, SemanticModel semanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)
|
||||
{
|
||||
// simple access
|
||||
if (node is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
declaringType = semanticModel.GetTypeInfo(memberAccess.Expression).Type;
|
||||
memberType = semanticModel.GetTypeInfo(node);
|
||||
memberName = memberAccess.Name.Identifier.Text;
|
||||
return true;
|
||||
}
|
||||
|
||||
// conditional access
|
||||
if (node is ConditionalAccessExpressionSyntax conditionalAccess && conditionalAccess.WhenNotNull is MemberBindingExpressionSyntax conditionalBinding)
|
||||
{
|
||||
declaringType = semanticModel.GetTypeInfo(conditionalAccess.Expression).Type;
|
||||
memberType = semanticModel.GetTypeInfo(node);
|
||||
memberName = conditionalBinding.Name.Identifier.Text;
|
||||
return true;
|
||||
}
|
||||
|
||||
// invalid
|
||||
declaringType = null;
|
||||
memberType = default(TypeInfo);
|
||||
memberName = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Get the class types in a type's inheritance chain, including itself.</summary>
|
||||
/// <param name="type">The initial type.</param>
|
||||
public static IEnumerable<ITypeSymbol> GetConcreteTypes(ITypeSymbol type)
|
||||
{
|
||||
while (type != null)
|
||||
{
|
||||
yield return type;
|
||||
type = type.BaseType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
@ -21,6 +22,13 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
/// <summary>Maps net fields to their equivalent non-net properties where available.</summary>
|
||||
private readonly IDictionary<string, string> NetFieldWrapperProperties = new Dictionary<string, string>
|
||||
{
|
||||
// AnimatedSprite
|
||||
["StardewValley.AnimatedSprite::currentAnimation"] = "CurrentAnimation",
|
||||
["StardewValley.AnimatedSprite::currentFrame"] = "CurrentFrame",
|
||||
["StardewValley.AnimatedSprite::sourceRect"] = "SourceRect",
|
||||
["StardewValley.AnimatedSprite::spriteHeight"] = "SpriteHeight",
|
||||
["StardewValley.AnimatedSprite::spriteWidth"] = "SpriteWidth",
|
||||
|
||||
// Character
|
||||
["StardewValley.Character::currentLocationRef"] = "currentLocation",
|
||||
["StardewValley.Character::facingDirection"] = "FacingDirection",
|
||||
|
@ -106,7 +114,6 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
["StardewValley.Object::netName"] = "name",
|
||||
["StardewValley.Object::price"] = "Price",
|
||||
["StardewValley.Object::quality"] = "Quality",
|
||||
["StardewValley.Object::scale"] = "Scale",
|
||||
["StardewValley.Object::stack"] = "Stack",
|
||||
["StardewValley.Object::tileLocation"] = "TileLocation",
|
||||
["StardewValley.Object::type"] = "Type",
|
||||
|
@ -124,28 +131,27 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
["StardewValley.Tool::upgradeLevel"] = "UpgradeLevel"
|
||||
};
|
||||
|
||||
/// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
|
||||
private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
|
||||
{
|
||||
["SMAPI001"] = new DiagnosticDescriptor(
|
||||
id: "SMAPI001",
|
||||
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/smapi001 for details.",
|
||||
category: "SMAPI.CommonErrors",
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://smapi.io/buildmsg/smapi001"
|
||||
),
|
||||
["SMAPI002"] = new DiagnosticDescriptor(
|
||||
id: "SMAPI002",
|
||||
title: "Avoid Netcode types when possible",
|
||||
messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/smapi002 for details.",
|
||||
category: "SMAPI.CommonErrors",
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://smapi.io/buildmsg/smapi001"
|
||||
)
|
||||
};
|
||||
/// <summary>The diagnostic info for an implicit net field cast.</summary>
|
||||
private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor(
|
||||
id: "AvoidImplicitNetFieldCast",
|
||||
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.",
|
||||
category: "SMAPI.CommonErrors",
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://smapi.io/buildmsg/avoid-implicit-net-field-cast"
|
||||
);
|
||||
|
||||
/// <summary>The diagnostic info for an avoidable net field access.</summary>
|
||||
private readonly DiagnosticDescriptor AvoidNetFieldRule = new DiagnosticDescriptor(
|
||||
id: "AvoidNetField",
|
||||
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.",
|
||||
category: "SMAPI.CommonErrors",
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://smapi.io/buildmsg/avoid-net-field"
|
||||
);
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -161,22 +167,25 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
/// <summary>Construct an instance.</summary>
|
||||
public NetFieldAnalyzer()
|
||||
{
|
||||
this.SupportedDiagnostics = ImmutableArray.CreateRange(this.Rules.Values);
|
||||
this.SupportedDiagnostics = ImmutableArray.CreateRange(new[] { this.AvoidNetFieldRule, this.AvoidImplicitNetFieldCastRule });
|
||||
}
|
||||
|
||||
/// <summary>Called once at session start to register actions in the analysis context.</summary>
|
||||
/// <param name="context">The analysis context.</param>
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
// SMAPI002: avoid net fields if possible
|
||||
context.RegisterSyntaxNodeAction(
|
||||
this.AnalyzeAvoidableNetField,
|
||||
SyntaxKind.SimpleMemberAccessExpression
|
||||
this.AnalyzeMemberAccess,
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
SyntaxKind.ConditionalAccessExpression
|
||||
);
|
||||
|
||||
// SMAPI001: avoid implicit net field conversion
|
||||
context.RegisterSyntaxNodeAction(
|
||||
this.AnalyseNetFieldConversions,
|
||||
this.AnalyzeCast,
|
||||
SyntaxKind.CastExpression,
|
||||
SyntaxKind.AsExpression
|
||||
);
|
||||
context.RegisterSyntaxNodeAction(
|
||||
this.AnalyzeBinaryComparison,
|
||||
SyntaxKind.EqualsExpression,
|
||||
SyntaxKind.NotEqualsExpression,
|
||||
SyntaxKind.GreaterThanExpression,
|
||||
|
@ -190,77 +199,126 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Analyse a syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available.</summary>
|
||||
/// <summary>Analyse a member access syntax node and add a diagnostic message if applicable.</summary>
|
||||
/// <param name="context">The analysis context.</param>
|
||||
private void AnalyzeAvoidableNetField(SyntaxNodeAnalysisContext context)
|
||||
/// <returns>Returns whether any warnings were added.</returns>
|
||||
private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
try
|
||||
this.HandleErrors(context.Node, () =>
|
||||
{
|
||||
// check member type
|
||||
MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node;
|
||||
TypeInfo memberType = context.SemanticModel.GetTypeInfo(node);
|
||||
// get member access info
|
||||
if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName))
|
||||
return;
|
||||
if (!this.IsNetType(memberType.Type))
|
||||
return;
|
||||
|
||||
// get reference info
|
||||
ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type;
|
||||
string propertyName = node.Name.Identifier.Text;
|
||||
|
||||
// suggest replacement
|
||||
for (ITypeSymbol type = declaringType; type != null; type = type.BaseType)
|
||||
// warn: use property wrapper if available
|
||||
foreach (ITypeSymbol type in AnalyzerUtilities.GetConcreteTypes(declaringType))
|
||||
{
|
||||
if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{propertyName}", out string suggestedPropertyName))
|
||||
if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{memberName}", out string suggestedPropertyName))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI002"], context.Node.GetLocation(), node, memberType.Type.Name, suggestedPropertyName));
|
||||
break;
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.AvoidNetFieldRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, suggestedPropertyName));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
|
||||
}
|
||||
|
||||
// warn: implicit conversion
|
||||
if (this.IsInvalidConversion(memberType.Type, memberType.ConvertedType))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Analyse a syntax node and add a diagnostic message if it implicitly converts a net field.</summary>
|
||||
/// <summary>Analyse an explicit cast or 'x as y' node and add a diagnostic message if applicable.</summary>
|
||||
/// <param name="context">The analysis context.</param>
|
||||
private void AnalyseNetFieldConversions(SyntaxNodeAnalysisContext context)
|
||||
/// <returns>Returns whether any warnings were added.</returns>
|
||||
private void AnalyzeCast(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
try
|
||||
// NOTE: implicit conversion within the expression is detected by the member access
|
||||
// checks. This method is only concerned with the conversion of its final value.
|
||||
this.HandleErrors(context.Node, () =>
|
||||
{
|
||||
BinaryExpressionSyntax binaryExpression = (BinaryExpressionSyntax)context.Node;
|
||||
foreach (var pair in new[] { Tuple.Create(binaryExpression.Left, binaryExpression.Right), Tuple.Create(binaryExpression.Right, binaryExpression.Left) })
|
||||
if (AnalyzerUtilities.TryGetCastOrAsInfo(context.Node, context.SemanticModel, out ExpressionSyntax fromExpression, out TypeInfo fromType, out TypeInfo toType))
|
||||
{
|
||||
if (this.IsInvalidConversion(fromType.ConvertedType, toType.Type))
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), fromExpression, fromType.ConvertedType.Name, toType.Type));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Analyse a binary comparison syntax node and add a diagnostic message if applicable.</summary>
|
||||
/// <param name="context">The analysis context.</param>
|
||||
/// <returns>Returns whether any warnings were added.</returns>
|
||||
private void AnalyzeBinaryComparison(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
// NOTE: implicit conversion within an operand is detected by the member access checks.
|
||||
// This method is only concerned with the conversion of each side's final value.
|
||||
this.HandleErrors(context.Node, () =>
|
||||
{
|
||||
BinaryExpressionSyntax expression = (BinaryExpressionSyntax)context.Node;
|
||||
foreach (var pair in new[] { Tuple.Create(expression.Left, expression.Right), Tuple.Create(expression.Right, expression.Left) })
|
||||
{
|
||||
// get node info
|
||||
ExpressionSyntax curExpression = pair.Item1; // the side of the comparison being examined
|
||||
ExpressionSyntax otherExpression = pair.Item2; // the other side
|
||||
TypeInfo typeInfo = context.SemanticModel.GetTypeInfo(curExpression);
|
||||
if (!this.IsNetType(typeInfo.Type))
|
||||
TypeInfo curType = context.SemanticModel.GetTypeInfo(curExpression);
|
||||
TypeInfo otherType = context.SemanticModel.GetTypeInfo(otherExpression);
|
||||
if (!this.IsNetType(curType.ConvertedType))
|
||||
continue;
|
||||
|
||||
// warn for implicit conversion
|
||||
if (!this.IsNetType(typeInfo.ConvertedType))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI001"], context.Node.GetLocation(), curExpression, typeInfo.Type.Name, typeInfo.ConvertedType));
|
||||
break;
|
||||
}
|
||||
|
||||
// warn for comparison to null
|
||||
// An expression like `building.indoors != null` will sometimes convert `building.indoors` to NetFieldBase instead of object before comparison. Haven't reproduced this in unit tests yet.
|
||||
Optional<object> otherValue = context.SemanticModel.GetConstantValue(otherExpression);
|
||||
if (otherValue.HasValue && otherValue.Value == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI001"], context.Node.GetLocation(), curExpression, typeInfo.Type.Name, "null"));
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), curExpression, curType.Type.Name, "null"));
|
||||
break;
|
||||
}
|
||||
|
||||
// warn for implicit conversion
|
||||
if (!this.IsNetType(otherType.ConvertedType))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), curExpression, curType.Type.Name, curType.ConvertedType));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Handle exceptions raised while analyzing a node.</summary>
|
||||
/// <param name="node">The node being analysed.</param>
|
||||
/// <param name="action">The callback to invoke.</param>
|
||||
private void HandleErrors(SyntaxNode node, Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
|
||||
throw new InvalidOperationException($"Failed processing expression: '{node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get whether a net field was converted in an error-prone way.</summary>
|
||||
/// <param name="fromType">The source type.</param>
|
||||
/// <param name="toType">The target type.</param>
|
||||
private bool IsInvalidConversion(ITypeSymbol fromType, ITypeSymbol toType)
|
||||
{
|
||||
// no conversion
|
||||
if (!this.IsNetType(fromType) || this.IsNetType(toType))
|
||||
return false;
|
||||
|
||||
// conversion to implemented interface is OK
|
||||
if (fromType.AllInterfaces.Contains(toType))
|
||||
return false;
|
||||
|
||||
// avoid any other conversions
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a type symbol references a <c>Netcode</c> type.</summary>
|
||||
/// <param name="typeSymbol">The type symbol.</param>
|
||||
private bool IsNetType(ITypeSymbol typeSymbol)
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
||||
|
@ -25,14 +24,14 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
/// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
|
||||
private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
|
||||
{
|
||||
["SMAPI003"] = new DiagnosticDescriptor(
|
||||
id: "SMAPI003",
|
||||
["AvoidObsoleteField"] = new DiagnosticDescriptor(
|
||||
id: "AvoidObsoleteField",
|
||||
title: "Reference to obsolete field",
|
||||
messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/smapi003 for details.",
|
||||
messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.",
|
||||
category: "SMAPI.CommonErrors",
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
helpLinkUri: "https://smapi.io/buildmsg/smapi003"
|
||||
helpLinkUri: "https://smapi.io/buildmsg/avoid-obsolete-field"
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -57,10 +56,10 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
/// <param name="context">The analysis context.</param>
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
// SMAPI003: avoid obsolete fields
|
||||
context.RegisterSyntaxNodeAction(
|
||||
this.AnalyzeObsoleteFields,
|
||||
SyntaxKind.SimpleMemberAccessExpression
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
SyntaxKind.ConditionalAccessExpression
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -75,16 +74,15 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
try
|
||||
{
|
||||
// get reference info
|
||||
MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node;
|
||||
ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type;
|
||||
string propertyName = node.Name.Identifier.Text;
|
||||
if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName))
|
||||
return;
|
||||
|
||||
// suggest replacement
|
||||
for (ITypeSymbol type = declaringType; type != null; type = type.BaseType)
|
||||
foreach (ITypeSymbol type in AnalyzerUtilities.GetConcreteTypes(declaringType))
|
||||
{
|
||||
if (this.ReplacedFields.TryGetValue($"{type}::{propertyName}", out string replacement))
|
||||
if (this.ReplacedFields.TryGetValue($"{type}::{memberName}", out string replacement))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI003"], context.Node.GetLocation(), $"{type}.{propertyName}", replacement));
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidObsoleteField"], context.Node.GetLocation(), $"{type}.{memberName}", replacement));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.8.2" PrivateAssets="all" />
|
||||
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Utilities;
|
||||
using StardewModdingAPI.ModBuildConfig.Framework;
|
||||
|
@ -42,6 +44,9 @@ namespace StardewModdingAPI.ModBuildConfig
|
|||
[Required]
|
||||
public bool EnableModZip { get; set; }
|
||||
|
||||
/// <summary>Custom comma-separated regex patterns matching files to ignore when deploying or zipping the mod.</summary>
|
||||
public string IgnoreModFilePatterns { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -55,8 +60,11 @@ namespace StardewModdingAPI.ModBuildConfig
|
|||
|
||||
try
|
||||
{
|
||||
// parse ignore patterns
|
||||
Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray();
|
||||
|
||||
// get mod info
|
||||
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir);
|
||||
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip);
|
||||
|
||||
// deploy mod files
|
||||
if (this.EnableModDeploy)
|
||||
|
@ -91,6 +99,29 @@ namespace StardewModdingAPI.ModBuildConfig
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the custom ignore patterns provided by the user.</summary>
|
||||
private IEnumerable<Regex> GetCustomIgnorePatterns()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.IgnoreModFilePatterns))
|
||||
yield break;
|
||||
|
||||
foreach (string raw in this.IgnoreModFilePatterns.Split(','))
|
||||
{
|
||||
Regex regex;
|
||||
try
|
||||
{
|
||||
regex = new Regex(raw.Trim(), RegexOptions.IgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Log.LogWarning($"Ignored invalid <{nameof(this.IgnoreModFilePatterns)}> pattern {raw}:\n{ex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return regex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Copy the mod files into the game's mod folder.</summary>
|
||||
/// <param name="files">The files to include.</param>
|
||||
/// <param name="modFolderPath">The folder path to create with the mod files.</param>
|
||||
|
|
|
@ -2,8 +2,9 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web.Script.Serialization;
|
||||
using StardewModdingAPI.Common;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
|
||||
namespace StardewModdingAPI.ModBuildConfig.Framework
|
||||
{
|
||||
|
@ -26,8 +27,10 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="projectDir">The folder containing the project files.</param>
|
||||
/// <param name="targetDir">The folder containing the build output.</param>
|
||||
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
|
||||
/// <param name="validateRequiredModFiles">Whether to validate that required mod files like the manifest are present.</param>
|
||||
/// <exception cref="UserErrorException">The mod package isn't valid.</exception>
|
||||
public ModFileManager(string projectDir, string targetDir)
|
||||
public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns, bool validateRequiredModFiles)
|
||||
{
|
||||
this.Files = new Dictionary<string, FileInfo>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
|
@ -72,26 +75,26 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n"))
|
||||
continue;
|
||||
|
||||
// ignore release zips
|
||||
if (this.EqualsInvariant(file.Extension, ".zip"))
|
||||
continue;
|
||||
|
||||
// ignore Json.NET (bundled into SMAPI)
|
||||
if (this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml"))
|
||||
// handle ignored files
|
||||
if (this.ShouldIgnore(file, relativePath, ignoreFilePatterns))
|
||||
continue;
|
||||
|
||||
// add file
|
||||
this.Files[relativePath] = file;
|
||||
}
|
||||
|
||||
// check for missing manifest
|
||||
if (!this.Files.ContainsKey(this.ManifestFileName))
|
||||
throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");
|
||||
// check for required files
|
||||
if (validateRequiredModFiles)
|
||||
{
|
||||
// manifest
|
||||
if (!this.Files.ContainsKey(this.ManifestFileName))
|
||||
throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");
|
||||
|
||||
// check for missing DLL
|
||||
// ReSharper disable once SimplifyLinqExpression
|
||||
if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
|
||||
throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
|
||||
// DLL
|
||||
// ReSharper disable once SimplifyLinqExpression
|
||||
if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
|
||||
throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the files in the mod package.</summary>
|
||||
|
@ -136,15 +139,41 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0;
|
||||
int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0;
|
||||
string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null;
|
||||
return new SemanticVersionImpl(major, minor, patch, tag).ToString();
|
||||
return new SemanticVersion(major, minor, patch, tag).ToString();
|
||||
}
|
||||
return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+
|
||||
return new SemanticVersion(versionObj.ToString()).ToString(); // SMAPI 2.0+
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get whether a build output file should be ignored.</summary>
|
||||
/// <param name="file">The file to check.</param>
|
||||
/// <param name="relativePath">The file's relative path in the package.</param>
|
||||
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
|
||||
private bool ShouldIgnore(FileInfo file, string relativePath, Regex[] ignoreFilePatterns)
|
||||
{
|
||||
return
|
||||
// release zips
|
||||
this.EqualsInvariant(file.Extension, ".zip")
|
||||
|
||||
// Json.NET (bundled into SMAPI)
|
||||
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll")
|
||||
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")
|
||||
|
||||
// code analysis files
|
||||
|| file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.InvariantCultureIgnoreCase)
|
||||
|
||||
// OS metadata files
|
||||
|| this.EqualsInvariant(file.Name, ".DS_Store")
|
||||
|| this.EqualsInvariant(file.Name, "Thumbs.db")
|
||||
|
||||
// custom ignore patterns
|
||||
|| ignoreFilePatterns.Any(p => p.IsMatch(relativePath));
|
||||
}
|
||||
|
||||
/// <summary>Get a case-insensitive dictionary matching the given JSON.</summary>
|
||||
/// <param name="json">The JSON to parse.</param>
|
||||
private IDictionary<string, object> Parse(string json)
|
||||
|
|
|
@ -2,5 +2,5 @@ using System.Reflection;
|
|||
|
||||
[assembly: AssemblyTitle("SMAPI.ModBuildConfig")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyVersion("2.0.2.0")]
|
||||
[assembly: AssemblyFileVersion("2.0.2.0")]
|
||||
[assembly: AssemblyVersion("2.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.1.0.0")]
|
||||
|
|
|
@ -55,7 +55,14 @@
|
|||
<ItemGroup>
|
||||
<Content Include="assets\nuget-icon.png" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj">
|
||||
<Project>{ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6}</Project>
|
||||
<Name>StardewModdingAPI.Toolkit</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
<Import Project="..\..\build\prepare-nuget-package.targets" />
|
||||
</Project>
|
|
@ -19,14 +19,10 @@
|
|||
|
||||
<!-- set default settings -->
|
||||
<ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName>
|
||||
<ModUnitTests Condition="'$(ModUnitTests)' == ''">False</ModUnitTests>
|
||||
<ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath>
|
||||
<EnableModDeploy Condition="'$(EnableModDeploy)' == ''">True</EnableModDeploy>
|
||||
<EnableModZip Condition="'$(EnableModZip)' == ''">True</EnableModZip>
|
||||
|
||||
<!-- disable mod deploy in unit test project -->
|
||||
<EnableModDeploy Condition="'$(ModUnitTests)' == true">False</EnableModDeploy>
|
||||
<EnableModZip Condition="'$(ModUnitTests)' == true">False</EnableModZip>
|
||||
<CopyModReferencesToBuildOutput Condition="'$(CopyModReferencesToBuildOutput)' == ''">False</CopyModReferencesToBuildOutput>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- find platform + game path -->
|
||||
|
@ -35,6 +31,7 @@
|
|||
<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') -->
|
||||
|
@ -44,10 +41,18 @@
|
|||
</When>
|
||||
<When Condition="$(OS) == 'Windows_NT'">
|
||||
<PropertyGroup>
|
||||
<!-- default paths -->
|
||||
<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>
|
||||
|
@ -62,40 +67,45 @@
|
|||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="Netcode" Condition="Exists('$(GamePath)\Netcode.dll')">
|
||||
<Reference Include="Netcode">
|
||||
<HintPath>$(GamePath)\Netcode.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="Stardew Valley">
|
||||
<HintPath>$(GamePath)\Stardew Valley.exe</HintPath>
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewModdingAPI">
|
||||
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewModdingAPI.Toolkit.CoreInterfaces">
|
||||
<HintPath>$(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="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -113,22 +123,27 @@
|
|||
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewValley">
|
||||
<HintPath>$(GamePath)\StardewValley.exe</HintPath>
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewModdingAPI">
|
||||
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
|
||||
<Private>false</Private>
|
||||
<Private Condition="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
<Reference Include="StardewModdingAPI.Toolkit.CoreInterfaces">
|
||||
<HintPath>$(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="$(ModUnitTests)">true</Private>
|
||||
<Private Condition="$(CopyModReferencesToBuildOutput)">true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
|
@ -160,6 +175,7 @@
|
|||
ProjectDir="$(ProjectDir)"
|
||||
TargetDir="$(TargetDir)"
|
||||
GameDir="$(GamePath)"
|
||||
IgnoreModFilePatterns="$(IgnoreModFilePatterns)"
|
||||
/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Pathoschild.Stardew.ModBuildConfig</id>
|
||||
<version>2.1-alpha20180410</version>
|
||||
<version>2.1.0</version>
|
||||
<title>Build package for SMAPI mods</title>
|
||||
<authors>Pathoschild</authors>
|
||||
<owners>Pathoschild</owners>
|
||||
|
@ -10,12 +10,15 @@
|
|||
<licenseUrl>https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt</licenseUrl>
|
||||
<projectUrl>https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme</projectUrl>
|
||||
<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.</description>
|
||||
<description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later.</description>
|
||||
<releaseNotes>
|
||||
2.1:
|
||||
- Added support for Stardew Valley 1.3.
|
||||
- Added support for unit test projects.
|
||||
- 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.
|
||||
</releaseNotes>
|
||||
</metadata>
|
||||
</package>
|
||||
|
|
|
@ -15,14 +15,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <summary>Provides methods for searching and constructing items.</summary>
|
||||
private readonly ItemRepository Items = new ItemRepository();
|
||||
|
||||
/// <summary>The type names recognised by this command.</summary>
|
||||
private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray();
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public AddCommand()
|
||||
: base("player_add", AddCommand.GetDescription())
|
||||
{ }
|
||||
: base("player_add", AddCommand.GetDescription()) { }
|
||||
|
||||
/// <summary>Handle the command.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
|
@ -30,35 +32,34 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <param name="args">The command arguments.</param>
|
||||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// read arguments
|
||||
if (!args.TryGet(0, "item type", out string rawType, oneOf: Enum.GetNames(typeof(ItemType))))
|
||||
// validate
|
||||
if (!Context.IsWorldReady)
|
||||
{
|
||||
monitor.Log("You need to load a save to use this command.", LogLevel.Error);
|
||||
return;
|
||||
if (!args.TryGetInt(1, "item ID", out int id, min: 0))
|
||||
}
|
||||
|
||||
// read arguments
|
||||
if (!args.TryGet(0, "item type", out string type, oneOf: this.ValidTypes))
|
||||
return;
|
||||
if (!args.TryGetInt(2, "count", out int count, min: 1, required: false))
|
||||
count = 1;
|
||||
if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false))
|
||||
quality = Object.lowQuality;
|
||||
ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true);
|
||||
|
||||
// find matching item
|
||||
SearchableItem match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
|
||||
SearchableItem match = Enum.TryParse(type, true, out ItemType itemType)
|
||||
? this.FindItemByID(monitor, args, itemType)
|
||||
: this.FindItemByName(monitor, args);
|
||||
if (match == null)
|
||||
{
|
||||
monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// apply count
|
||||
match.Item.Stack = count;
|
||||
|
||||
// apply quality
|
||||
if (match.Item is Object obj)
|
||||
#if STARDEW_VALLEY_1_3
|
||||
obj.Quality = quality;
|
||||
#else
|
||||
obj.quality = quality;
|
||||
#endif
|
||||
else if (match.Item is Tool tool)
|
||||
tool.UpgradeLevel = quality;
|
||||
|
||||
|
@ -67,9 +68,60 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get a matching item by its ID.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
/// <param name="type">The item type.</param>
|
||||
private SearchableItem FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
|
||||
{
|
||||
// read arguments
|
||||
if (!args.TryGetInt(1, "item ID", out int id, min: 0))
|
||||
return null;
|
||||
|
||||
// find matching item
|
||||
SearchableItem item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
|
||||
if (item == null)
|
||||
monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>Get a matching item by its name.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
private SearchableItem FindItemByName(IMonitor monitor, ArgumentParser args)
|
||||
{
|
||||
// read arguments
|
||||
if (!args.TryGet(1, "item name", out string name))
|
||||
return null;
|
||||
|
||||
// find matching items
|
||||
SearchableItem[] matches = this.Items.GetAll().Where(p => p.NameContains(name)).ToArray();
|
||||
if (!matches.Any())
|
||||
{
|
||||
monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle single exact match
|
||||
SearchableItem[] exactMatches = matches.Where(p => p.NameEquivalentTo(name)).ToArray();
|
||||
if (exactMatches.Length == 1)
|
||||
return exactMatches[0];
|
||||
|
||||
// handle ambiguous results
|
||||
string options = this.GetTableString(
|
||||
data: matches,
|
||||
header: new[] { "type", "name", "command" },
|
||||
getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" }
|
||||
);
|
||||
monitor.Log($"There's no item with name '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Get the command description.</summary>
|
||||
private static string GetDescription()
|
||||
{
|
||||
string[] typeValues = Enum.GetNames(typeof(ItemType));
|
||||
|
@ -81,8 +133,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
+ "- count (optional): how many of the item to give.\n"
|
||||
+ $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n"
|
||||
+ "\n"
|
||||
+ "This example adds the galaxy sword to your inventory:\n"
|
||||
+ " player_add weapon 4";
|
||||
+ "Usage: player_add name \"<name>\" [count] [quality]\n"
|
||||
+ "- name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items.\n"
|
||||
+ "- count (optional): how many of the item to give.\n"
|
||||
+ $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n"
|
||||
+ "\n"
|
||||
+ "These examples both add the galaxy sword to your inventory:\n"
|
||||
+ " player_add weapon 4\n"
|
||||
+ " player_add name \"Galaxy Sword\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,11 +36,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
switch (target)
|
||||
{
|
||||
case "hair":
|
||||
#if STARDEW_VALLEY_1_3
|
||||
Game1.player.hairstyleColor.Value = color;
|
||||
#else
|
||||
Game1.player.hairstyleColor = color;
|
||||
#endif
|
||||
monitor.Log("OK, your hair color is updated.", LogLevel.Info);
|
||||
break;
|
||||
|
||||
|
@ -50,11 +46,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
break;
|
||||
|
||||
case "pants":
|
||||
#if STARDEW_VALLEY_1_3
|
||||
Game1.player.pantsColor.Value = color;
|
||||
#else
|
||||
Game1.player.pantsColor = color;
|
||||
#endif
|
||||
monitor.Log("OK, your pants color is updated.", LogLevel.Info);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using StardewValley;
|
||||
using SFarmer = StardewValley.Farmer;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's current level for a skill.</summary>
|
||||
internal class SetLevelCommand : TrainerCommand
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The experience points needed to reach each level.</summary>
|
||||
/// <remarks>Derived from <see cref="SFarmer.checkForLevelGain"/>.</remarks>
|
||||
private readonly IDictionary<int, int> LevelExp = new Dictionary<int, int>
|
||||
{
|
||||
[0] = 0,
|
||||
[1] = 100,
|
||||
[2] = 380,
|
||||
[3] = 770,
|
||||
[4] = 1300,
|
||||
[5] = 2150,
|
||||
[6] = 3300,
|
||||
[7] = 4800,
|
||||
[8] = 6900,
|
||||
[9] = 10000,
|
||||
[10] = 15000
|
||||
};
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public SetLevelCommand()
|
||||
: base("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel <skill> <value>\n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).") { }
|
||||
|
||||
/// <summary>Handle the command.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="command">The command name.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// validate
|
||||
if (!args.TryGet(0, "skill", out string skill, oneOf: new[] { "luck", "mining", "combat", "farming", "fishing", "foraging" }))
|
||||
return;
|
||||
if (!args.TryGetInt(1, "level", out int level, min: 0, max: 10))
|
||||
return;
|
||||
|
||||
// handle
|
||||
switch (skill)
|
||||
{
|
||||
case "luck":
|
||||
Game1.player.LuckLevel = level;
|
||||
Game1.player.experiencePoints[SFarmer.luckSkill] = this.LevelExp[level];
|
||||
monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info);
|
||||
break;
|
||||
|
||||
case "mining":
|
||||
Game1.player.MiningLevel = level;
|
||||
Game1.player.experiencePoints[SFarmer.miningSkill] = this.LevelExp[level];
|
||||
monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info);
|
||||
break;
|
||||
|
||||
case "combat":
|
||||
Game1.player.CombatLevel = level;
|
||||
Game1.player.experiencePoints[SFarmer.combatSkill] = this.LevelExp[level];
|
||||
monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info);
|
||||
break;
|
||||
|
||||
case "farming":
|
||||
Game1.player.FarmingLevel = level;
|
||||
Game1.player.experiencePoints[SFarmer.farmingSkill] = this.LevelExp[level];
|
||||
monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info);
|
||||
break;
|
||||
|
||||
case "fishing":
|
||||
Game1.player.FishingLevel = level;
|
||||
Game1.player.experiencePoints[SFarmer.fishingSkill] = this.LevelExp[level];
|
||||
monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info);
|
||||
break;
|
||||
|
||||
case "foraging":
|
||||
Game1.player.ForagingLevel = level;
|
||||
Game1.player.experiencePoints[SFarmer.foragingSkill] = this.LevelExp[level];
|
||||
monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,11 +39,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
case "farm":
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
#if STARDEW_VALLEY_1_3
|
||||
Game1.player.farmName.Value = args[1];
|
||||
#else
|
||||
Game1.player.farmName = args[1];
|
||||
#endif
|
||||
monitor.Log($"OK, your farm's name is now {Game1.player.farmName}.", LogLevel.Info);
|
||||
}
|
||||
else
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's current added speed.</summary>
|
||||
internal class SetSpeedCommand : TrainerCommand
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public SetSpeedCommand()
|
||||
: base("player_setspeed", "Sets the player's added speed to the specified value.\n\nUsage: player_setspeed <value>\n- value: an integer amount (0 is normal).") { }
|
||||
|
||||
/// <summary>Handle the command.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="command">The command name.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// parse arguments
|
||||
if (!args.TryGetInt(0, "added speed", out int amount, min: 0))
|
||||
return;
|
||||
|
||||
// handle
|
||||
Game1.player.addedSpeed = amount;
|
||||
monitor.Log($"OK, your added speed is now {Game1.player.addedSpeed}.", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using StardewValley;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public SetStyleCommand()
|
||||
: base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor <target> <value>.\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { }
|
||||
: base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changestyle <target> <value>.\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { }
|
||||
|
||||
/// <summary>Handle the command.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
|
|
|
@ -21,11 +21,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
{
|
||||
int level = (Game1.currentLocation as MineShaft)?.mineLevel ?? 0;
|
||||
monitor.Log($"OK, warping you to mine level {level + 1}.", LogLevel.Info);
|
||||
#if STARDEW_VALLEY_1_3
|
||||
Game1.enterMine(level + 1);
|
||||
#else
|
||||
Game1.enterMine(false, level + 1, "");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,11 +26,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
// handle
|
||||
level = Math.Max(1, level);
|
||||
monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info);
|
||||
#if STARDEW_VALLEY_1_3
|
||||
Game1.enterMine(level);
|
||||
#else
|
||||
Game1.enterMine(true, level, "");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
|
@ -38,7 +38,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
return;
|
||||
|
||||
// handle
|
||||
Game1.currentSeason = season;
|
||||
Game1.currentSeason = season.ToLower();
|
||||
Game1.setGraphicsForSeason();
|
||||
monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
|
@ -31,9 +32,38 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
return;
|
||||
|
||||
// handle
|
||||
Game1.timeOfDay = time;
|
||||
this.SafelySetTime(time);
|
||||
FreezeTimeCommand.FrozenTime = Game1.timeOfDay;
|
||||
monitor.Log($"OK, the time is now {Game1.timeOfDay.ToString().PadLeft(4, '0')}.", LogLevel.Info);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Safely transition to the given time, allowing NPCs to update their schedule.</summary>
|
||||
/// <param name="time">The time of day.</param>
|
||||
private void SafelySetTime(int time)
|
||||
{
|
||||
// define conversion between game time and TimeSpan
|
||||
TimeSpan ToTimeSpan(int value) => new TimeSpan(0, value / 100, value % 100, 0);
|
||||
int FromTimeSpan(TimeSpan span) => (int)((span.Hours * 100) + span.Minutes);
|
||||
|
||||
// transition to new time
|
||||
int intervals = (int)((ToTimeSpan(time) - ToTimeSpan(Game1.timeOfDay)).TotalMinutes / 10);
|
||||
if (intervals > 0)
|
||||
{
|
||||
for (int i = 0; i < intervals; i++)
|
||||
Game1.performTenMinuteClockUpdate();
|
||||
}
|
||||
else if (intervals < 0)
|
||||
{
|
||||
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.performTenMinuteClockUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using StardewValley;
|
||||
using System;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
|
||||
{
|
||||
|
@ -37,5 +38,23 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
|
|||
this.ID = id;
|
||||
this.Item = item;
|
||||
}
|
||||
|
||||
/// <summary>Get whether the item name contains a case-insensitive substring.</summary>
|
||||
/// <param name="substring">The substring to find.</param>
|
||||
public bool NameContains(string substring)
|
||||
{
|
||||
return
|
||||
this.Name.IndexOf(substring, StringComparison.InvariantCultureIgnoreCase) != -1
|
||||
|| this.DisplayName.IndexOf(substring, StringComparison.InvariantCultureIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
/// <summary>Get whether the item name is exactly equal to a case-insensitive string.</summary>
|
||||
/// <param name="name">The substring to find.</param>
|
||||
public bool NameEquivalentTo(string name)
|
||||
{
|
||||
return
|
||||
this.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)
|
||||
|| this.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,9 +83,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
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
|
||||
|
||||
|
@ -96,28 +106,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
if (item.category == SObject.FruitsCategory)
|
||||
{
|
||||
// wine
|
||||
#if STARDEW_VALLEY_1_3
|
||||
SObject wine =
|
||||
new SObject(348, 1)
|
||||
SObject wine = new SObject(348, 1)
|
||||
{
|
||||
Name = $"{item.Name} Wine",
|
||||
Price = item.price * 3
|
||||
};
|
||||
wine.preserve.Value = SObject.PreserveType.Wine;
|
||||
wine.preservedParentSheetIndex.Value = item.parentSheetIndex;
|
||||
#else
|
||||
SObject wine = new SObject(348, 1)
|
||||
{
|
||||
name = $"{item.Name} Wine",
|
||||
price = item.price * 3,
|
||||
preserve = SObject.PreserveType.Wine,
|
||||
preservedParentSheetIndex = item.parentSheetIndex
|
||||
};
|
||||
#endif
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, wine);
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, wine);
|
||||
|
||||
// jelly
|
||||
#if STARDEW_VALLEY_1_3
|
||||
SObject jelly = new SObject(344, 1)
|
||||
{
|
||||
Name = $"{item.Name} Jelly",
|
||||
|
@ -125,23 +123,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
};
|
||||
jelly.preserve.Value = SObject.PreserveType.Jelly;
|
||||
jelly.preservedParentSheetIndex.Value = item.parentSheetIndex;
|
||||
#else
|
||||
SObject jelly = new SObject(344, 1)
|
||||
{
|
||||
name = $"{item.Name} Jelly",
|
||||
price = 50 + item.Price * 2,
|
||||
preserve = SObject.PreserveType.Jelly,
|
||||
preservedParentSheetIndex = item.parentSheetIndex
|
||||
};
|
||||
#endif
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, jelly);
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, jelly);
|
||||
}
|
||||
|
||||
// vegetable products
|
||||
else if (item.category == SObject.VegetableCategory)
|
||||
{
|
||||
// juice
|
||||
#if STARDEW_VALLEY_1_3
|
||||
SObject juice = new SObject(350, 1)
|
||||
{
|
||||
Name = $"{item.Name} Juice",
|
||||
|
@ -149,19 +137,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
};
|
||||
juice.preserve.Value = SObject.PreserveType.Juice;
|
||||
juice.preservedParentSheetIndex.Value = item.parentSheetIndex;
|
||||
#else
|
||||
SObject juice = new SObject(350, 1)
|
||||
{
|
||||
name = $"{item.Name} Juice",
|
||||
price = (int)(item.price * 2.25d),
|
||||
preserve = SObject.PreserveType.Juice,
|
||||
preservedParentSheetIndex = item.parentSheetIndex
|
||||
};
|
||||
#endif
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, juice);
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, juice);
|
||||
|
||||
// pickled
|
||||
#if STARDEW_VALLEY_1_3
|
||||
SObject pickled = new SObject(342, 1)
|
||||
{
|
||||
Name = $"Pickled {item.Name}",
|
||||
|
@ -169,16 +147,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
};
|
||||
pickled.preserve.Value = SObject.PreserveType.Pickle;
|
||||
pickled.preservedParentSheetIndex.Value = item.parentSheetIndex;
|
||||
#else
|
||||
SObject pickled = new SObject(342, 1)
|
||||
{
|
||||
name = $"Pickled {item.Name}",
|
||||
price = 50 + item.Price * 2,
|
||||
preserve = SObject.PreserveType.Pickle,
|
||||
preservedParentSheetIndex = item.parentSheetIndex
|
||||
};
|
||||
#endif
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, pickled);
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, pickled);
|
||||
}
|
||||
|
||||
// flower honey
|
||||
|
@ -211,7 +180,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
// yield honey
|
||||
if (type != null)
|
||||
{
|
||||
#if STARDEW_VALLEY_1_3
|
||||
SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false)
|
||||
{
|
||||
Name = "Wild Honey"
|
||||
|
@ -223,18 +191,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
honey.Name = $"{item.Name} Honey";
|
||||
honey.Price += item.Price * 2;
|
||||
}
|
||||
#else
|
||||
SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false)
|
||||
{
|
||||
name = "Wild Honey",
|
||||
honeyType = type
|
||||
};
|
||||
if (type != SObject.HoneyType.Wild)
|
||||
{
|
||||
honey.name = $"{item.Name} Honey";
|
||||
honey.price += item.price * 2;
|
||||
}
|
||||
#endif
|
||||
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, honey);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands;
|
|||
namespace StardewModdingAPI.Mods.ConsoleCommands
|
||||
{
|
||||
/// <summary>The main entry point for the mod.</summary>
|
||||
public class ConsoleCommandsMod : Mod
|
||||
public class ModEntry : Mod
|
||||
{
|
||||
/*********
|
||||
** Properties
|
|
@ -38,7 +38,7 @@
|
|||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Xml" />
|
||||
|
@ -56,8 +56,6 @@
|
|||
<Compile Include="Framework\Commands\Player\AddCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetStyleCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetColorCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetSpeedCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetLevelCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetMaxHealthCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetMaxStaminaCommand.cs" />
|
||||
<Compile Include="Framework\Commands\Player\SetHealthCommand.cs" />
|
||||
|
@ -77,7 +75,7 @@
|
|||
<Compile Include="Framework\Commands\ITrainerCommand.cs" />
|
||||
<Compile Include="Framework\ItemData\SearchableItem.cs" />
|
||||
<Compile Include="Framework\ItemRepository.cs" />
|
||||
<Compile Include="ConsoleCommandsMod.cs" />
|
||||
<Compile Include="ModEntry.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "2.5.5",
|
||||
"Version": "2.6.0",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
namespace StardewModdingAPI.Mods.SaveBackup.Framework
|
||||
{
|
||||
/// <summary>The mod configuration.</summary>
|
||||
internal class ModConfig
|
||||
{
|
||||
/// <summary>The number of backups to keep.</summary>
|
||||
public int BackupsToKeep { get; set; } = 10;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using StardewModdingAPI.Mods.SaveBackup.Framework;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.SaveBackup
|
||||
{
|
||||
/// <summary>The main entry point for the mod.</summary>
|
||||
public class ModEntry : Mod
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The name of the save archive to create.</summary>
|
||||
private readonly string FileName = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}.zip";
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
|
||||
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||
public override void Entry(IModHelper helper)
|
||||
{
|
||||
try
|
||||
{
|
||||
ModConfig config = this.Helper.ReadConfig<ModConfig>();
|
||||
|
||||
// init backup folder
|
||||
DirectoryInfo backupFolder = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "backups"));
|
||||
backupFolder.Create();
|
||||
|
||||
// back up saves
|
||||
this.CreateBackup(backupFolder);
|
||||
this.PruneBackups(backupFolder, config.BackupsToKeep);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"Error backing up saves: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Back up the current saves.</summary>
|
||||
/// <param name="backupFolder">The folder containing save backups.</param>
|
||||
private void CreateBackup(DirectoryInfo backupFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
// get target path
|
||||
FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName));
|
||||
if (targetFile.Exists)
|
||||
targetFile.Delete(); //return;
|
||||
|
||||
// create zip
|
||||
// due to limitations with the bundled Mono on Mac, we can't reference System.IO.Compression.
|
||||
this.Monitor.Log($"Adding {targetFile.Name}...", LogLevel.Trace);
|
||||
switch (Constants.TargetPlatform)
|
||||
{
|
||||
case GamePlatform.Linux:
|
||||
case GamePlatform.Windows:
|
||||
{
|
||||
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 });
|
||||
}
|
||||
break;
|
||||
|
||||
case GamePlatform.Mac:
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log("Couldn't back up save files (see log file for details).", LogLevel.Warn);
|
||||
this.Monitor.Log(ex.ToString(), LogLevel.Trace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Remove old backups if we've exceeded the limit.</summary>
|
||||
/// <param name="backupFolder">The folder containing save backups.</param>
|
||||
/// <param name="backupsToKeep">The number of backups to keep.</param>
|
||||
private void PruneBackups(DirectoryInfo backupFolder, int backupsToKeep)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oldBackups = backupFolder
|
||||
.GetFiles()
|
||||
.OrderByDescending(p => p.CreationTimeUtc)
|
||||
.Skip(backupsToKeep);
|
||||
|
||||
foreach (FileInfo file in oldBackups)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace);
|
||||
file.Delete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log("Couldn't remove old backups (see log file for details).", LogLevel.Warn);
|
||||
this.Monitor.Log(ex.ToString(), LogLevel.Trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyTitle("StardewModdingAPI.Mods.SaveBackup")]
|
||||
[assembly: AssemblyDescription("")]
|
|
@ -1,30 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
|
||||
<ProjectGuid>{10DB0676-9FC1-4771-A2C8-E2519F091E49}</ProjectGuid>
|
||||
<ProjectGuid>{E272EB5D-8C57-417E-8E60-C1079D3F53C4}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>StardewModdingAPI.AssemblyRewriters</RootNamespace>
|
||||
<AssemblyName>StardewModdingAPI.AssemblyRewriters</AssemblyName>
|
||||
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
|
||||
<AssemblyName>SaveBackup</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<OutputPath>$(SolutionDir)\..\bin\Debug\Mods\SaveBackup\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<OutputPath>$(SolutionDir)\..\bin\Release\Mods\SaveBackup\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
|
@ -36,8 +36,26 @@
|
|||
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
|
||||
<Link>Properties\GlobalAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Framework\ModConfig.cs" />
|
||||
<Compile Include="ModEntry.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SpriteBatchMethods.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="manifest.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI\StardewModdingAPI.csproj">
|
||||
<Project>{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}</Project>
|
||||
<Name>StardewModdingAPI</Name>
|
||||
<Private>False</Private>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj">
|
||||
<Project>{d5cfd923-37f1-4bc3-9be8-e506e202ac28}</Project>
|
||||
<Name>StardewModdingAPI.Toolkit.CoreInterfaces</Name>
|
||||
<Private>False</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\build\common.targets" />
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "2.6.0",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll"
|
||||
}
|
|
@ -6,10 +6,10 @@ using Moq;
|
|||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using StardewModdingAPI.Framework;
|
||||
using StardewModdingAPI.Framework.ModData;
|
||||
using StardewModdingAPI.Framework.Models;
|
||||
using StardewModdingAPI.Framework.ModLoading;
|
||||
using StardewModdingAPI.Framework.Serialisation;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||
using StardewModdingAPI.Toolkit.Serialisation.Models;
|
||||
|
||||
namespace StardewModdingAPI.Tests.Core
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
Directory.CreateDirectory(rootFolder);
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray();
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
|
||||
|
@ -46,7 +46,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
Directory.CreateDirectory(modFolder);
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray();
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
|
||||
IModMetadata mod = mods.FirstOrDefault();
|
||||
|
||||
// assert
|
||||
|
@ -85,7 +85,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
File.WriteAllText(filename, JsonConvert.SerializeObject(original));
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray();
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
|
||||
IModMetadata mod = mods.FirstOrDefault();
|
||||
|
||||
// assert
|
||||
|
@ -93,8 +93,8 @@ namespace StardewModdingAPI.Tests.Core
|
|||
Assert.IsNotNull(mod, "The loaded manifest shouldn't be null.");
|
||||
Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one.");
|
||||
Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match.");
|
||||
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
|
||||
Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded.");
|
||||
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
|
||||
|
||||
Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name.");
|
||||
Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match.");
|
||||
|
@ -142,7 +142,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
|
||||
this.SetupMetadataForValidation(mock, new ParsedModDataRecord
|
||||
this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields
|
||||
{
|
||||
Status = ModStatus.AssumeBroken,
|
||||
AlternativeUrl = "http://example.org"
|
||||
|
@ -160,7 +160,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
|
||||
mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1")));
|
||||
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
|
||||
this.SetupMetadataForValidation(mock);
|
||||
|
||||
// act
|
||||
|
@ -174,7 +174,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
public void ValidateManifests_MissingEntryDLL_Fails()
|
||||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.EntryDll = "Missing.dll"), allowStatusChange: true);
|
||||
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true);
|
||||
this.SetupMetadataForValidation(mock);
|
||||
|
||||
// act
|
||||
|
@ -189,7 +189,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.Name = "Mod B"), allowStatusChange: true);
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
|
||||
Mock<IModMetadata> modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false);
|
||||
foreach (Mock<IModMetadata> mod in new[] { modA, modB, modC })
|
||||
this.SetupMetadataForValidation(mod);
|
||||
|
@ -398,8 +398,8 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
// A 1.0 ◀── B (need A 1.1)
|
||||
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true);
|
||||
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0"));
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.1") }), allowStatusChange: true);
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray();
|
||||
|
@ -414,8 +414,8 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
// A 1.0 ◀── B (need A 1.0-beta)
|
||||
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false);
|
||||
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0"));
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0-beta") }), allowStatusChange: false);
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray();
|
||||
|
@ -431,8 +431,8 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
// A ◀── B
|
||||
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false);
|
||||
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0"));
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false);
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }, new ModDatabase()).ToArray();
|
||||
|
@ -448,7 +448,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
{
|
||||
// arrange
|
||||
// A ◀── B where A doesn't exist
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false);
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false);
|
||||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }, new ModDatabase()).ToArray();
|
||||
|
@ -463,46 +463,27 @@ namespace StardewModdingAPI.Tests.Core
|
|||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get a randomised basic manifest.</summary>
|
||||
/// <param name="adjust">Adjust the generated manifest.</param>
|
||||
private Manifest GetManifest(Action<Manifest> adjust = null)
|
||||
/// <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="version">The <see cref="IManifest.Version"/> value, or <c>null</c> for a generated value.</param>
|
||||
/// <param name="entryDll">The <see cref="IManifest.EntryDll"/> value, or <c>null</c> for a generated value.</param>
|
||||
/// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param>
|
||||
/// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param>
|
||||
/// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param>
|
||||
private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null)
|
||||
{
|
||||
Manifest manifest = new Manifest
|
||||
return new Manifest
|
||||
{
|
||||
Name = Sample.String(),
|
||||
UniqueID = id ?? $"{Sample.String()}.{Sample.String()}",
|
||||
Name = name ?? id ?? Sample.String(),
|
||||
Author = Sample.String(),
|
||||
Version = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
|
||||
Description = Sample.String(),
|
||||
UniqueID = $"{Sample.String()}.{Sample.String()}",
|
||||
EntryDll = $"{Sample.String()}.dll"
|
||||
Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
|
||||
EntryDll = entryDll ?? $"{Sample.String()}.dll",
|
||||
ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null,
|
||||
MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null,
|
||||
Dependencies = dependencies
|
||||
};
|
||||
adjust?.Invoke(manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/// <summary>Get a randomised basic manifest.</summary>
|
||||
/// <param name="uniqueID">The mod's name and unique ID.</param>
|
||||
/// <param name="version">The mod version.</param>
|
||||
/// <param name="adjust">Adjust the generated manifest.</param>
|
||||
/// <param name="dependencies">The dependencies this mod requires.</param>
|
||||
private IManifest GetManifest(string uniqueID, string version, Action<Manifest> adjust, params IManifestDependency[] dependencies)
|
||||
{
|
||||
return this.GetManifest(manifest =>
|
||||
{
|
||||
manifest.Name = uniqueID;
|
||||
manifest.UniqueID = uniqueID;
|
||||
manifest.Version = new SemanticVersion(version);
|
||||
manifest.Dependencies = dependencies;
|
||||
adjust?.Invoke(manifest);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Get a randomised basic manifest.</summary>
|
||||
/// <param name="uniqueID">The mod's name and unique ID.</param>
|
||||
/// <param name="version">The mod version.</param>
|
||||
/// <param name="dependencies">The dependencies this mod requires.</param>
|
||||
private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies)
|
||||
{
|
||||
return this.GetManifest(uniqueID, version, null, dependencies);
|
||||
}
|
||||
|
||||
/// <summary>Get a randomised basic manifest.</summary>
|
||||
|
@ -518,7 +499,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
|
||||
private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false)
|
||||
{
|
||||
IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray());
|
||||
IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray());
|
||||
return this.GetMetadata(manifest, allowStatusChange);
|
||||
}
|
||||
|
||||
|
@ -545,7 +526,7 @@ namespace StardewModdingAPI.Tests.Core
|
|||
/// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
|
||||
/// <param name="mod">The mock mod metadata.</param>
|
||||
/// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param>
|
||||
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ParsedModDataRecord modRecord = null)
|
||||
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields modRecord = null)
|
||||
{
|
||||
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
|
||||
mod.Setup(p => p.DataRecord).Returns(() => null);
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll</HintPath>
|
||||
<HintPath>..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Moq, Version=4.8.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Moq.4.8.2\lib\net45\Moq.dll</HintPath>
|
||||
<HintPath>..\packages\Moq.4.8.3\lib\net45\Moq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
|
@ -45,18 +45,21 @@
|
|||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Threading.Tasks.Extensions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll</HintPath>
|
||||
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.1\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
|
||||
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
|
||||
<Link>Properties\GlobalAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Core\PathUtilitiesTests.cs" />
|
||||
<Compile Include="Toolkit\PathUtilitiesTests.cs" />
|
||||
<Compile Include="Utilities\SemanticVersionTests.cs" />
|
||||
<Compile Include="Utilities\SDateTests.cs" />
|
||||
<Compile Include="Core\TranslationTests.cs" />
|
||||
|
@ -73,6 +76,14 @@
|
|||
<Project>{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}</Project>
|
||||
<Name>StardewModdingAPI</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj">
|
||||
<Project>{d5cfd923-37f1-4bc3-9be8-e506e202ac28}</Project>
|
||||
<Name>StardewModdingAPI.Toolkit.CoreInterfaces</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj">
|
||||
<Project>{ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6}</Project>
|
||||
<Name>StardewModdingAPI.Toolkit</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using NUnit.Framework;
|
||||
using StardewModdingAPI.Framework.Utilities;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
||||
namespace StardewModdingAPI.Tests.Core
|
||||
namespace StardewModdingAPI.Tests.Toolkit
|
||||
{
|
||||
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
|
||||
[TestFixture]
|
|
@ -22,6 +22,7 @@ namespace StardewModdingAPI.Tests.Utilities
|
|||
[TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")]
|
||||
[TestCase("1.2-some-tag.4", ExpectedResult = "1.2-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")]
|
||||
public string Constructor_FromString(string input)
|
||||
{
|
||||
|
@ -35,6 +36,7 @@ namespace StardewModdingAPI.Tests.Utilities
|
|||
[TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, "0", 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", ExpectedResult = "1.2.3-sOMe-TaG.4")]
|
||||
[TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")]
|
||||
public string Constructor_FromParts(int major, int minor, int patch, string tag)
|
||||
{
|
||||
|
@ -49,6 +51,19 @@ namespace StardewModdingAPI.Tests.Utilities
|
|||
return version.ToString();
|
||||
}
|
||||
|
||||
[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(-1, 0, 0, null)]
|
||||
[TestCase(0, -1, 0, null)]
|
||||
[TestCase(0, 0, -1, null)]
|
||||
[TestCase(1, 0, 0, "-tag")]
|
||||
[TestCase(1, 0, 0, "tag spaces")]
|
||||
[TestCase(1, 0, 0, "tag~")]
|
||||
public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string tag)
|
||||
{
|
||||
this.AssertAndLogException<FormatException>(() => new SemanticVersion(major, minor, patch, tag));
|
||||
}
|
||||
|
||||
[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, 2, 3, ExpectedResult = "1.2.3")]
|
||||
|
@ -79,6 +94,7 @@ namespace StardewModdingAPI.Tests.Utilities
|
|||
[TestCase("1.2.3.apple")]
|
||||
[TestCase("1..2..3")]
|
||||
[TestCase("1.2.3-")]
|
||||
[TestCase("1.2.3--some-tag")]
|
||||
[TestCase("1.2.3-some-tag...")]
|
||||
[TestCase("1.2.3-some-tag...4")]
|
||||
[TestCase("apple")]
|
||||
|
@ -271,22 +287,6 @@ namespace StardewModdingAPI.Tests.Utilities
|
|||
Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions.");
|
||||
}
|
||||
|
||||
/****
|
||||
** LegacyManifestVersion
|
||||
****/
|
||||
[Test(Description = "Assert that the LegacyManifestVersion subclass correctly parses legacy manifest versions.")]
|
||||
[TestCase(1, 0, 0, null, ExpectedResult = "1.0")]
|
||||
[TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")]
|
||||
[TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3")] // special case: drop '0' tag for legacy manifest versions
|
||||
[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")]
|
||||
public string LegacyManifestVersion(int major, int minor, int patch, string tag)
|
||||
{
|
||||
return new LegacyManifestVersion(major, minor, patch, tag).ToString();
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.2.0.0" newVersion="4.2.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Castle.Core" version="4.2.1" targetFramework="net45" />
|
||||
<package id="Moq" version="4.8.2" targetFramework="net45" />
|
||||
<package id="Castle.Core" version="4.3.1" targetFramework="net45" />
|
||||
<package id="Moq" version="4.8.3" targetFramework="net45" />
|
||||
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net45" />
|
||||
<package id="NUnit" version="3.10.1" targetFramework="net45" />
|
||||
<package id="System.Threading.Tasks.Extensions" version="4.4.0" targetFramework="net45" />
|
||||
<package id="System.ValueTuple" version="4.4.0" targetFramework="net45" />
|
||||
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.1" targetFramework="net45" />
|
||||
<package id="System.Threading.Tasks.Extensions" version="4.5.1" targetFramework="net45" />
|
||||
<package id="System.ValueTuple" version="4.5.0" targetFramework="net45" />
|
||||
</packages>
|
|
@ -1,10 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using StardewModdingAPI.Common;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.ViewModels;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers
|
||||
|
@ -17,6 +22,9 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The site config settings.</summary>
|
||||
private readonly SiteConfig SiteConfig;
|
||||
|
||||
/// <summary>The cache in which to store release data.</summary>
|
||||
private readonly IMemoryCache Cache;
|
||||
|
||||
|
@ -24,7 +32,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
private readonly IGitHubClient GitHub;
|
||||
|
||||
/// <summary>The cache time for release info.</summary>
|
||||
private readonly TimeSpan CacheTime = TimeSpan.FromSeconds(1);
|
||||
private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10);
|
||||
|
||||
/// <summary>The GitHub repository name to check for update.</summary>
|
||||
private readonly string RepositoryName = "Pathoschild/SMAPI";
|
||||
|
@ -36,34 +44,35 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="cache">The cache in which to store release data.</param>
|
||||
/// <param name="github">The GitHub API client.</param>
|
||||
public IndexController(IMemoryCache cache, IGitHubClient github)
|
||||
/// <param name="siteConfig">The context config settings.</param>
|
||||
public IndexController(IMemoryCache cache, IGitHubClient github, IOptions<SiteConfig> siteConfig)
|
||||
{
|
||||
this.Cache = cache;
|
||||
this.GitHub = github;
|
||||
this.SiteConfig = siteConfig.Value;
|
||||
}
|
||||
|
||||
/// <summary>Display the index page.</summary>
|
||||
[HttpGet]
|
||||
public async Task<ViewResult> Index()
|
||||
{
|
||||
// fetch SMAPI releases
|
||||
IndexVersionModel stableVersion = await this.Cache.GetOrCreateAsync("stable-version", async entry =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
|
||||
GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);
|
||||
return new IndexVersionModel(release.Name, release.Body, this.GetMainDownloadUrl(release), this.GetDevDownloadUrl(release));
|
||||
});
|
||||
IndexVersionModel betaVersion = await this.Cache.GetOrCreateAsync("beta-version", async entry =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
|
||||
GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true);
|
||||
return release.IsPrerelease
|
||||
? this.GetBetaDownload(release)
|
||||
: null;
|
||||
});
|
||||
// choose versions
|
||||
ReleaseVersion[] versions = await this.GetReleaseVersionsAsync();
|
||||
ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsBeta && !version.IsForDevs);
|
||||
ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => !version.IsBeta && version.IsForDevs);
|
||||
ReleaseVersion betaVersion = versions.LastOrDefault(version => version.IsBeta && !version.IsForDevs);
|
||||
ReleaseVersion betaVersionForDevs = versions.LastOrDefault(version => version.IsBeta && version.IsForDevs);
|
||||
|
||||
// render view
|
||||
var model = new IndexModel(stableVersion, betaVersion);
|
||||
IndexVersionModel stableVersionModel = stableVersion != null
|
||||
? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl)
|
||||
: new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong)
|
||||
IndexVersionModel betaVersionModel = betaVersion != null && this.SiteConfig.EnableSmapiBeta
|
||||
? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl)
|
||||
: null;
|
||||
|
||||
// render view
|
||||
var model = new IndexModel(stableVersionModel, betaVersionModel);
|
||||
return this.View(model);
|
||||
}
|
||||
|
||||
|
@ -71,62 +80,109 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the main download URL for a SMAPI release.</summary>
|
||||
/// <param name="release">The SMAPI release.</param>
|
||||
private string GetMainDownloadUrl(GitRelease release)
|
||||
/// <summary>Get a sorted, parsed list of SMAPI downloads for the latest releases.</summary>
|
||||
private async Task<ReleaseVersion[]> GetReleaseVersionsAsync()
|
||||
{
|
||||
// get main download URL
|
||||
foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
|
||||
return await this.Cache.GetOrCreateAsync("available-versions", async entry =>
|
||||
{
|
||||
if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer.zip"))
|
||||
return asset.DownloadUrl;
|
||||
}
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
|
||||
|
||||
// fallback just in case
|
||||
return "https://github.com/pathoschild/SMAPI/releases";
|
||||
}
|
||||
// get latest release (whether preview or stable)
|
||||
GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true);
|
||||
|
||||
/// <summary>Get the for-developers download URL for a SMAPI release.</summary>
|
||||
/// <param name="release">The SMAPI release.</param>
|
||||
private string GetDevDownloadUrl(GitRelease release)
|
||||
{
|
||||
// get dev download URL
|
||||
foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
|
||||
{
|
||||
if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer-for-developers.zip"))
|
||||
return asset.DownloadUrl;
|
||||
}
|
||||
|
||||
// fallback just in case
|
||||
return "https://github.com/pathoschild/SMAPI/releases";
|
||||
}
|
||||
|
||||
/// <summary>Get the latest beta download for a SMAPI release.</summary>
|
||||
/// <param name="release">The SMAPI release.</param>
|
||||
private IndexVersionModel GetBetaDownload(GitRelease release)
|
||||
{
|
||||
// get download with the latest version
|
||||
SemanticVersionImpl latestVersion = null;
|
||||
string latestUrl = null;
|
||||
foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
|
||||
{
|
||||
// parse version
|
||||
Match versionMatch = Regex.Match(asset.FileName, @"SMAPI-([\d\.]+(?:-.+)?)-installer.zip");
|
||||
if (!versionMatch.Success || !SemanticVersionImpl.TryParse(versionMatch.Groups[1].Value, out SemanticVersionImpl version))
|
||||
continue;
|
||||
|
||||
// save latest version
|
||||
if (latestVersion == null || latestVersion.CompareTo(version) < 0)
|
||||
// split stable/prerelease if applicable
|
||||
GitRelease betaRelease = null;
|
||||
if (stableRelease.IsPrerelease)
|
||||
{
|
||||
latestVersion = version;
|
||||
latestUrl = asset.DownloadUrl;
|
||||
GitRelease result = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);
|
||||
if (result != null)
|
||||
{
|
||||
betaRelease = stableRelease;
|
||||
stableRelease = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return if prerelease
|
||||
return latestVersion?.Tag != null
|
||||
? new IndexVersionModel(latestVersion.ToString(), release.Body, latestUrl, null)
|
||||
: null;
|
||||
// strip 'noinclude' blocks from release descriptions
|
||||
foreach (GitRelease release in new[] { stableRelease, betaRelease })
|
||||
{
|
||||
if (release == null)
|
||||
continue;
|
||||
|
||||
HtmlDocument doc = new HtmlDocument();
|
||||
doc.LoadHtml(release.Body);
|
||||
foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? new HtmlNode[0])
|
||||
node.Remove();
|
||||
release.Body = doc.DocumentNode.InnerHtml.Trim();
|
||||
}
|
||||
|
||||
// get versions
|
||||
ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray();
|
||||
ReleaseVersion[] betaVersions = this.ParseReleaseVersions(betaRelease).ToArray();
|
||||
return stableVersions
|
||||
.Concat(betaVersions)
|
||||
.OrderBy(p => p.Version)
|
||||
.ToArray();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Get a parsed list of SMAPI downloads for a release.</summary>
|
||||
/// <param name="release">The GitHub release.</param>
|
||||
private IEnumerable<ReleaseVersion> ParseReleaseVersions(GitRelease release)
|
||||
{
|
||||
if (release?.Assets == null)
|
||||
yield break;
|
||||
|
||||
foreach (GitAsset asset in release.Assets)
|
||||
{
|
||||
Match match = Regex.Match(asset.FileName, @"SMAPI-(?<version>[\d\.]+(?:-.+)?)-installer(?<forDevs>-for-developers)?.zip");
|
||||
if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version))
|
||||
continue;
|
||||
bool isBeta = version.IsPrerelease();
|
||||
bool isForDevs = match.Groups["forDevs"].Success;
|
||||
|
||||
yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A parsed release download.</summary>
|
||||
private class ReleaseVersion
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The underlying GitHub release.</summary>
|
||||
public GitRelease Release { get; }
|
||||
|
||||
/// <summary>The underlying download asset.</summary>
|
||||
public GitAsset Asset { get; }
|
||||
|
||||
/// <summary>The SMAPI version.</summary>
|
||||
public ISemanticVersion Version { get; }
|
||||
|
||||
/// <summary>Whether this is a beta download.</summary>
|
||||
public bool IsBeta { get; }
|
||||
|
||||
/// <summary>Whether this is a 'for developers' download.</summary>
|
||||
public bool IsForDevs { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="release">The underlying GitHub release.</param>
|
||||
/// <param name="asset">The underlying download asset.</param>
|
||||
/// <param name="version">The SMAPI version.</param>
|
||||
/// <param name="isBeta">Whether this is a beta download.</param>
|
||||
/// <param name="isForDevs">Whether this is a 'for developers' download.</param>
|
||||
public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isBeta, bool isForDevs)
|
||||
{
|
||||
this.Release = release;
|
||||
this.Asset = asset;
|
||||
this.Version = version;
|
||||
this.IsBeta = isBeta;
|
||||
this.IsForDevs = isForDevs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -20,8 +21,8 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The log parser config settings.</summary>
|
||||
private readonly ContextConfig Config;
|
||||
/// <summary>The site config settings.</summary>
|
||||
private readonly SiteConfig Config;
|
||||
|
||||
/// <summary>The underlying Pastebin client.</summary>
|
||||
private readonly IPastebinClient Pastebin;
|
||||
|
@ -38,11 +39,11 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
** Constructor
|
||||
***/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="contextProvider">The context config settings.</param>
|
||||
/// <param name="siteConfig">The context config settings.</param>
|
||||
/// <param name="pastebin">The Pastebin API client.</param>
|
||||
public LogParserController(IOptions<ContextConfig> contextProvider, IPastebinClient pastebin)
|
||||
public LogParserController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin)
|
||||
{
|
||||
this.Config = contextProvider.Value;
|
||||
this.Config = siteConfig.Value;
|
||||
this.Pastebin = pastebin;
|
||||
}
|
||||
|
||||
|
@ -51,34 +52,49 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
***/
|
||||
/// <summary>Render the log parser UI.</summary>
|
||||
/// <param name="id">The paste ID.</param>
|
||||
/// <param name="raw">Whether to display the raw unparsed log.</param>
|
||||
[HttpGet]
|
||||
[Route("log")]
|
||||
[Route("log/{id}")]
|
||||
public async Task<ViewResult> Index(string id = null)
|
||||
public async Task<ViewResult> Index(string id = null, bool raw = false)
|
||||
{
|
||||
// fresh page
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null));
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
|
||||
|
||||
// log page
|
||||
PasteInfo paste = await this.GetAsync(id);
|
||||
ParsedLog log = paste.Success
|
||||
? new LogParser().Parse(paste.Content)
|
||||
: new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log));
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log, raw));
|
||||
}
|
||||
|
||||
/***
|
||||
** JSON
|
||||
***/
|
||||
/// <summary>Save raw log data.</summary>
|
||||
/// <param name="content">The log content to save.</param>
|
||||
[HttpPost, Produces("application/json"), AllowLargePosts]
|
||||
[Route("log/save")]
|
||||
public async Task<SavePasteResult> PostAsync([FromBody] string content)
|
||||
[HttpPost, AllowLargePosts]
|
||||
[Route("log")]
|
||||
public async Task<ActionResult> PostAsync()
|
||||
{
|
||||
content = this.CompressString(content);
|
||||
return await this.Pastebin.PostAsync(content);
|
||||
// get raw log text
|
||||
string input = this.Request.Form["input"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." });
|
||||
|
||||
// upload log
|
||||
input = this.CompressString(input);
|
||||
SavePasteResult result = await this.Pastebin.PostAsync(input);
|
||||
|
||||
// handle errors
|
||||
if (!result.Success)
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" });
|
||||
|
||||
// redirect to view
|
||||
UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl));
|
||||
uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID;
|
||||
return this.Redirect(uri.Uri.ToString());
|
||||
}
|
||||
|
||||
|
||||
|
@ -115,7 +131,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
}
|
||||
|
||||
// prefix length
|
||||
var zipBuffer = new byte[compressedData.Length + 4];
|
||||
byte[] zipBuffer = new byte[compressedData.Length + 4];
|
||||
Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4);
|
||||
|
||||
|
@ -151,7 +167,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4);
|
||||
|
||||
// read data
|
||||
var buffer = new byte[dataLength];
|
||||
byte[] buffer = new byte[dataLength];
|
||||
memoryStream.Position = 0;
|
||||
using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
|
||||
gZipStream.Read(buffer, 0, buffer.Length);
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
|
@ -38,19 +43,28 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>A regex which matches SMAPI-style semantic version.</summary>
|
||||
private readonly string VersionRegex;
|
||||
|
||||
/// <summary>The internal mod metadata list.</summary>
|
||||
private readonly ModDatabase ModDatabase;
|
||||
|
||||
/// <summary>The web URL for the wiki compatibility list.</summary>
|
||||
private readonly string WikiCompatibilityPageUrl;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="environment">The web hosting environment.</param>
|
||||
/// <param name="cache">The cache in which to store mod metadata.</param>
|
||||
/// <param name="configProvider">The config settings for mod update checks.</param>
|
||||
/// <param name="chucklefish">The Chucklefish API client.</param>
|
||||
/// <param name="github">The GitHub API client.</param>
|
||||
/// <param name="nexus">The Nexus API client.</param>
|
||||
public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
|
||||
public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
|
||||
{
|
||||
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json"));
|
||||
ModUpdateCheckConfig config = configProvider.Value;
|
||||
this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl;
|
||||
|
||||
this.Cache = cache;
|
||||
this.SuccessCacheMinutes = config.SuccessCacheMinutes;
|
||||
|
@ -67,76 +81,126 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
}
|
||||
|
||||
/// <summary>Fetch version metadata for the given mods.</summary>
|
||||
/// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param>
|
||||
/// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param>
|
||||
[HttpGet]
|
||||
public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys, bool allowInvalidVersions = false)
|
||||
{
|
||||
string[] modKeysArray = modKeys?.Split(',').ToArray();
|
||||
if (modKeysArray == null || !modKeysArray.Any())
|
||||
return new Dictionary<string, ModInfoModel>();
|
||||
|
||||
return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions));
|
||||
}
|
||||
|
||||
/// <summary>Fetch version metadata for the given mods.</summary>
|
||||
/// <param name="search">The mod search criteria.</param>
|
||||
/// <param name="model">The mod search criteria.</param>
|
||||
[HttpPost]
|
||||
public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search)
|
||||
public async Task<object> PostAsync([FromBody] ModSearchModel model)
|
||||
{
|
||||
// parse model
|
||||
bool allowInvalidVersions = search?.AllowInvalidVersions ?? false;
|
||||
string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0])
|
||||
.Distinct(StringComparer.CurrentCultureIgnoreCase)
|
||||
.OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToArray();
|
||||
// parse request data
|
||||
ISemanticVersion apiVersion = this.GetApiVersion();
|
||||
ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray();
|
||||
|
||||
// fetch mod info
|
||||
IDictionary<string, ModInfoModel> result = new Dictionary<string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase);
|
||||
foreach (string modKey in modKeys)
|
||||
// fetch wiki data
|
||||
WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync();
|
||||
|
||||
// fetch data
|
||||
IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
|
||||
foreach (ModSearchEntryModel mod in searchMods)
|
||||
{
|
||||
// parse mod key
|
||||
if (!this.TryParseModKey(modKey, out string vendorKey, out string modID))
|
||||
{
|
||||
result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
|
||||
if (string.IsNullOrWhiteSpace(mod.ID))
|
||||
continue;
|
||||
}
|
||||
|
||||
// get matching repository
|
||||
if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
|
||||
{
|
||||
result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
|
||||
continue;
|
||||
}
|
||||
|
||||
// fetch mod info
|
||||
result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
|
||||
{
|
||||
// fetch info
|
||||
ModInfoModel info = await repository.GetModInfoAsync(modID);
|
||||
|
||||
// validate
|
||||
if (info.Error == null)
|
||||
{
|
||||
if (info.Version == null)
|
||||
info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number.");
|
||||
if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
|
||||
info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'.");
|
||||
}
|
||||
|
||||
// cache & return
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes);
|
||||
return info;
|
||||
});
|
||||
ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata);
|
||||
result.SetBackwardsCompatibility(apiVersion);
|
||||
mods[mod.ID] = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
// return in expected structure
|
||||
return apiVersion.IsNewerThan("2.6-beta.18")
|
||||
? mods.Values
|
||||
: (object)mods;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the metadata for a mod.</summary>
|
||||
/// <param name="search">The mod data to match.</param>
|
||||
/// <param name="wikiData">The wiki data.</param>
|
||||
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
|
||||
/// <returns>Returns the mod data if found, else <c>null</c>.</returns>
|
||||
private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata)
|
||||
{
|
||||
// resolve update keys
|
||||
var updateKeys = new HashSet<string>(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase);
|
||||
ModDataRecord record = this.ModDatabase.Get(search.ID);
|
||||
if (record?.Fields != null)
|
||||
{
|
||||
string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(defaultUpdateKey))
|
||||
updateKeys.Add(defaultUpdateKey);
|
||||
}
|
||||
|
||||
// get latest versions
|
||||
ModEntryModel result = new ModEntryModel { ID = search.ID };
|
||||
IList<string> errors = new List<string>();
|
||||
foreach (string updateKey in updateKeys)
|
||||
{
|
||||
// fetch data
|
||||
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);
|
||||
if (data.Error != null)
|
||||
{
|
||||
errors.Add(data.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle main version
|
||||
if (data.Version != null)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version))
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.IsNewer(version, result.Main?.Version))
|
||||
result.Main = new ModEntryVersionModel(version, data.Url);
|
||||
}
|
||||
|
||||
// handle optional version
|
||||
if (data.PreviewVersion != null)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version))
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.IsNewer(version, result.Optional?.Version))
|
||||
result.Optional = new ModEntryVersionModel(version, data.Url);
|
||||
}
|
||||
}
|
||||
|
||||
// get unofficial version
|
||||
WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
|
||||
if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version))
|
||||
result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl);
|
||||
|
||||
// fallback to preview if latest is invalid
|
||||
if (result.Main == null && result.Optional != null)
|
||||
{
|
||||
result.Main = result.Optional;
|
||||
result.Optional = null;
|
||||
}
|
||||
|
||||
// special cases
|
||||
if (result.ID == "Pathoschild.SMAPI")
|
||||
{
|
||||
if (result.Main != null)
|
||||
result.Main.Url = "https://smapi.io/";
|
||||
if (result.Optional != null)
|
||||
result.Optional.Url = "https://smapi.io/";
|
||||
}
|
||||
|
||||
// add extended metadata
|
||||
if (includeExtendedMetadata && (wikiEntry != null || record != null))
|
||||
result.Metadata = new ModExtendedMetadataModel(wikiEntry, record);
|
||||
|
||||
// add result
|
||||
result.Errors = errors.ToArray();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Parse a namespaced mod ID.</summary>
|
||||
/// <param name="raw">The raw mod ID to parse.</param>
|
||||
/// <param name="vendorKey">The parsed vendor key.</param>
|
||||
|
@ -158,5 +222,91 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
modID = parts[1].Trim();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary>
|
||||
/// <param name="current">The current version.</param>
|
||||
/// <param name="other">The other version.</param>
|
||||
private bool IsNewer(ISemanticVersion current, ISemanticVersion other)
|
||||
{
|
||||
return current != null && (other == null || other.IsOlderThan(current));
|
||||
}
|
||||
|
||||
/// <summary>Get the mods for which the API should return data.</summary>
|
||||
/// <param name="model">The search model.</param>
|
||||
/// <param name="apiVersion">The requested API version.</param>
|
||||
private IEnumerable<ModSearchEntryModel> GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion)
|
||||
{
|
||||
if (model == null)
|
||||
yield break;
|
||||
|
||||
// yield standard entries
|
||||
if (model.Mods != null)
|
||||
{
|
||||
foreach (ModSearchEntryModel mod in model.Mods)
|
||||
yield return mod;
|
||||
}
|
||||
|
||||
// yield mod update keys if backwards compatible
|
||||
if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17"))
|
||||
{
|
||||
foreach (string updateKey in model.ModKeys.Distinct())
|
||||
yield return new ModSearchEntryModel(updateKey, new[] { updateKey });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get mod data from the wiki compatibility list.</summary>
|
||||
private async Task<WikiCompatibilityEntry[]> GetWikiDataAsync()
|
||||
{
|
||||
ModToolkit toolkit = new ModToolkit();
|
||||
return await this.Cache.GetOrCreateAsync($"_wiki", async entry =>
|
||||
{
|
||||
try
|
||||
{
|
||||
WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync();
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes);
|
||||
return entries;
|
||||
}
|
||||
catch
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes);
|
||||
return new WikiCompatibilityEntry[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Get the mod info for an update key.</summary>
|
||||
/// <param name="updateKey">The namespaced update key.</param>
|
||||
private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(string updateKey)
|
||||
{
|
||||
// parse update key
|
||||
if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID))
|
||||
return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
|
||||
|
||||
// get matching repository
|
||||
if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
|
||||
return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
|
||||
|
||||
// fetch mod info
|
||||
return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
|
||||
{
|
||||
ModInfoModel result = await repository.GetModInfoAsync(modID);
|
||||
if (result.Error != null)
|
||||
{
|
||||
if (result.Version == null)
|
||||
result.Error = $"The update key '{updateKey}' matches a mod with no version number.";
|
||||
else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
|
||||
result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.";
|
||||
}
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Get the requested API version.</summary>
|
||||
private ISemanticVersion GetApiVersion()
|
||||
{
|
||||
string actualVersion = (string)this.RouteData.Values["version"];
|
||||
return new SemanticVersion(actualVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
using System.Threading.Tasks;
|
||||
using Pathoschild.Http.Client;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from the Nexus Mods API.</summary>
|
||||
internal class NexusClient : INexusClient
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ModUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
|
||||
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
|
||||
/// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
public NexusClient(string userAgent, string baseUrl, string modUrlFormat)
|
||||
{
|
||||
this.ModUrlFormat = modUrlFormat;
|
||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Nexus mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
public async Task<NexusMod> GetModAsync(uint id)
|
||||
{
|
||||
return await this.Client
|
||||
.GetAsync(string.Format(this.ModUrlFormat, id))
|
||||
.As<NexusMod>();
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Client?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
|
@ -14,6 +15,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
/// <summary>The mod's semantic version number.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The latest file version.</summary>
|
||||
public ISemanticVersion LatestFileVersion { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
[JsonProperty("mod_page_uri")]
|
||||
public string Url { get; set; }
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using HtmlAgilityPack;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
|
@ -12,9 +15,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The URL for a Nexus web page excluding the base URL, where {0} is the mod ID.</summary>
|
||||
/// <summary>The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ModUrlFormat;
|
||||
|
||||
/// <summary>The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</summary>
|
||||
public string ModScrapeUrlFormat { get; set; }
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
|
||||
|
@ -25,10 +31,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
|
||||
/// <param name="baseUrl">The base URL for the Nexus Mods site.</param>
|
||||
/// <param name="modUrlFormat">The URL for a Nexus Mods web page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat)
|
||||
/// <param name="modUrlFormat">The URL for a Nexus Mods mod page for the user, excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
/// <param name="modScrapeUrlFormat">The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</param>
|
||||
public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat)
|
||||
{
|
||||
this.ModUrlFormat = modUrlFormat;
|
||||
this.ModScrapeUrlFormat = modScrapeUrlFormat;
|
||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||
}
|
||||
|
||||
|
@ -42,7 +50,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
try
|
||||
{
|
||||
html = await this.Client
|
||||
.GetAsync(string.Format(this.ModUrlFormat, id))
|
||||
.GetAsync(string.Format(this.ModScrapeUrlFormat, id))
|
||||
.AsString();
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
|
@ -75,11 +83,43 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
|||
string url = this.GetModUrl(id);
|
||||
string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim();
|
||||
string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim();
|
||||
SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion);
|
||||
|
||||
// extract file versions
|
||||
List<string> rawVersions = new List<string>();
|
||||
foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
|
||||
{
|
||||
string sectionName = fileSection.Descendants("h2").First().InnerText;
|
||||
if (sectionName != "Main files" && sectionName != "Optional files")
|
||||
continue;
|
||||
|
||||
rawVersions.AddRange(
|
||||
from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version"))
|
||||
from versionStat in statBox.Descendants().Where(p => p.HasClass("stat"))
|
||||
select versionStat.InnerText.Trim()
|
||||
);
|
||||
}
|
||||
|
||||
// choose latest file version
|
||||
ISemanticVersion latestFileVersion = null;
|
||||
foreach (string rawVersion in rawVersions)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur))
|
||||
continue;
|
||||
if (parsedVersion != null && !cur.IsNewerThan(parsedVersion))
|
||||
continue;
|
||||
if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion))
|
||||
continue;
|
||||
|
||||
latestFileVersion = cur;
|
||||
}
|
||||
|
||||
// yield info
|
||||
return new NexusMod
|
||||
{
|
||||
Name = name,
|
||||
Version = version,
|
||||
Version = parsedVersion?.ToString() ?? version,
|
||||
LatestFileVersion = latestFileVersion,
|
||||
Url = url
|
||||
};
|
||||
}
|
||||
|
|
|
@ -47,24 +47,21 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
/****
|
||||
** Nexus Mods
|
||||
****/
|
||||
/// <summary>The user agent for the Nexus Mods API client.</summary>
|
||||
public string NexusUserAgent { get; set; }
|
||||
|
||||
/// <summary>The base URL for the Nexus Mods API.</summary>
|
||||
public string NexusBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
/// <summary>The URL for a Nexus mod page for the user, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
public string NexusModUrlFormat { get; set; }
|
||||
|
||||
/// <summary>The URL for a Nexus mod page to scrape for versions, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
public string NexusModScrapeUrlFormat { get; set; }
|
||||
|
||||
/****
|
||||
** Pastebin
|
||||
****/
|
||||
/// <summary>The base URL for the Pastebin API.</summary>
|
||||
public string PastebinBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The user agent for the Pastebin API client, where {0} is the SMAPI version.</summary>
|
||||
public string PastebinUserAgent { get; set; }
|
||||
|
||||
/// <summary>The user key used to authenticate with the Pastebin API.</summary>
|
||||
public string PastebinUserKey { get; set; }
|
||||
|
||||
|
|
|
@ -24,5 +24,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
|
||||
/// <summary>The repository key for Nexus Mods.</summary>
|
||||
public string NexusKey { get; set; }
|
||||
|
||||
/// <summary>The web URL for the wiki compatibility list.</summary>
|
||||
public string WikiCompatibilityPageUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||
{
|
||||
/// <summary>The config settings for the app context.</summary>
|
||||
public class ContextConfig // must be public to pass into views
|
||||
/// <summary>The site config settings.</summary>
|
||||
public class SiteConfig // must be public to pass into views
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
|
@ -11,5 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
|
||||
/// <summary>The root URL for the log parser.</summary>
|
||||
public string LogParserUrl { get; set; }
|
||||
|
||||
/// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary>
|
||||
public bool EnableSmapiBeta { get; set; }
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using StardewModdingAPI.Common;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||
|
@ -31,13 +31,13 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
|
||||
/// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary>
|
||||
/// <remarks>The author name and description are optional.</remarks>
|
||||
private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>" + SemanticVersionImpl.UnboundedVersionPattern + @")(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching the start of SMAPI's content pack list.</summary>
|
||||
private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching an entry in SMAPI's content pack list.</summary>
|
||||
private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| for (?<for>.+?) \| (?<description>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| for (?<for>.+?)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -135,7 +135,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
|||
{
|
||||
Match match = this.ModPathPattern.Match(message.Text);
|
||||
log.ModPath = match.Groups["path"].Value;
|
||||
log.GamePath = new FileInfo(log.ModPath).Directory.FullName;
|
||||
int lastDelimiterPos = log.ModPath.LastIndexOfAny(new char[] { '/', '\\' });
|
||||
log.GamePath = lastDelimiterPos >= 0
|
||||
? log.ModPath.Substring(0, lastDelimiterPos)
|
||||
: log.ModPath;
|
||||
}
|
||||
|
||||
// log UTC timestamp line
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
using StardewModdingAPI.Internal.ConsoleWriting;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
/// <summary>The log severity levels.</summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>Tracing info intended for developers.</summary>
|
||||
Trace,
|
||||
Trace = ConsoleLogLevel.Trace,
|
||||
|
||||
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
|
||||
Debug,
|
||||
Debug = ConsoleLogLevel.Debug,
|
||||
|
||||
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
|
||||
Info,
|
||||
Info = ConsoleLogLevel.Info,
|
||||
|
||||
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
|
||||
Warn,
|
||||
Warn = ConsoleLogLevel.Warn,
|
||||
|
||||
/// <summary>A message indicating something went wrong.</summary>
|
||||
Error,
|
||||
Error = ConsoleLogLevel.Error,
|
||||
|
||||
/// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary>
|
||||
Alert
|
||||
Alert = ConsoleLogLevel.Alert,
|
||||
|
||||
/// <summary>A critical issue that generally signals an immediate end to the application.</summary>
|
||||
Critical = ConsoleLogLevel.Critical
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
|
@ -38,21 +38,25 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
// fetch info
|
||||
try
|
||||
{
|
||||
// get latest release
|
||||
// get latest release (whether preview or stable)
|
||||
GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true);
|
||||
GitRelease preview = null;
|
||||
if (latest == null)
|
||||
return new ModInfoModel("Found no mod with this ID.");
|
||||
|
||||
// get latest stable release (if not latest)
|
||||
// split stable/prerelease if applicable
|
||||
GitRelease preview = null;
|
||||
if (latest.IsPrerelease)
|
||||
{
|
||||
preview = latest;
|
||||
latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
|
||||
GitRelease result = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
|
||||
if (result != null)
|
||||
{
|
||||
preview = latest;
|
||||
latest = result;
|
||||
}
|
||||
}
|
||||
|
||||
// return data
|
||||
return new ModInfoModel(name: id, version: this.NormaliseVersion(latest?.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
|
||||
return new ModInfoModel(name: id, version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace StardewModdingAPI.Common.Models
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
/// <summary>Generic metadata about a mod.</summary>
|
||||
internal class ModInfoModel
|
||||
|
@ -9,10 +9,10 @@ namespace StardewModdingAPI.Common.Models
|
|||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The semantic version for the mod's latest release.</summary>
|
||||
/// <summary>The mod's latest release number.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</summary>
|
||||
/// <summary>The mod's latest optional release, if newer than <see cref="Version"/>.</summary>
|
||||
public string PreviewVersion { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
|
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Common.Models
|
|||
this.Version = version;
|
||||
this.PreviewVersion = previewVersion;
|
||||
this.Url = url;
|
||||
this.Error = error; // mainly initialised here for the JSON deserialiser
|
||||
this.Error = error;
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
|
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
return new ModInfoModel("Found no mod with this ID.");
|
||||
if (mod.Error != null)
|
||||
return new ModInfoModel(mod.Error);
|
||||
return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url);
|
||||
return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using StardewModdingAPI.Common;
|
||||
using StardewModdingAPI.Toolkit;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
|
@ -11,6 +11,6 @@ namespace StardewModdingAPI.Web.Framework
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public VersionConstraint()
|
||||
: base(SemanticVersionImpl.Regex) { }
|
||||
: base(SemanticVersion.Regex) { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,18 +10,26 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.7.2" />
|
||||
<PackageReference Include="Markdig" Version="0.14.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.1.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.8.4" />
|
||||
<PackageReference Include="Markdig" Version="0.15.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
|
||||
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="wwwroot\StardewModdingAPI.metadata.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Serialisation;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
|
@ -49,13 +50,16 @@ namespace StardewModdingAPI.Web
|
|||
// init configuration
|
||||
services
|
||||
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
|
||||
.Configure<ContextConfig>(this.Configuration.GetSection("Context"))
|
||||
.Configure<SiteConfig>(this.Configuration.GetSection("Site"))
|
||||
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
|
||||
.AddMemoryCache()
|
||||
.AddMvc()
|
||||
.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()))
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
|
||||
options.SerializerSettings.Converters.Add(converter);
|
||||
|
||||
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
|
@ -82,15 +86,11 @@ namespace StardewModdingAPI.Web
|
|||
password: api.GitHubPassword
|
||||
));
|
||||
|
||||
//services.AddSingleton<INexusClient>(new NexusClient(
|
||||
// userAgent: api.NexusUserAgent,
|
||||
// baseUrl: api.NexusBaseUrl,
|
||||
// modUrlFormat: api.NexusModUrlFormat
|
||||
//));
|
||||
services.AddSingleton<INexusClient>(new NexusWebScrapeClient(
|
||||
userAgent: userAgent,
|
||||
baseUrl: api.NexusBaseUrl,
|
||||
modUrlFormat: api.NexusModUrlFormat
|
||||
modUrlFormat: api.NexusModUrlFormat,
|
||||
modScrapeUrlFormat: api.NexusModScrapeUrlFormat
|
||||
));
|
||||
|
||||
services.AddSingleton<IPastebinClient>(new PastebinClient(
|
||||
|
@ -153,23 +153,23 @@ namespace StardewModdingAPI.Web
|
|||
));
|
||||
|
||||
// shortcut redirects
|
||||
redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
|
||||
|
||||
// redirect legacy canimod.com URLs
|
||||
var wikiRedirects = new Dictionary<string, string[]>
|
||||
{
|
||||
["Modding:Creating_a_SMAPI_mod"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod" },
|
||||
["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
|
||||
["Modding:Modder_Guide"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod", "^/for-devs/creating-a-smapi-mod-advanced-config" },
|
||||
["Modding:Player_Guide"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods", "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },
|
||||
|
||||
["Modding:Editing_XNB_files"] = new[] { "^/for-devs/creating-an-xnb-mod", "^/guides/creating-an-xnb-mod" },
|
||||
["Modding:Event_data"] = new[] { "^/for-devs/events", "^/guides/events" },
|
||||
["Modding:Gift_taste_data"] = new[] { "^/for-devs/npc-gift-tastes", "^/guides/npc-gift-tastes" },
|
||||
["Modding:IDE_reference"] = new[] { "^/for-devs/creating-a-smapi-mod-ide-primer" },
|
||||
["Modding:Installing_SMAPI"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods" },
|
||||
["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
|
||||
["Modding:Player_FAQs"] = new[] { "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },
|
||||
["Modding:SMAPI_APIs"] = new[] { "^/for-devs/creating-a-smapi-mod-advanced-config" },
|
||||
["Modding:Updating_deprecated_SMAPI_code"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
|
||||
["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
|
||||
};
|
||||
foreach (KeyValuePair<string, string[]> pair in wikiRedirects)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
|
||||
namespace StardewModdingAPI.Web.ViewModels
|
||||
|
@ -5,6 +8,13 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>The view model for the log parser page.</summary>
|
||||
public class LogParserModel
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>A regex pattern matching characters to remove from a mod name to create the slug ID.</summary>
|
||||
private readonly Regex SlugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
|
@ -17,6 +27,15 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>The parsed log info.</summary>
|
||||
public ParsedLog ParsedLog { get; set; }
|
||||
|
||||
/// <summary>Whether to show the raw unparsed log.</summary>
|
||||
public bool ShowRaw { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while uploading the log to Pastebin.</summary>
|
||||
public string UploadError { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while parsing the log file.</summary>
|
||||
public string ParseError => this.ParsedLog?.Error;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -27,12 +46,46 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="sectionUrl">The root URL for the log parser controller.</param>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
/// <param name="parsedLog">The parsed log info.</param>
|
||||
public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog)
|
||||
public LogParserModel(string sectionUrl, string pasteID)
|
||||
{
|
||||
this.SectionUrl = sectionUrl;
|
||||
this.PasteID = pasteID;
|
||||
this.ParsedLog = null;
|
||||
this.ShowRaw = false;
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="sectionUrl">The root URL for the log parser controller.</param>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
/// <param name="parsedLog">The parsed log info.</param>
|
||||
/// <param name="showRaw">Whether to show the raw unparsed log.</param>
|
||||
public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog, bool showRaw)
|
||||
: this(sectionUrl, pasteID)
|
||||
{
|
||||
this.ParsedLog = parsedLog;
|
||||
this.ShowRaw = showRaw;
|
||||
}
|
||||
|
||||
/// <summary>Get all content packs in the log grouped by the mod they're for.</summary>
|
||||
public IDictionary<string, LogModInfo[]> GetContentPacksByMod()
|
||||
{
|
||||
// get all mods & content packs
|
||||
LogModInfo[] mods = this.ParsedLog?.Mods;
|
||||
if (mods == null || !mods.Any())
|
||||
return new Dictionary<string, LogModInfo[]>();
|
||||
|
||||
// group by mod
|
||||
return mods
|
||||
.Where(mod => mod.ContentPackFor != null)
|
||||
.GroupBy(mod => mod.ContentPackFor)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Get a sanitised mod name that's safe to use in anchors, attributes, and URLs.</summary>
|
||||
/// <param name="modName">The mod name.</param>
|
||||
public string GetSlug(string modName)
|
||||
{
|
||||
return this.SlugInvalidCharPattern.Replace(modName, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
}
|
||||
@model StardewModdingAPI.Web.ViewModels.IndexModel
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/index.css" />
|
||||
<link rel="stylesheet" href="~/Content/css/index.css?r=20180615" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/index.js?r=20180615"></script>
|
||||
}
|
||||
|
||||
<p id="blurb">
|
||||
|
@ -13,17 +15,29 @@
|
|||
</p>
|
||||
|
||||
<div id="call-to-action">
|
||||
<a href="@Model.StableVersion.DownloadUrl" class="main-cta">Download SMAPI @Model.StableVersion.Version</a><br />
|
||||
<div class="cta-dropdown">
|
||||
<a href="@Model.StableVersion.DownloadUrl" class="main-cta download">Download SMAPI @Model.StableVersion.Version</a><br/>
|
||||
<div class="dropdown-content">
|
||||
<a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
|
||||
<a href="@Model.StableVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
|
||||
</div>
|
||||
</div><br />
|
||||
|
||||
@if (Model.BetaVersion != null)
|
||||
{
|
||||
<a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta">Download SMAPI @Model.BetaVersion.Version<br /><small>for Stardew Valley 1.3 beta</small></a><br />
|
||||
<div class="cta-dropdown secondary-cta-dropdown">
|
||||
<a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta download">Download SMAPI @Model.BetaVersion.Version<br/><small>for Stardew Valley 1.3 beta</small></a><br/>
|
||||
<div class="dropdown-content">
|
||||
<a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
|
||||
<a href="@Model.BetaVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
|
||||
</div>
|
||||
</div><br />
|
||||
}
|
||||
<a href="https://stardewvalleywiki.com/Modding:Installing_SMAPI" class="secondary-cta">Install guide</a><br />
|
||||
<a href="https://stardewvalleywiki.com/Modding:Player_FAQs" class="secondary-cta">FAQs</a><br />
|
||||
<img src="favicon.ico" />
|
||||
<a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a><br />
|
||||
<img id="pufferchick" src="Content/images/pufferchick.png" />
|
||||
</div>
|
||||
|
||||
<h2>Get help</h2>
|
||||
<h2 id="help">Get help</h2>
|
||||
<ul>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">Mod compatibility list</a></li>
|
||||
<li>Get help <a href="https://stardewvalleywiki.com/Modding:Community#Discord">on Discord</a> or <a href="https://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/">in the forums</a></li>
|
||||
|
@ -31,7 +45,7 @@
|
|||
|
||||
@if (Model.BetaVersion == null)
|
||||
{
|
||||
<h2>What's new in SMAPI @Model.StableVersion.Version?</h2>
|
||||
<h2 id="whatsnew">What's new in SMAPI @Model.StableVersion.Version?</h2>
|
||||
<div class="github-description">
|
||||
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
|
||||
</div>
|
||||
|
@ -39,7 +53,7 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
<h2>What's new in...</h2>
|
||||
<h2 id="whatsnew">What's new in...</h2>
|
||||
<h3>SMAPI @Model.StableVersion.Version?</h3>
|
||||
<div class="github-description">
|
||||
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
|
||||
|
@ -53,7 +67,7 @@ else
|
|||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p>
|
||||
}
|
||||
|
||||
<h2>Donate to support SMAPI ♥</h2>
|
||||
<h2 id="donate">Donate to support SMAPI ♥</h2>
|
||||
<p>
|
||||
SMAPI is an open-source project by Pathoschild. It will always be free, but donations
|
||||
are much appreciated to help pay for development, server hosting, domain fees, coffee, etc.
|
||||
|
@ -75,15 +89,23 @@ else
|
|||
Special thanks to
|
||||
acerbicon,
|
||||
<a href="https://www.nexusmods.com/stardewvalley/users/31393530">ChefRude</a>,
|
||||
cheesysteak,
|
||||
hawkfalcon,
|
||||
jwdred,
|
||||
OfficialPiAddict,
|
||||
KNakamura,
|
||||
Kono Tyran,
|
||||
Pucklynn,
|
||||
Robby LaFarge,
|
||||
and a few anonymous users for their ongoing support; you're awesome! 🏅
|
||||
</p>
|
||||
|
||||
<h2>For mod creators</h2>
|
||||
<h2 id="modcreators">For mod creators</h2>
|
||||
<ul>
|
||||
<li><a href="@Model.StableVersion.DevDownloadUrl">SMAPI @Model.StableVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li>
|
||||
@if (Model.BetaVersion != null)
|
||||
{
|
||||
<li><a href="@Model.BetaVersion.DevDownloadUrl">SMAPI @Model.BetaVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li>
|
||||
}
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Index">Modding documentation</a></li>
|
||||
<li>Need help? Come <a href="https://stardewvalleywiki.com/Modding:Community#Discord">chat on Discord</a>.</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,76 +1,120 @@
|
|||
@{
|
||||
ViewData["Title"] = "SMAPI log parser";
|
||||
|
||||
IDictionary<string, LogModInfo[]> contentPacks = Model.ParsedLog?.Mods
|
||||
?.GroupBy(mod => mod.ContentPackFor)
|
||||
.Where(group => group.Key != null)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||
|
||||
Regex slugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
string GetSlug(string modName)
|
||||
{
|
||||
return slugInvalidCharPattern.Replace(modName, "");
|
||||
}
|
||||
}
|
||||
@using System.Text.RegularExpressions
|
||||
@using Newtonsoft.Json
|
||||
@using StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
@model StardewModdingAPI.Web.ViewModels.LogParserModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "SMAPI log parser";
|
||||
IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod();
|
||||
IDictionary<string, bool> defaultFilters = Enum
|
||||
.GetValues(typeof(LogLevel))
|
||||
.Cast<LogLevel>()
|
||||
.ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
|
||||
JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None };
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180225" />
|
||||
@if (Model.PasteID != null)
|
||||
{
|
||||
<meta name="robots" content="noindex" />
|
||||
}
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180627" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/log-parser.js?r=20180225"></script>
|
||||
<script src="~/Content/js/log-parser.js?r=20180627"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
smapi.logParser({
|
||||
logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)),
|
||||
showPopup: @Json.Serialize(Model.ParsedLog == null),
|
||||
showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }),
|
||||
showLevels: {
|
||||
trace: false,
|
||||
debug: false,
|
||||
info: true,
|
||||
alert: true,
|
||||
warn: true,
|
||||
error: true
|
||||
}
|
||||
showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting),
|
||||
showLevels: @Json.Serialize(defaultFilters, noFormatting),
|
||||
enableFilters: @Json.Serialize(!Model.ShowRaw)
|
||||
}, '@Model.SectionUrl');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
@*********
|
||||
** Intro
|
||||
*********@
|
||||
<p id="blurb">This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.</p>
|
||||
|
||||
@if (Model.ParsedLog?.IsValid == true)
|
||||
{
|
||||
<div class="banner success" v-pre>
|
||||
<strong>The log was uploaded successfully!</strong><br/>
|
||||
Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br/>
|
||||
(Or <a id="upload-button" href="#">upload a new log</a>.)
|
||||
</div>
|
||||
}
|
||||
else if (Model.ParsedLog?.IsValid == false)
|
||||
@* upload result banner *@
|
||||
@if (Model.UploadError != null)
|
||||
{
|
||||
<div class="banner error" v-pre>
|
||||
<strong>Oops, couldn't parse that file. (Make sure you upload the log file, not the console text.)</strong><br />
|
||||
Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
|
||||
(Or <a id="upload-button" href="#">upload a new log</a>.)<br />
|
||||
<br />
|
||||
<small v-pre>Error details: @Model.ParsedLog.Error</small>
|
||||
<strong>Oops, the server ran into trouble saving that file.</strong><br />
|
||||
<small v-pre>Error details: @Model.UploadError</small>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
else if (Model.ParseError != null)
|
||||
{
|
||||
<input type="button" id="upload-button" value="Share a new log" />
|
||||
<div class="banner error" v-pre>
|
||||
<strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br />
|
||||
Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
|
||||
(Or <a href="@Model.SectionUrl">upload a new log</a>.)<br />
|
||||
<br />
|
||||
<small v-pre>Error details: @Model.ParseError</small>
|
||||
</div>
|
||||
}
|
||||
else if (Model.ParsedLog?.IsValid == true)
|
||||
{
|
||||
<div class="banner success" v-pre>
|
||||
<strong>Share this link to let someone else see the log:</strong> <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
|
||||
(Or <a href="@Model.SectionUrl">upload a new log</a>.)
|
||||
</div>
|
||||
}
|
||||
|
||||
@*********
|
||||
** Parsed log
|
||||
*********@
|
||||
@* upload new log *@
|
||||
@if (Model.ParsedLog == null)
|
||||
{
|
||||
<h2>Where do I find my SMAPI log?</h2>
|
||||
<div>What system do you use?</div>
|
||||
<ul id="os-list">
|
||||
<li><input type="radio" name="os" value="linux" id="os-linux" /> <label for="os-linux">Linux</label></li>
|
||||
<li><input type="radio" name="os" value="mac" id="os-mac" /> <label for="os-mac">Mac</label></li>
|
||||
<li><input type="radio" name="os" value="windows" id="os-windows" /> <label for="os-windows">Windows</label></li>
|
||||
</ul>
|
||||
<div data-os="linux">
|
||||
On Linux:
|
||||
<ol>
|
||||
<li>Open the Files app.</li>
|
||||
<li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li>
|
||||
<li>Choose <em>Enter Location</em>.</li>
|
||||
<li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
|
||||
<li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div data-os="mac">
|
||||
On Mac:
|
||||
<ol>
|
||||
<li>Open the Finder app.</li>
|
||||
<li>Click <em>Go</em> at the top, then <em>Enter Location</em>.</li>
|
||||
<li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
|
||||
<li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div data-os="windows">
|
||||
On Windows:
|
||||
<ol>
|
||||
<li>Press the <code>Windows</code> and <code>R</code> buttons at the same time.</li>
|
||||
<li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li>
|
||||
<li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2>How do I share my log?</h2>
|
||||
<form action="@Model.SectionUrl" method="post">
|
||||
<ol>
|
||||
<li>
|
||||
Drag the file onto this textbox (or paste the text in):<br />
|
||||
<textarea id="input" name="input" placeholder="paste log here"></textarea>
|
||||
</li>
|
||||
<li>
|
||||
Click this button:<br />
|
||||
<input type="submit" id="submit" value="save log" />
|
||||
</li>
|
||||
<li>On the new page, copy the URL and send it to the person helping you.</li>
|
||||
</ol>
|
||||
</form>
|
||||
}
|
||||
|
||||
@* parsed log *@
|
||||
@if (Model.ParsedLog?.IsValid == true)
|
||||
{
|
||||
<h2>Log info</h2>
|
||||
|
@ -95,17 +139,20 @@ else
|
|||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<table id="mods">
|
||||
<table id="mods" class="@(Model.ShowRaw ? "filters-disabled" : null)">
|
||||
<caption>
|
||||
Installed mods:
|
||||
<span class="notice txt"><i>click any mod to filter</i></span>
|
||||
<span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span>
|
||||
<span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
|
||||
@if (!Model.ShowRaw)
|
||||
{
|
||||
<span class="notice txt"><i>click any mod to filter</i></span>
|
||||
<span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span>
|
||||
<span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
|
||||
}
|
||||
</caption>
|
||||
@foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null))
|
||||
{
|
||||
<tr v-on:click="toggleMod('@GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@GetSlug(mod.Name)'] }">
|
||||
<td><input type="checkbox" v-bind:checked="showMods['@GetSlug(mod.Name)']" v-show="anyModsHidden" /></td>
|
||||
<tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }">
|
||||
<td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-show="anyModsHidden" /></td>
|
||||
<td v-pre>
|
||||
<strong>@mod.Name</strong> @mod.Version
|
||||
@if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList))
|
||||
|
@ -134,36 +181,47 @@ else
|
|||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div id="filters">
|
||||
Filter messages:
|
||||
<span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
|
||||
<span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
|
||||
<span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
|
||||
<span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
|
||||
<span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
|
||||
<span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
|
||||
</div>
|
||||
|
||||
<table id="log">
|
||||
@foreach (var message in Model.ParsedLog.Messages)
|
||||
{
|
||||
string levelStr = message.Level.ToString().ToLower();
|
||||
@if (!Model.ShowRaw)
|
||||
{
|
||||
<div id="filters">
|
||||
Filter messages:
|
||||
<span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
|
||||
<span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
|
||||
<span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
|
||||
<span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
|
||||
<span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
|
||||
<span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
|
||||
</div>
|
||||
|
||||
<tr class="@levelStr mod" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')">
|
||||
<td v-pre>@message.Time</td>
|
||||
<td v-pre>@message.Level.ToString().ToUpper()</td>
|
||||
<td v-pre data-title="@message.Mod">@message.Mod</td>
|
||||
<td v-pre>@message.Text</td>
|
||||
</tr>
|
||||
if (message.Repeated > 0)
|
||||
<table id="log">
|
||||
@foreach (var message in Model.ParsedLog.Messages)
|
||||
{
|
||||
<tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')">
|
||||
<td colspan="3"></td>
|
||||
<td v-pre><i>repeats [@message.Repeated] times.</i></td>
|
||||
string levelStr = message.Level.ToString().ToLower();
|
||||
|
||||
<tr class="@levelStr mod" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr')">
|
||||
<td v-pre>@message.Time</td>
|
||||
<td v-pre>@message.Level.ToString().ToUpper()</td>
|
||||
<td v-pre data-title="@message.Mod">@message.Mod</td>
|
||||
<td v-pre>@message.Text</td>
|
||||
</tr>
|
||||
if (message.Repeated > 0)
|
||||
{
|
||||
<tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr')">
|
||||
<td colspan="3"></td>
|
||||
<td v-pre><i>repeats [@message.Repeated] times.</i></td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</table>
|
||||
</table>
|
||||
|
||||
<small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))?raw=true">view raw log</a></small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<pre v-pre>@Model.ParsedLog.RawText</pre>
|
||||
<small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))">view parsed log</a></small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (Model.ParsedLog?.IsValid == false)
|
||||
|
@ -171,22 +229,3 @@ else if (Model.ParsedLog?.IsValid == false)
|
|||
<h3>Raw log</h3>
|
||||
<pre v-pre>@Model.ParsedLog.RawText</pre>
|
||||
}
|
||||
|
||||
<div id="upload-area">
|
||||
<div id="popup-upload" class="popup">
|
||||
<h1>Upload log file</h1>
|
||||
<div class="frame">
|
||||
<ol>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li>
|
||||
<li>Drag the file onto the textbox below (or paste the text in).</li>
|
||||
<li>Click <em>Parse</em>.</li>
|
||||
</ol>
|
||||
<textarea id="input" placeholder="Paste or drag the log here"></textarea>
|
||||
<div class="buttons">
|
||||
<input type="button" id="submit" value="Parse" />
|
||||
<input type="button" id="cancel" value="Cancel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploader"></div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using StardewModdingAPI.Web.Framework.ConfigModels
|
||||
@inject IOptions<ContextConfig> ContextConfig
|
||||
@inject IOptions<SiteConfig> SiteConfig
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>@ViewData["Title"] - SMAPI.io</title>
|
||||
<link rel="stylesheet" href="~/Content/css/main.css" />
|
||||
@RenderSection("Head", required: false)
|
||||
|
@ -14,8 +15,8 @@
|
|||
<div id="sidebar">
|
||||
<h4>SMAPI</h4>
|
||||
<ul>
|
||||
<li><a href="@ContextConfig.Value.RootUrl">About SMAPI</a></li>
|
||||
<li><a href="@ContextConfig.Value.LogParserUrl">Log parser</a></li>
|
||||
<li><a href="@SiteConfig.Value.RootUrl">About SMAPI</a></li>
|
||||
<li><a href="@SiteConfig.Value.LogParserUrl">Log parser</a></li>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue