Merge branch 'beta' into develop

This commit is contained in:
Jesse Plamondon-Willard 2021-11-30 17:12:49 -05:00
commit 919bbe94aa
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
68 changed files with 1161 additions and 973 deletions

Binary file not shown.

View File

@ -7,7 +7,7 @@
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
<!--set platform-->
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS;SMAPI_FOR_XNA</DefineConstants>
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
</PropertyGroup>
<!--find game folder-->
@ -27,20 +27,31 @@
<TranslationFiles Include="$(TargetDir)\i18n\*.json" />
</ItemGroup>
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" />
<!-- SMAPI -->
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName)" DestinationFolder="$(GamePath)" Condition="$(OS) != 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\SMAPI.config.json" DestinationFiles="$(GamePath)\smapi-internal\config.json" />
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
<!-- Harmony + dependencies -->
<Copy SourceFiles="$(TargetDir)\0Harmony.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\0Harmony.xml" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.Mdb.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.Pdb.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
<!-- .NET dependencies -->
<Copy SourceFiles="$(TargetDir)\System.Configuration.ConfigurationManager.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\System.Management.dll" DestinationFolder="$(GamePath)\smapi-internal" Condition="$(OS) == 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\System.Runtime.Caching.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\System.Security.Permissions.dll" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.ErrorHandler' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'">
@ -54,13 +65,13 @@
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)\i18n" />
</Target>
<Target Name="CopyToolkit" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit' AND $(TargetFramework) == 'net452'" AfterTargets="PostBuildEvent">
<Target Name="CopyToolkit" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit.CoreInterfaces' AND $(TargetFramework) == 'net452'" AfterTargets="PostBuildEvent">
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit.CoreInterfaces'" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />

View File

@ -41,14 +41,4 @@
</PropertyGroup>
</When>
</Choose>
<!-- set game metadata -->
<PropertyGroup>
<!--standard executable name-->
<GameExecutableName>Stardew Valley</GameExecutableName>
<GameExecutableName Condition="$(OS) != 'Windows_NT'">StardewValley</GameExecutableName>
<!--Linux install on Windows (for 64-bit hack)-->
<GameExecutableName Condition="$(OS) == 'Windows_NT' AND !Exists('$(GamePath)\$(GameExecutableName).exe') AND Exists('$(GamePath)\StardewValley.exe')">StardewValley</GameExecutableName>
</PropertyGroup>
</Project>

View File

@ -14,7 +14,7 @@
<OutRootPath>$(SolutionDir)\..\bin</OutRootPath>
<SmapiBin>$(BuildRootPath)\SMAPI\bin\$(Configuration)</SmapiBin>
<ToolkitBin>$(BuildRootPath)\SMAPI.Toolkit\bin\$(Configuration)\net452</ToolkitBin>
<ToolkitBin>$(BuildRootPath)\SMAPI.Toolkit\bin\$(Configuration)\net5.0</ToolkitBin>
<ConsoleCommandsBin>$(BuildRootPath)\SMAPI.Mods.ConsoleCommands\bin\$(Configuration)</ConsoleCommandsBin>
<ErrorHandlerBin>$(BuildRootPath)\SMAPI.Mods.ErrorHandler\bin\$(Configuration)</ErrorHandlerBin>
<SaveBackupBin>$(BuildRootPath)\SMAPI.Mods.SaveBackup\bin\$(Configuration)</SaveBackupBin>
@ -35,12 +35,16 @@
<Copy SourceFiles="$(TargetDir)\assets\unix-install.sh" DestinationFiles="$(PackagePath)\install on Linux.sh" />
<Copy SourceFiles="$(TargetDir)\assets\unix-install.sh" DestinationFiles="$(PackagePath)\install on macOS.command" />
<Copy SourceFiles="$(TargetDir)\assets\windows-install.bat" DestinationFiles="$(PackagePath)\install on Windows.bat" />
<Copy SourceFiles="$(TargetDir)\assets\README.txt" DestinationFiles="$(PackagePath)\README.txt" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFiles="$(PackagePath)\internal\$(PlatformName)-install.exe" />
<Copy Condition="$(PlatformName) == 'windows'" SourceFiles="$(TargetDir)\assets\windows-exe-config.xml" DestinationFiles="$(PackagePath)\internal\$(PlatformName)-install.exe.config" />
<Copy SourceFiles="$(TargetDir)\assets\README.txt" DestinationFolder="$(PackagePath)" />
<Copy SourceFiles="$(TargetDir)\assets\windows-exe-config.xml" DestinationFiles="$(PackagePath)\internal\$(PlatformName)\install.exe.config" Condition="$(PlatformName) == 'windows'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(PackagePath)\internal\$(PlatformName)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).runtimeconfig.json" DestinationFolder="$(PackagePath)\internal\$(PlatformName)" />
<!--copy bundle files-->
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.exe" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(TargetDir)\assets\runtimeconfig.$(PlatformName).json" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI.runtimeconfig.json" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.dll" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.exe" DestinationFolder="$(PackagePath)\bundle" Condition="$(PlatformName) == 'windows'" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI" DestinationFolder="$(PackagePath)\bundle" Condition="$(PlatformName) != 'windows'" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.pdb" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\StardewModdingAPI.xml" DestinationFolder="$(PackagePath)\bundle" />
<Copy SourceFiles="$(SmapiBin)\steam_appid.txt" DestinationFolder="$(PackagePath)\bundle" />
@ -61,11 +65,16 @@
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.CoreInterfaces.pdb" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(ToolkitBin)\SMAPI.Toolkit.CoreInterfaces.xml" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(PackagePath)\bundle\smapi-internal\i18n" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\unix-launcher.sh" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(SmapiBin)\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\unix-launcher.sh" DestinationFolder="$(PackagePath)\bundle" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(SmapiBin)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(PlatformName) == 'windows'" SourceFiles="$(TargetDir)\assets\windows-exe-config.xml" DestinationFiles="$(PackagePath)\bundle\StardewModdingAPI.exe.config" />
<!-- copy .NET dependencies -->
<Copy SourceFiles="$(SmapiBin)\System.Configuration.ConfigurationManager.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(SmapiBin)\System.Management.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" Condition="$(PlatformName) == 'windows'" />
<Copy SourceFiles="$(SmapiBin)\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy SourceFiles="$(SmapiBin)\System.Security.Permissions.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<!--copy bundled mods-->
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.dll" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
@ -78,63 +87,33 @@
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<Copy SourceFiles="$(SaveBackupBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
<!-- fix errors on Linux/macOS (sample: https://smapi.io/log/mMdFUpgB) -->
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<Copy Condition="$(PlatformName) == 'unix'" SourceFiles="$(TargetDir)\assets\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
<!-- fix Linux/macOS permissions -->
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)\install on Linux.sh&quot;" />
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)\install on macOS.command&quot;" />
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)/install on Linux.sh&quot;" />
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)/install on macOS.command&quot;" />
<Exec Condition="$(PlatformName) == 'unix'" Command="chmod 755 &quot;$(PackagePath)/bundle/unix-launcher.sh&quot;" />
<!-- finalise 'for developers' installer -->
<ItemGroup>
<PackageFiles Include="$(PackagePath)\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PackageFiles)" DestinationFolder="$(PackageDevPath)\%(RecursiveDir)" />
<ZipDirectory FromDirPath="$(PackageDevPath)\bundle" ToFilePath="$(PackageDevPath)\internal\$(PlatformName)-install.dat" />
<ZipDirectory SourceDirectory="$(PackageDevPath)\bundle" DestinationFile="$(PackageDevPath)\internal\$(PlatformName)\install.dat" />
<RemoveDir Directories="$(PackageDevPath)\bundle" />
<!-- finalise normal installer -->
<ReplaceFileText FilePath="$(PackagePath)\bundle\smapi-internal\config.json" Search="&quot;DeveloperMode&quot;: true" Replace="&quot;DeveloperMode&quot;: false" />
<ZipDirectory FromDirPath="$(PackagePath)\bundle" ToFilePath="$(PackagePath)\internal\$(PlatformName)-install.dat" />
<ZipDirectory SourceDirectory="$(PackagePath)\bundle" DestinationFile="$(PackagePath)\internal\$(PlatformName)\install.dat" />
<RemoveDir Directories="$(PackagePath)\bundle" />
</Target>
<!-- Create a zip file with the contents of a given folder path. Derived from https://stackoverflow.com/a/38127938/262123. -->
<UsingTask TaskName="ZipDirectory" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
<ParameterGroup>
<FromDirPath ParameterType="System.String" Required="true" />
<ToFilePath ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.IO.Compression.FileSystem" />
<Using Namespace="System.IO.Compression" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try
{
ZipFile.CreateFromDirectory(FromDirPath, ToFilePath);
return true;
}
catch(Exception ex)
{
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
<!-- Replace text in a file based on a regex pattern. Derived from https://stackoverflow.com/a/22571621/262123. -->
<UsingTask TaskName="ReplaceFileText" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<UsingTask TaskName="ReplaceFileText" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<FilePath ParameterType="System.String" Required="true" />
<Search ParameterType="System.String" Required="true" />
<Replace ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Text.RegularExpressions" />

View File

@ -3,7 +3,25 @@
# Release notes
## Upcoming release
* For players:
* Updated for Stardew Valley 1.5.5.
* Updated compatibility list.
* Added support for loading BmFont `.fnt` files for [custom languages](https://stardewvalleywiki.com/Modding:Custom_languages) through the [content API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content).
* Added `set_farm_type` [console command](https://stardewvalleywiki.com/Modding:Console_commands#Console_commands) to change the current farm type.
* Fixed installer window closing immediately if the installer crashed.
* For mod authors:
* Migrated to 64-bit MonoGame and .NET 5 on all platforms (see [migration guide for mod authors](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5)).
* Added support for [map overlays via `asset.AsMap().PatchMap`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content#Edit_a_map).
**Update note for players with older systems:**
The game now has two branches: the _main branch_ which you'll get by default, and an optional
_compatibility branch_ for [older systems](https://www.stardewvalley.net/compatibility/). The two
branches have identical content, but use [different technologies](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5#Stardew_Valley_compatibility_branch).
Unfortunately **SMAPI only supports the main branch of the game**. There are formidable difficulties
across all mods in supporting all three variations, the [Steam hardware stats](https://store.steampowered.com/hwsurvey)
show that 99.69% of players have 64-bit, and 32-bit imposes significant restrictions on what mods
can do.
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.24.0.

View File

@ -29,20 +29,19 @@ change how these work):
* **Detect game path:**
The package automatically finds your game folder by scanning the default install paths and
Windows registry. It adds two MSBuild properties for use in your `.csproj` file if needed:
`$(GamePath)` and `$(GameExecutableName)`.
`$(GamePath)` and `$(GameModsPath)`.
* **Add assembly references:**
The package adds assembly references to SMAPI, Stardew Valley, xTile, and the game framework
(MonoGame on Linux/macOS, XNA Framework on Windows). It automatically adjusts depending on which OS
you're compiling it on. If you use [Harmony](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Harmony),
it can optionally add a reference to that too.
The package adds assembly references to MonoGame, SMAPI, Stardew Valley, and xTile. It
automatically adjusts depending on which OS you're compiling it on. If you use
[Harmony](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Harmony), it can optionally add
a reference to that too.
* **Copy files into the `Mods` folder:**
The package automatically copies your mod's DLL and PDB files, `manifest.json`, [`i18n`
files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder (if
any), and [build output](https://stackoverflow.com/a/10828462/262123) into your game's `Mods`
folder when you rebuild the code, with a subfolder matching the mod's project name. That lets you
try the mod in-game right after building it.
files](https://stardewvalleywiki.com/Modding:Translations) (if any), and the `assets` folder (if
any) into the `Mods` folder when you rebuild the code, with a subfolder matching the mod's project
name. That lets you try the mod in-game right after building it.
* **Create release zip:**
The package adds a zip file in your project's `bin` folder when you rebuild the code, in the
@ -129,23 +128,6 @@ The absolute path to the folder containing the game's installed mods (defaults t
</td>
</tr>
<tr>
<td><code>GameExecutableName</code></td>
<td>
The filename for the game's executable (i.e. `StardewValley.exe` on Linux/macOS or
`Stardew Valley.exe` on Windows). This is auto-detected, and you should almost never change this.
</td>
</tr>
<tr>
<td><code>GameFramework</code></td>
<td>
The game framework for which the mod is being compiled (one of `Xna` or `MonoGame`). This is
auto-detected based on the platform, and you should almost never change this.
</td>
</tr>
</table>
</li>
@ -206,11 +188,63 @@ The folder path where the release zip is created (defaults to the project's `bin
<th>effect</th>
</tr>
<tr>
<td><code>CopyModReferencesToBuildOutput</code></td>
<td><code>BundleExtraAssemblies</code></td>
<td>
Whether to copy game and framework DLLs into the mod folder (default `false`). This is useful for
unit test projects, but not needed for mods that'll be run through SMAPI.
**Most mods should not change this option.**
By default (when this is _not_ enabled), only the mod files [normally considered part of the
mod](#Features) will be added to the release `.zip` and copied into the `Mods` folder (i.e.
"deployed"). That includes the assembly files (`*.dll`, `*.pdb`, and `*.xml`) for your mod project,
but any other DLLs won't be deployed.
Enabling this option will add _all_ dependencies to the build output, then deploy _some_ of them
depending on the comma-separated value(s) you set:
<table>
<tr>
<th>option</th>
<th>result</th>
</tr>
<tr>
<td><code>ThirdParty</code></td>
<td>
Assembly files which don't match any other category.
</td>
</tr>
<tr>
<td><code>System</code></td>
<td>
Assembly files whose names start with `Microsoft.*` or `System.*`.
</td>
</tr>
<tr>
<td><code>Game</code></td>
<td>
Assembly files which are part of MonoGame, SMAPI, or Stardew Valley.
</td>
</tr>
<tr>
<td><code>All</code></td>
<td>
Equivalent to `System, Game, ThirdParty`.
</td>
</tr>
</table>
Most mods should omit the option. Some mods may need `ThirdParty` if they bundle third-party DLLs
with their mod. The other options are mainly useful for unit tests.
When enabling this option, you should **manually review which files get deployed** and use the
`IgnoreModFilePaths` or `IgnoreModFilePatterns` options to exclude files as needed.
</td>
</tr>
@ -222,6 +256,20 @@ Whether to configure the project so you can launch or debug the game through the
Visual Studio (default `true`). There's usually no reason to change this, unless it's a unit test
project.
</td>
</tr>
<tr>
<td><code>IgnoreModFilePaths</code></td>
<td>
A comma-delimited list of literal file paths to ignore, relative to the mod's `bin` folder. Paths
are case-sensitive, but path delimiters are normalized automatically. For example, this ignores a
set of tilesheets:
```xml
<IgnoreModFilePaths>assets/paths.png, assets/springobjects.png</IgnoreModFilePaths>
```
</td>
</tr>
<tr>
@ -330,16 +378,15 @@ The configuration will check your custom path first, then fall back to the defau
still compile on a different computer).
### How do I change which files are included in the mod deploy/zip?
For custom files, you can [add/remove them in the build output](https://stackoverflow.com/a/10828462/262123).
(If your project references another mod, make sure the reference is [_not_ marked 'copy
local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).)
To exclude a file the package copies by default, see `IgnoreModFilePatterns` under
[_configure_](#configure).
* For normal files, you can [add/remove them in the build output](https://stackoverflow.com/a/10828462/262123).
* For assembly files (`*.dll`, `*.exe`, `*.pdb`, or `*.xml`), see the
[`BundleExtraAssemblies` option](#configure).
* To exclude a file which the package copies by default, see the [`IgnoreModFilePaths` or
`IgnoreModFilePatterns` options](#configure).
### Can I use the package for non-mod projects?
You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable
the mod-related package features (see [_configure_](#configure)):
Yep, this works in unit tests and framework projects too. Just disable the mod-related package
features (see [_configure_](#configure)):
```xml
<EnableGameDebugging>false</EnableGameDebugging>
@ -347,9 +394,9 @@ the mod-related package features (see [_configure_](#configure)):
<EnableModZip>false</EnableModZip>
```
If you need to copy the referenced DLLs into your build output, add this too:
To copy referenced DLLs into your build output for unit tests, add this too:
```xml
<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput>
<BundleExtraAssemblies>All</BundleExtraAssemblies>
```
## For SMAPI developers
@ -366,13 +413,31 @@ when you compile it.
## Release notes
## Upcoming release
* Updated for Stardew Valley 1.5.5 and SMAPI 3.13.0. (Older versions are no longer supported.)
* Added `IgnoreModFilePaths` option to ignore literal paths.
* Added `BundleExtraAssemblies` option to copy bundled DLLs into the mod zip/folder.
* Removed the `GameExecutableName` and `GameFramework` options (since they now have the same value
on all platforms).
* Removed the `CopyModReferencesToBuildOutput` option (superseded by `BundleExtraAssemblies`).
* Improved analyzer performance by enabling parallel execution.
**Migration guide for mod authors:**
1. See [_migrate to 64-bit_](https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows) and
[_migrate to Stardew Valley 1.5.5_](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5).
2. Possible changes in your `.csproj` or `.targets` files:
* Replace `$(GameExecutableName)` with `Stardew Valley`.
* Replace `$(GameFramework)` with `MonoGame` and remove any XNA Framework-specific logic.
* Replace `<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput>` with
`<BundleExtraAssemblies>Game</BundleExtraAssemblies>`.
* If you need to bundle extra DLLs besides your mod DLL, see the [`BundleExtraAssemblies`
documentation](#configure).
## 3.3.0
Released 30 March 2021.
* Added a build warning when the mod isn't compiled for `Any CPU`.
* Added a `GameFramework` build property set to `MonoGame` or `Xna` based on the platform. This can be overridden to change which framework it references.
* Added a `GameFramework` build property set to `MonoGame` or `Xna` based on the platform. This can
be overridden to change which framework it references.
* Added support for building mods against the 64-bit Linux version of the game on Windows.
* The package now suppresses the misleading 'processor architecture mismatch' warnings.
@ -380,7 +445,8 @@ Released 30 March 2021.
Released 23 September 2020.
* Reworked and streamlined how the package is compiled.
* Added [SMAPI-ModTranslationClassBuilder](https://github.com/Pathoschild/SMAPI-ModTranslationClassBuilder) files to the ignore list.
* Added [SMAPI-ModTranslationClassBuilder](https://github.com/Pathoschild/SMAPI-ModTranslationClassBuilder)
files to the ignore list.
### 3.2.1
Released 11 September 2020.

View File

@ -57,7 +57,6 @@ SMAPI uses a small number of conditional compilation constants, which you can se
flag | purpose
---- | -------
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled for Windows; if not set, the code assumes Linux/macOS. Set automatically in `common.targets`.
`SMAPI_FOR_XNA` | Whether SMAPI is being compiled for XNA Framework; if not set, the code assumes MonoGame. Set automatically in `common.targets` with the same value as `SMAPI_FOR_WINDOWS`.
## For SMAPI developers
### Compiling from source

View File

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

View File

@ -1,6 +1,4 @@
using System;
using System.IO;
using Microsoft.Win32;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.GameScanning;
using StardewModdingAPI.Toolkit.Utilities;
@ -13,9 +11,6 @@ namespace StardewModdingAPI.Installer.Framework
/*********
** Fields
*********/
/// <summary>The <see cref="Environment.OSVersion"/> value that represents Windows 7.</summary>
private readonly Version Windows7Version = new Version(6, 1);
/// <summary>The underlying toolkit game scanner.</summary>
private readonly GameScanner GameScanner = new GameScanner();
@ -29,9 +24,6 @@ namespace StardewModdingAPI.Installer.Framework
/// <summary>The human-readable OS name and version.</summary>
public string PlatformName { get; }
/// <summary>The name of the Stardew Valley executable.</summary>
public string ExecutableName { get; }
/// <summary>Whether the installer is running on Windows.</summary>
public bool IsWindows => this.Platform == Platform.Windows;
@ -47,7 +39,6 @@ namespace StardewModdingAPI.Installer.Framework
{
this.Platform = EnvironmentUtility.DetectPlatform();
this.PlatformName = EnvironmentUtility.GetFriendlyPlatformName(this.Platform);
this.ExecutableName = EnvironmentUtility.GetExecutableName(this.Platform);
}
/// <summary>Get the installer's version number.</summary>
@ -57,42 +48,6 @@ namespace StardewModdingAPI.Installer.Framework
return new SemanticVersion(raw);
}
/// <summary>Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows.</summary>
/// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
public bool HasNetFramework45()
{
switch (this.Platform)
{
case Platform.Windows:
using (RegistryKey versionKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"))
return versionKey?.GetValue("Release") != null; // .NET Framework 4.5+
default:
throw new NotSupportedException("The installed .NET Framework version can only be checked on Windows.");
}
}
/// <summary>Get whether the current system has XNA Framework installed. This only applies on Windows.</summary>
/// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
public bool HasXna()
{
switch (this.Platform)
{
case Platform.Windows:
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\XNA\Framework"))
return key != null; // XNA Framework 4.0+
default:
throw new NotSupportedException("The installed XNA Framework version can only be checked on Windows.");
}
}
/// <summary>Whether the current OS supports newer versions of .NET Framework.</summary>
public bool CanInstallLatestNetFramework()
{
return Environment.OSVersion.Version >= this.Windows7Version; // Windows 7+
}
/// <summary>Get whether a folder seems to contain the game files.</summary>
/// <param name="dir">The folder to check.</param>
public bool LooksLikeGameFolder(DirectoryInfo dir)

View File

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

View File

@ -39,18 +39,19 @@ namespace StardewModdingApi.Installer
string GetInstallPath(string path) => Path.Combine(installDir.FullName, path);
// current files
yield return GetInstallPath("libgdiplus.dylib"); // Linux/macOS only
yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only
yield return GetInstallPath("StardewModdingAPI.dll");
yield return GetInstallPath("StardewModdingAPI.exe");
yield return GetInstallPath("StardewModdingAPI.exe.config");
yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/macOS only
yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only
yield return GetInstallPath("StardewModdingAPI.runtimeconfig.json");
yield return GetInstallPath("StardewModdingAPI.xml");
yield return GetInstallPath("StardewModdingAPI-x64.exe"); // not normally added to game folder, but may be mistakenly added by a manual install
yield return GetInstallPath("smapi-internal");
yield return GetInstallPath("steam_appid.txt");
// obsolete
yield return GetInstallPath("libgdiplus.dylib"); // before 3.13 (macOS only)
yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *2.0 (renamed to ConsoleCommands)
yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.31.8
@ -70,9 +71,7 @@ namespace StardewModdingApi.Installer
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8
yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8
yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8
yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8
yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8
yield return GetInstallPath("StardewModdingAPI-x64.exe"); // before 3.13
if (modsDir.Exists)
{
@ -149,30 +148,6 @@ namespace StardewModdingApi.Installer
}
#endif
/****
** Check Windows dependencies
****/
if (context.IsWindows)
{
// .NET Framework 4.5+
if (!context.HasNetFramework45())
{
this.PrintError(context.CanInstallLatestNetFramework()
? "Please install the latest version of .NET Framework before installing SMAPI."
: "Please install .NET Framework 4.5 before installing SMAPI."
);
this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details.");
Console.ReadLine();
return;
}
if (!context.HasXna())
{
this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup.");
Console.ReadLine();
return;
}
}
/****
** read command-line arguments
****/
@ -270,51 +245,20 @@ namespace StardewModdingApi.Installer
// get folders
DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath);
paths = new InstallerPaths(bundleDir, installDir, context.ExecutableName);
paths = new InstallerPaths(bundleDir, installDir);
}
/*********
** Step 4: validate assumptions
*********/
// not 64-bit on Windows
if (context.Platform == Platform.Windows)
{
FileInfo linuxExecutable = new FileInfo(Path.Combine(paths.GamePath, "StardewValley.exe"));
if (linuxExecutable.Exists && this.Is64Bit(linuxExecutable.FullName))
{
this.PrintError("Oops! The detected game install path seems to be unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info.");
Console.ReadLine();
return;
}
}
// executable exists
if (!File.Exists(paths.ExecutablePath))
if (!File.Exists(paths.GameDllPath))
{
this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
Console.ReadLine();
return;
}
// not Stardew Valley 1.5.5+
if (File.Exists(Path.Combine(paths.GamePath, "Stardew Valley.dll")))
{
this.PrintError("Oops! The detected game install path seems to be Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io.");
Console.ReadLine();
return;
}
// game folder doesn't contain paths beyond the max limit
{
string[] tooLongPaths = PathUtilities.GetTooLongPaths(Path.Combine(paths.GamePath, "Mods")).ToArray();
if (tooLongPaths.Any())
{
this.PrintError($"SMAPI can't install to the detected game folder, because some of its files exceed the maximum {context.Platform} path length.\nIf you need help fixing this error, see https://smapi.io/help\n\nAffected paths:\n {string.Join("\n ", tooLongPaths)}");
Console.ReadLine();
return;
}
}
Console.Clear();
@ -387,11 +331,11 @@ namespace StardewModdingApi.Installer
** Always uninstall old files
****/
// restore game launcher
if (context.IsUnix && File.Exists(paths.UnixBackupLauncherPath))
if (context.IsUnix && File.Exists(paths.BackupLaunchScriptPath))
{
this.PrintDebug("Removing SMAPI launcher...");
this.InteractivelyDelete(paths.UnixLauncherPath);
File.Move(paths.UnixBackupLauncherPath, paths.UnixLauncherPath);
this.InteractivelyDelete(paths.VanillaLaunchScriptPath);
File.Move(paths.BackupLaunchScriptPath, paths.VanillaLaunchScriptPath);
}
// remove old files
@ -439,30 +383,41 @@ namespace StardewModdingApi.Installer
this.PrintDebug("Safely replacing game launcher...");
// back up & remove current launcher
if (File.Exists(paths.UnixLauncherPath))
if (File.Exists(paths.VanillaLaunchScriptPath))
{
if (!File.Exists(paths.UnixBackupLauncherPath))
File.Move(paths.UnixLauncherPath, paths.UnixBackupLauncherPath);
if (!File.Exists(paths.BackupLaunchScriptPath))
File.Move(paths.VanillaLaunchScriptPath, paths.BackupLaunchScriptPath);
else
this.InteractivelyDelete(paths.UnixLauncherPath);
this.InteractivelyDelete(paths.VanillaLaunchScriptPath);
}
// add new launcher
File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath);
File.Move(paths.NewLaunchScriptPath, paths.VanillaLaunchScriptPath);
// mark file executable
// mark files executable
// (MSBuild doesn't keep permission flags for files zipped in a build task.)
new Process
foreach (string path in new[] { paths.VanillaLaunchScriptPath, paths.UnixSmapiExecutablePath })
{
StartInfo = new ProcessStartInfo
new Process
{
FileName = "chmod",
Arguments = $"755 \"{paths.UnixLauncherPath}\"",
CreateNoWindow = true
}
}.Start();
StartInfo = new ProcessStartInfo
{
FileName = "chmod",
Arguments = $"755 \"{path}\"",
CreateNoWindow = true
}
}.Start();
}
}
// copy the game's deps.json file
// (This is needed to resolve native DLLs like libSkiaSharp.)
File.Copy(
sourceFileName: Path.Combine(paths.GamePath, "Stardew Valley.deps.json"),
destFileName: Path.Combine(paths.GamePath, "StardewModdingAPI.deps.json"),
overwrite: true
);
// create mods directory (if needed)
if (!paths.ModsDir.Exists)
{
@ -527,7 +482,7 @@ namespace StardewModdingApi.Installer
/*********
** Step 6: final instructions
** Step 7: final instructions
*********/
if (context.IsWindows)
{
@ -556,13 +511,6 @@ namespace StardewModdingApi.Installer
/*********
** Private methods
*********/
/// <summary>Get whether an executable is 64-bit.</summary>
/// <param name="executablePath">The absolute path to the executable file.</param>
private bool Is64Bit(string executablePath)
{
return LowLevelEnvironmentUtility.Is64BitAssembly(executablePath);
}
/// <summary>Get the display text for a color scheme.</summary>
/// <param name="scheme">The color scheme.</param>
private string GetDisplayText(MonitorColorScheme scheme)
@ -725,7 +673,7 @@ namespace StardewModdingApi.Installer
{
// get path from user
Console.WriteLine();
this.PrintInfo($"Type the file path to the game directory (the one containing '{context.ExecutableName}'), then press enter.");
this.PrintInfo($"Type the file path to the game directory (the one containing '{Constants.GameDllName}'), then press enter.");
string path = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(path))
{

View File

@ -31,8 +31,7 @@ namespace StardewModdingApi.Installer
public static void Main(string[] args)
{
// find install bundle
PlatformID platform = Environment.OSVersion.Platform;
FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-install.dat"));
FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, "install.dat"));
if (!zipFile.Exists)
{
Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})");

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<RootNamespace>StardewModdingAPI.Installer</RootNamespace>
<Description>The SMAPI installer for players.</Description>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>

View File

@ -24,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. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on
1. Unzip "internal/windows/install.dat" (on Windows) or "internal/unix/install.dat" (on
Linux/macOS). You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent
confusion.
2. Copy the files from the folder you just unzipped into your game folder. The

View File

@ -0,0 +1,14 @@
{
"runtimeOptions": {
"tfm": "net5.0",
"includedFrameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "5.0.7"
}
],
"configProperties": {
"System.Runtime.TieredCompilation": false
}
}
}

View File

@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net5.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "5.0.0"
},
"configProperties": {
"System.Runtime.TieredCompilation": false
}
}
}

View File

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

View File

@ -1,51 +1,19 @@
#!/usr/bin/env bash
# MonoKickstart Shell Script
# Written by Ethan "flibitijibibo" Lee
# Modified for SMAPI by various contributors
# Move to script's directory
##########
## Initial setup
##########
# move to script's directory
cd "$(dirname "$0")" || exit $?
# Get the system architecture
UNAME=$(uname)
ARCH=$(uname -m)
# MonoKickstart picks the right libfolder, so just execute the right binary.
if [ "$UNAME" == "Darwin" ]; then
# ... Except on OSX.
export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/
# El Capitan is a total idiot and wipes this variable out, making the
# Steam overlay disappear. This sidesteps "System Integrity Protection"
# and resets the variable with Valve's own variable (they provided this
# fix by the way, thanks Valve!). Note that you will need to update your
# launch configuration to the script location, NOT just the app location
# (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app).
# -flibit
if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then
export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES"
fi
# this was here before
ln -sf mcs.bin.osx mcs
# fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI
if [ -f libgdiplus.dylib ]; then
rm libgdiplus.dylib
fi
if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then
ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib
fi
# create bin file
# Note: don't overwrite if it's identical, to avoid resetting permission flags
if [ ! -x StardewModdingAPI.bin.osx ] || ! cmp StardewValley.bin.osx StardewModdingAPI.bin.osx >/dev/null 2>&1; then
cp -p StardewValley.bin.osx StardewModdingAPI.bin.osx
fi
# Make sure we're running in Terminal (so the user can see errors/warnings/update alerts).
# Previously we would just use `open -a Terminal` to launch the .bin.osx file, but that
# doesn't let us set environment variables.
##########
## Open terminal if needed
##########
# on macOS, make sure we're running in a Terminal
# Besides letting the player see errors/warnings/alerts in the console, this is also needed because
# Steam messes with the PATH.
if [ "$(uname)" == "Darwin" ]; then
if [ ! -t 1 ]; then # https://stackoverflow.com/q/911168/262123
# sanity check to make sure we don't have an infinite loop of opening windows
SKIP_TERMINAL=false
@ -68,21 +36,38 @@ if [ "$UNAME" == "Darwin" ]; then
exit 0
fi
fi
fi
# launch SMAPI
LC_ALL="C" ./StardewModdingAPI.bin.osx "$@"
##########
## Validate assumptions
##########
# script must be run from the game folder
if [ ! -f "Stardew Valley.dll" ]; then
echo "Oops! SMAPI must be placed in the Stardew Valley game folder.\nSee instructions: https://stardewvalleywiki.com/Modding:Player_Guide";
read
exit 1
fi
# .NET 5 must be installed
if ! command -v dotnet >/dev/null 2>&1; then
echo "Oops! You must have .NET 5 installed to use SMAPI: https://dotnet.microsoft.com/download";
read
exit 1
fi
##########
## Launch SMAPI
##########
# macOS
if [ "$(uname)" == "Darwin" ]; then
dotnet StardewModdingAPI.dll "$@"
# Linux
else
# choose binary file to launch
LAUNCH_FILE=""
if [ "$ARCH" == "x86_64" ]; then
ln -sf mcs.bin.x86_64 mcs
cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64
LAUNCH_FILE="./StardewModdingAPI.bin.x86_64"
else
ln -sf mcs.bin.x86 mcs
cp StardewValley.bin.x86 StardewModdingAPI.bin.x86
LAUNCH_FILE="./StardewModdingAPI.bin.x86"
fi
LAUNCH_FILE="./StardewModdingAPI"
export LAUNCH_FILE
# select terminal (prefer xterm for best compatibility, then known supported terminals)
@ -105,44 +90,44 @@ else
terminal|termite)
# consumes only one argument after -e
# options containing space characters are unsupported
exec $TERMINAL_NAME -e "env TERM=xterm LC_ALL=\"C\" $LAUNCH_FILE $@"
exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@"
;;
xterm|konsole|alacritty)
# consumes all arguments after -e
exec $TERMINAL_NAME -e env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@"
;;
terminator|xfce4-terminal|mate-terminal)
# consumes all arguments after -x
exec $TERMINAL_NAME -x env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@"
;;
gnome-terminal)
# consumes all arguments after --
exec $TERMINAL_NAME -- env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@"
;;
kitty)
# consumes all trailing arguments
exec $TERMINAL_NAME env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@"
;;
*)
# If we don't know the terminal, just try to run it in the current shell.
# If THAT fails, launch with no output.
env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
env TERM=xterm $LAUNCH_FILE "$@"
if [ $? -eq 127 ]; then
exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@"
exec $LAUNCH_FILE --no-terminal "$@"
fi
esac
## terminal isn't executable; fallback to current shell or no terminal
else
echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell."
env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
env TERM=xterm $LAUNCH_FILE "$@"
if [ $? -eq 127 ]; then
exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@"
exec $LAUNCH_FILE --no-terminal "$@"
fi
fi
fi

View File

@ -1,8 +1,49 @@
@echo off
echo "%~dp0" | findstr /C:"%TEMP%" 1>nul
if not errorlevel 1 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
pause
) else (
start /WAIT /B internal\windows-install.exe
SET installerDir=%~dp0
REM make sure we're not running within a zip folder
echo %installerDir% | findstr /C:"%TEMP%" 1>nul
if %ERRORLEVEL% EQU 0 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
echo.
pause
exit
)
REM make sure .NET 5 is installed
WHERE dotnet /q
if %ERRORLEVEL% NEQ 0 (
echo Oops! You must have .NET 5 ^(desktop x64^) installed to use SMAPI: https://dotnet.microsoft.com/download/dotnet/5.0/runtime
echo.
pause
exit
)
dotnet --info | findstr /C:"Microsoft.WindowsDesktop.App 5." 1>nul
if %ERRORLEVEL% NEQ 0 (
echo Oops! You must have .NET 5 ^(desktop x64^) installed to use SMAPI: https://dotnet.microsoft.com/download/dotnet/5.0/runtime
echo.
pause
exit
)
REM make sure an antivirus hasn't deleted the installer DLL
if not exist "%installerDir%internal\windows\SMAPI.Installer.dll" (
echo Oops! SMAPI is missing one of its files. Your antivirus might have deleted it.
echo Missing file: %installerDir%internal\windows\SMAPI.Installer.dll
echo.
pause
exit
)
REM start installer
dotnet internal\windows\SMAPI.Installer.dll
REM keep window open if it failed
if %ERRORLEVEL% NEQ 0 (
echo.
echo Oops! The SMAPI installer seems to have failed. The error details may be shown above.
echo.
pause
exit
)

View File

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

View File

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

View File

@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using StardewModdingAPI.ModBuildConfig.Framework;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.ModBuildConfig
{
@ -17,6 +18,10 @@ namespace StardewModdingAPI.ModBuildConfig
/*********
** Accessors
*********/
/// <summary>The name (without extension or path) of the current mod's DLL.</summary>
[Required]
public string ModDllName { get; set; }
/// <summary>The name of the mod folder.</summary>
[Required]
public string ModFolderName { get; set; }
@ -45,9 +50,15 @@ 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>
/// <summary>A comma-separated list of regex patterns matching files to ignore when deploying or zipping the mod.</summary>
public string IgnoreModFilePatterns { get; set; }
/// <summary>A comma-separated list of relative file paths to ignore when deploying or zipping the mod.</summary>
public string IgnoreModFilePaths { get; set; }
/// <summary>A comma-separated list of <see cref="ExtraAssemblyTypes"/> values which indicate which extra DLLs to bundle.</summary>
public string BundleExtraAssemblies { get; set; }
/*********
** Public methods
@ -69,11 +80,15 @@ namespace StardewModdingAPI.ModBuildConfig
try
{
// parse extra DLLs to bundle
ExtraAssemblyTypes bundleAssemblyTypes = this.GetExtraAssembliesToBundleOption();
// parse ignore patterns
string[] ignoreFilePaths = this.GetCustomIgnoreFilePaths().ToArray();
Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray();
// get mod info
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip);
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip);
// deploy mod files
if (this.EnableModDeploy)
@ -134,6 +149,28 @@ namespace StardewModdingAPI.ModBuildConfig
}
}
/// <summary>Parse the extra assembly types which should be bundled with the mod.</summary>
private ExtraAssemblyTypes GetExtraAssembliesToBundleOption()
{
ExtraAssemblyTypes flags = ExtraAssemblyTypes.None;
if (!string.IsNullOrWhiteSpace(this.BundleExtraAssemblies))
{
foreach (string raw in this.BundleExtraAssemblies.Split(','))
{
if (!Enum.TryParse(raw, out ExtraAssemblyTypes type))
{
this.Log.LogWarning($"[mod build package] Ignored invalid <{nameof(this.BundleExtraAssemblies)}> value '{raw}', expected one of '{string.Join("', '", Enum.GetNames(typeof(ExtraAssemblyTypes)))}'.");
continue;
}
flags |= type;
}
}
return flags;
}
/// <summary>Get the custom ignore patterns provided by the user.</summary>
private IEnumerable<Regex> GetCustomIgnorePatterns()
{
@ -157,6 +194,29 @@ namespace StardewModdingAPI.ModBuildConfig
}
}
/// <summary>Get the custom relative file paths provided by the user to ignore.</summary>
private IEnumerable<string> GetCustomIgnoreFilePaths()
{
if (string.IsNullOrWhiteSpace(this.IgnoreModFilePaths))
yield break;
foreach (string raw in this.IgnoreModFilePaths.Split(','))
{
string path;
try
{
path = PathUtilities.NormalizePath(raw);
}
catch (Exception ex)
{
this.Log.LogWarning($"[mod build package] Ignored invalid <{nameof(this.IgnoreModFilePaths)}> path {raw}:\n{ex}");
continue;
}
yield return path;
}
}
/// <summary>Copy the mod files into the game's mod folder.</summary>
/// <param name="files">The files to include.</param>
/// <param name="modFolderPath">The folder path to create with the mod files.</param>

View File

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

View File

@ -21,6 +21,45 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// <summary>The files that are part of the package.</summary>
private readonly IDictionary<string, FileInfo> Files;
/// <summary>The file extensions used by assembly files.</summary>
private readonly ISet<string> AssemblyFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".dll",
".exe",
".pdb",
".xml"
};
/// <summary>The DLLs which match the <see cref="ExtraAssemblyTypes.Game"/> type.</summary>
private readonly ISet<string> GameDllNames = new HashSet<string>
{
// SMAPI
"0Harmony",
"Mono.Cecil",
"Mono.Cecil.Mdb",
"Mono.Cecil.Pdb",
"MonoMod.Common",
"Newtonsoft.Json",
"StardewModdingAPI",
"SMAPI.Toolkit",
"SMAPI.Toolkit.CoreInterfaces",
"TMXTile",
// game + framework
"BmFont",
"FAudio-CS",
"GalaxyCSharp",
"GalaxyCSharpGlue",
"Lidgren.Network",
"MonoGame.Framework",
"SkiaSharp",
"Stardew Valley",
"StardewValley.GameData",
"Steamworks.NET",
"TextCopy",
"xTile"
};
/*********
** Public methods
@ -28,10 +67,13 @@ 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="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param>
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
/// <param name="bundleAssemblyTypes">The extra assembly types which should be bundled with the mod.</param>
/// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param>
/// <param name="validateRequiredModFiles">Whether to validate that required mod files like the manifest are present.</param>
/// <exception cref="UserErrorException">The mod package isn't valid.</exception>
public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns, bool validateRequiredModFiles)
public ModFileManager(string projectDir, string targetDir, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, ExtraAssemblyTypes bundleAssemblyTypes, string modDllName, bool validateRequiredModFiles)
{
this.Files = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
@ -47,7 +89,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
string relativePath = entry.Item1;
FileInfo file = entry.Item2;
if (!this.ShouldIgnore(file, relativePath, ignoreFilePatterns))
if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName))
this.Files[relativePath] = file;
}
@ -149,36 +191,72 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// <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="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param>
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
private bool ShouldIgnore(FileInfo file, string relativePath, Regex[] ignoreFilePatterns)
/// <param name="bundleAssemblyTypes">The extra assembly types which should be bundled with the mod.</param>
/// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param>
private bool ShouldIgnore(FileInfo file, string relativePath, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, ExtraAssemblyTypes bundleAssemblyTypes, string modDllName)
{
return
// release zips
this.EqualsInvariant(file.Extension, ".zip")
// apply custom patterns
if (ignoreFilePaths.Any(p => p == relativePath) || ignoreFilePatterns.Any(p => p.IsMatch(relativePath)))
return true;
// Harmony (bundled into SMAPI)
|| this.EqualsInvariant(file.Name, "0Harmony.dll")
// ignore unneeded files
{
bool shouldIgnore =
// 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.pdb")
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")
// *.deps.json (only SMAPI's top-level one is used)
|| file.Name.EndsWith(".deps.json")
// mod translation class builder (not used at runtime)
|| this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.dll")
|| this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.pdb")
|| this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.xml")
// code analysis files
|| file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.OrdinalIgnoreCase)
|| file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.OrdinalIgnoreCase)
// code analysis files
|| file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.OrdinalIgnoreCase)
|| file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.OrdinalIgnoreCase)
// translation class builder (not used at runtime)
|| (
file.Name.StartsWith("Pathoschild.Stardew.ModTranslationClassBuilder")
&& this.AssemblyFileExtensions.Contains(file.Extension)
)
// OS metadata files
|| this.EqualsInvariant(file.Name, ".DS_Store")
|| this.EqualsInvariant(file.Name, "Thumbs.db")
// OS metadata files
|| this.EqualsInvariant(file.Name, ".DS_Store")
|| this.EqualsInvariant(file.Name, "Thumbs.db");
if (shouldIgnore)
return true;
}
// custom ignore patterns
|| ignoreFilePatterns.Any(p => p.IsMatch(relativePath));
// check for bundled assembly types
// When bundleAssemblyTypes is set, *all* dependencies are copied into the build output but only those which match the given assembly types should be bundled.
if (bundleAssemblyTypes != ExtraAssemblyTypes.None)
{
var type = this.GetExtraAssemblyType(file, modDllName);
if (type != ExtraAssemblyTypes.None && !bundleAssemblyTypes.HasFlag(type))
return true;
}
return false;
}
/// <summary>Get the extra assembly type for a file, assuming that the user specified one or more extra types to bundle.</summary>
/// <param name="file">The file to check.</param>
/// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param>
private ExtraAssemblyTypes GetExtraAssemblyType(FileInfo file, string modDllName)
{
string baseName = Path.GetFileNameWithoutExtension(file.Name);
string extension = file.Extension;
if (baseName == modDllName || !this.AssemblyFileExtensions.Contains(extension))
return ExtraAssemblyTypes.None;
if (this.GameDllNames.Contains(baseName))
return ExtraAssemblyTypes.Game;
if (baseName.StartsWith("System.", StringComparison.OrdinalIgnoreCase) || baseName.StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase))
return ExtraAssemblyTypes.System;
return ExtraAssemblyTypes.ThirdParty;
}
/// <summary>Get whether a string is equal to another case-insensitively.</summary>

View File

@ -2,28 +2,28 @@
<PropertyGroup>
<!--build-->
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
<TargetFramework>net452</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!--NuGet package-->
<PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId>
<Title>Build package for SMAPI mods</Title>
<Version>3.3.0</Version>
<Version>4.0.0</Version>
<Authors>Pathoschild</Authors>
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later.</Description>
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>images/icon.png</PackageIcon>
<PackageProjectUrl>https://smapi.io/package/readme</PackageProjectUrl>
<IncludeBuildOutput>false</IncludeBuildOutput>
<!--copy dependency DLLs to bin folder so we can include them in package -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Build" />
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="Microsoft.Build.Utilities.v4.0" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Web.Extensions" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -12,8 +12,8 @@
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<!-- recognise XNA Framework DLLs in the GAC (only affects mods using new csproj format) -->
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
<!-- don't create the 'refs' folder (which isn't useful for mods) -->
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<!-- suppress processor architecture mismatch warning (mods should be compiled in 'Any CPU' so they work in both 32-bit and 64-bit mode) -->
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
@ -26,10 +26,10 @@
<EnableModZip Condition="'$(EnableModZip)' == ''">true</EnableModZip>
<EnableHarmony Condition="'$(EnableHarmony)' == ''">false</EnableHarmony>
<EnableGameDebugging Condition="'$(EnableGameDebugging)' == ''">true</EnableGameDebugging>
<CopyModReferencesToBuildOutput Condition="'$(CopyModReferencesToBuildOutput)' == '' OR ('$(CopyModReferencesToBuildOutput)' != 'true' AND '$(CopyModReferencesToBuildOutput)' != 'false')">false</CopyModReferencesToBuildOutput>
<BundleExtraAssemblies Condition="'$(BundleExtraAssemblies)' == ''"></BundleExtraAssemblies>
<GameFramework Condition="'$(GameFramework)' == '' AND '$(OS)' == 'Windows_NT'">Xna</GameFramework>
<GameFramework Condition="'$(GameFramework)' == ''">MonoGame</GameFramework>
<!-- coppy referenced DLLs into build output -->
<CopyLocalLockFileAssemblies Condition="$(BundleExtraAssemblies.Contains('ThirdParty')) OR $(BundleExtraAssemblies.Contains('Game')) OR $(BundleExtraAssemblies.Contains('System')) OR $(BundleExtraAssemblies.Contains('All'))">true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(OS)' == 'Windows_NT' AND '$(EnableGameDebugging)' == 'true'">
@ -43,37 +43,20 @@
<!--*********************************************
** Add assembly references
**********************************************-->
<!-- common -->
<ItemGroup>
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.exe" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
<!-- game -->
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
<!-- Windows only -->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
<!-- SMAPI -->
<Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
<Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
<!-- Game framework -->
<Choose>
<When Condition="'$(GameFramework)' == 'Xna'">
<ItemGroup>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" />
</ItemGroup>
</Otherwise>
</Choose>
<!-- Harmony -->
<Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
</ItemGroup>
<!--*********************************************
@ -85,8 +68,8 @@
<!-- invalid game path -->
<Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see https://smapi.io/package/custom-game-path." ContinueOnError="false" />
<Error Condition="!Exists('$(GamePath)\$(GameExecutableName).exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the $(GameExecutableName) file. If this folder is invalid, delete it and the package will autodetect another game install path." ContinueOnError="false" />
<Error Condition="!Exists('$(GamePath)\StardewModdingAPI.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." ContinueOnError="false" />
<Error Condition="!Exists('$(GamePath)\Stardew Valley.dll')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the Stardew Valley file. If this folder is invalid, delete it and the package will autodetect another game install path." ContinueOnError="false" />
<Error Condition="!Exists('$(GamePath)\StardewModdingAPI.dll')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." ContinueOnError="false" />
<!-- invalid target architecture (note: internal value is 'AnyCPU', value shown in Visual Studio is 'Any CPU') -->
<Warning Condition="'$(Platform)' != 'AnyCPU'" Text="The target platform should be set to 'Any CPU' for compatibility with both 32-bit and 64-bit versions of Stardew Valley (currently set to '$(Platform)'). See https://smapi.io/package/wrong-processor-architecture for details." HelpLink="https://smapi.io/package/wrong-processor-architecture" />
@ -98,6 +81,7 @@
**********************************************-->
<Target Name="AfterBuild">
<DeployModTask
ModDllName="$(TargetName)"
ModFolderName="$(ModFolderName)"
ModZipPath="$(ModZipPath)"
@ -108,6 +92,9 @@
TargetDir="$(TargetDir)"
GameModsDir="$(GameModsPath)"
IgnoreModFilePatterns="$(IgnoreModFilePatterns)"
IgnoreModFilePaths="$(IgnoreModFilePaths)"
BundleExtraAssemblies="$(BundleExtraAssemblies)"
/>
</Target>
</Project>

View File

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

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>ConsoleCommands</AssemblyName>
<RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
@ -13,32 +13,12 @@
</ItemGroup>
<ItemGroup>
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" />
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" />
</ItemGroup>
<!-- Windows only -->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
</ItemGroup>
<!-- Game framework -->
<Choose>
<When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
<ItemGroup>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -19,9 +19,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
public override void Apply(Harmony harmony, IMonitor monitor)
{
harmony.Patch(
original: Constants.GameFramework == GameFramework.Xna
? this.RequireMethod<SpriteBatch>("InternalDraw")
: this.RequireMethod<SpriteBatch>("CheckValid", new[] { typeof(Texture2D) }),
original: this.RequireMethod<SpriteBatch>("CheckValid", new[] { typeof(Texture2D) }),
postfix: this.GetHarmonyMethod(nameof(SpriteBatchPatcher.After_CheckValid))
);
}
@ -30,13 +28,8 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/*********
** Private methods
*********/
#if SMAPI_FOR_XNA
/// <summary>The method to call after <see cref="SpriteBatch.InternalDraw"/>.</summary>
/// <param name="texture">The texture to validate.</param>
#else
/// <summary>The method to call after <see cref="SpriteBatch.CheckValid"/>.</summary>
/// <param name="texture">The texture to validate.</param>
#endif
private static void After_CheckValid(Texture2D texture)
{
if (texture?.IsDisposed == true)

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>ErrorHandler</AssemblyName>
<RootNamespace>StardewModdingAPI.Mods.ErrorHandler</RootNamespace>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
@ -14,33 +14,12 @@
</ItemGroup>
<ItemGroup>
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" />
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" />
</ItemGroup>
<!-- Windows only -->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
</ItemGroup>
<!-- Game framework -->
<Choose>
<When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
<ItemGroup>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<None Update="i18n\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" />

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>SaveBackup</AssemblyName>
<RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>SMAPI.Tests</AssemblyName>
<RootNamespace>SMAPI.Tests</RootNamespace>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>latest</LangVersion>
</PropertyGroup>
@ -16,16 +16,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NUnit" Version="3.13.2" />
</ItemGroup>
<ItemGroup>
<Reference Include="$(GameExecutableName)">
<HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="True" />
</ItemGroup>
<ItemGroup>

View File

@ -182,18 +182,14 @@ namespace SMAPI.Tests.Utilities
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
public void NormalizeAssetName(SamplePath path)
{
if (Path.IsPathRooted(path.OriginalPath) || path.OriginalPath.StartsWith("/") || path.OriginalPath.StartsWith("\\"))
if (Path.IsPathRooted(path.OriginalPath) || path.OriginalPath.StartsWith('/') || path.OriginalPath.StartsWith('\\'))
Assert.Ignore("Absolute paths can't be used as asset names.");
// act
string normalized = PathUtilities.NormalizeAssetName(path.OriginalPath);
// assert
#if SMAPI_FOR_WINDOWS
Assert.AreEqual(path.NormalizedOnWindows, normalized);
#else
Assert.AreEqual(path.NormalizedOnUnix, normalized);
#endif
Assert.AreEqual(path.NormalizedOnUnix, normalized); // MonoGame uses the Linux format
}
/****

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<RootNamespace>StardewModdingAPI</RootNamespace>
<Description>Provides toolkit interfaces which are available to SMAPI mods.</Description>
<TargetFrameworks>net452;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net5.0; netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@ -0,0 +1,9 @@
namespace StardewModdingAPI.Toolkit.Framework
{
/// <summary>Contains the SMAPI installer's constants and assumptions.</summary>
internal static class Constants
{
/// <summary>The name of the game's main DLL, used to detect game folders.</summary>
public const string GameDllName = "Stardew Valley.dll";
}
}

View File

@ -56,10 +56,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
{
return
dir.Exists
&& (
dir.EnumerateFiles("StardewValley.exe").Any()
|| dir.EnumerateFiles("Stardew Valley.exe").Any()
);
&& dir.EnumerateFiles("Stardew Valley.dll").Any();
}

View File

@ -80,15 +80,6 @@ namespace StardewModdingAPI.Toolkit.Framework
return name;
}
/// <summary>Get the name of the Stardew Valley executable.</summary>
/// <param name="platform">The current platform.</param>
public static string GetExecutableName(string platform)
{
return platform == nameof(Platform.Windows)
? "Stardew Valley.exe"
: "StardewValley.exe";
}
/// <summary>Get whether an executable is 64-bit.</summary>
/// <param name="path">The absolute path to the assembly file.</param>
public static bool Is64BitAssembly(string path)

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
<Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description>
<TargetFrameworks>net452;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net5.0; netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@ -12,8 +12,8 @@
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" />
<PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
</ItemGroup>
<ItemGroup>

View File

@ -40,13 +40,6 @@ namespace StardewModdingAPI.Toolkit.Utilities
return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString());
}
/// <summary>Get the name of the Stardew Valley executable.</summary>
/// <param name="platform">The current platform.</param>
public static string GetExecutableName(Platform platform)
{
return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString());
}
/// <summary>Get whether an executable is 64-bit.</summary>
/// <param name="path">The absolute path to the assembly file.</param>
public static bool Is64BitAssembly(string path)

View File

@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
namespace StardewModdingAPI.Toolkit.Utilities
@ -42,5 +44,16 @@ namespace StardewModdingAPI.Toolkit.Utilities
if (entry.Exists)
throw new IOException($"Timed out trying to delete {entry.FullName}");
}
/// <summary>Get the MD5 hash for a file.</summary>
/// <param name="absolutePath">The absolute file path.</param>
public static string GetFileHash(string absolutePath)
{
using FileStream stream = File.OpenRead(absolutePath);
using MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
@ -27,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
public static readonly char PreferredPathSeparator = Path.DirectorySeparatorChar;
/// <summary>The preferred directory separator character in an asset key.</summary>
public static readonly char PreferredAssetSeparator = PathUtilities.PreferredPathSeparator;
public static readonly char PreferredAssetSeparator = '/';
/*********
@ -88,14 +87,18 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>Get a directory or file path relative to a given source path. If no relative path is possible (e.g. the paths are on different drives), an absolute path is returned.</summary>
/// <param name="sourceDir">The source folder path.</param>
/// <param name="targetPath">The target folder or file path.</param>
/// <remarks>
///
/// NOTE: this is a heuristic implementation that works in the cases SMAPI needs it for, but it doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between UNC paths on Windows). This should be replaced with the more comprehensive <c>Path.GetRelativePath</c> if the game ever migrates to .NET Core.
///
/// </remarks>
[Pure]
public static string GetRelativePath(string sourceDir, string targetPath)
{
#if NET5_0
return Path.GetRelativePath(sourceDir, targetPath);
#else
// NOTE:
// this is a heuristic implementation that works in the cases SMAPI needs it for, but it
// doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between
// UNC paths on Windows). SMAPI and mods will use the more robust .NET 5 version anyway
// though, this is only for compatibility with the mod build package.
// convert to URIs
Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
@ -123,6 +126,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
}
return relative;
#endif
}
/// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary>
@ -145,32 +149,5 @@ namespace StardewModdingAPI.Toolkit.Utilities
{
return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase);
}
/// <summary>Get the paths which exceed the OS length limit.</summary>
/// <param name="rootPath">The root path to search.</param>
internal static IEnumerable<string> GetTooLongPaths(string rootPath)
{
if (!Directory.Exists(rootPath))
return new string[0];
return Directory
.EnumerateFileSystemEntries(rootPath, "*.*", SearchOption.AllDirectories)
.Where(PathUtilities.IsPathTooLong);
}
/// <summary>Get whether a file or directory path exceeds the OS path length limit.</summary>
/// <param name="path">The path to test.</param>
internal static bool IsPathTooLong(string path)
{
try
{
_ = Path.GetFullPath(path);
return false;
}
catch (PathTooLongException)
{
return true;
}
}
}
}

View File

@ -167,6 +167,60 @@
"~ | StatusReasonPhrase": "split-screen mode was added in Stardew Valley 1.5"
},
/*********
** Broke in SDV 1.5.5
*********/
"Animated Portrait Framework": {
"ID": "akai.AnimatedPortrait",
"~1.0.0 | Status": "AssumeBroken",
"~1.0.0 | StatusReasonDetails": "requires the 'System.Windows.Forms' API, which isn't available in .NET 5"
},
"Audio Devices": {
"ID": "maxvollmer.audiodevices",
"~3.0.1 | Status": "AssumeBroken",
"~3.0.1 | StatusReasonDetails": "fails to load due to an outdated implementation of the game's 'IAudioEngine' interface"
},
"Battery Warning": {
"ID": "Husky110.BatteryWarningMod",
"~1.0.4 | Status": "AssumeBroken",
"~1.0.4 | StatusReasonDetails": "requires the 'System.Management' API, which is a different DLL in .NET 5"
},
"Junimo Studio": {
"ID": "Becks723.JunimoStudio",
"~2.0.1 | Status": "AssumeBroken",
"~2.0.1 | StatusReasonDetails": "requires 'Microsoft.Xna.Framework.Audio.AudioCategory' which doesn't exist in MonoGame"
},
"Skip Intro": {
"ID": "Pathoschild.SkipIntro",
"~1.9.1 | Status": "AssumeBroken",
"~1.9.1 | StatusReasonDetails": "causes freeze during game launch"
},
"Stardew Hack": {
"ID": "bcmpinc.StardewHack",
"~5.1.0 | Status": "AssumeBroken",
"~5.1.0 | StatusReasonDetails": "runtime error when initializing due to an API change between .NET Framework and .NET 5"
},
"Stardew Valley Expanded": {
"ID": "FlashShifter.SVECode",
"~1.13.11 | Status": "AssumeBroken",
"~1.13.11 | StatusReasonDetails": "fails to load due to an outdated implementation of the game's 'ICue' interface"
},
"Stardew Web": {
"ID": "prism99.stardewweb",
"~0.7.2 | Status": "AssumeBroken",
"~0.7.2 | StatusReasonDetails": "requires the 'System.Drawing' API, which isn't available in .NET 5"
},
"Sundrop City": {
"ID": "SundropTeam.SundropCity",
"~0.4.1 | Status": "AssumeBroken",
"~0.4.1 | StatusReasonDetails": "causes freeze during game launch"
},
"Video Player": {
"ID": "aedenthorn.VideoPlayer",
"~0.2.5 | Status": "AssumeBroken",
"~0.2.5 | StatusReasonDetails": "requires an XNA Framework API that's not available in MonoGame and causes a crash to desktop"
},
/*********
** Broke in SMAPI 3.12.0
*********/
@ -205,11 +259,6 @@
"~1.9.3 | Status": "AssumeBroken",
"~1.9.3 | StatusReasonDetails": "fails to load with 'ReflectionTypeLoadException' error"
},
"Stardew Hack": {
"ID": "bcmpinc.StardewHack",
"~5.0.0 | Status": "AssumeBroken",
"~5.0.0 | StatusReasonDetails": "causes Harmony patching errors for other mods"
},
"Tilled Soil Decay": {
"ID": "bcmpinc.TilledSoilDecay",
"~4.1.0 | Status": "AssumeBroken",
@ -238,12 +287,6 @@
/*********
** Broke in SDV 1.5 (SMAPI mods)
*********/
"Audio Devices": {
"ID": "maxvollmer.audiodevices",
"~2.0.0 | Status": "AssumeBroken",
"~2.0.0 | StatusReasonDetails": "causes crash to desktop when starting the game"
},
"ChestEx": {
"ID": "berkayylmao.ChestEx",
"~1.3.4 | Status": "AssumeBroken",

View File

@ -40,15 +40,10 @@ namespace StardewModdingAPI
internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
/// <summary>The game framework running the game.</summary>
internal static GameFramework GameFramework { get; } =
#if SMAPI_FOR_XNA
GameFramework.Xna;
#else
GameFramework.MonoGame;
#endif
internal static GameFramework GameFramework { get; } = GameFramework.MonoGame;
/// <summary>The game's assembly name.</summary>
internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley";
internal static string GameAssemblyName { get; } = "Stardew Valley";
/// <summary>The <see cref="Context.ScreenId"/> value which should appear in the SMAPI log, if any.</summary>
internal static int? LogScreenId { get; set; }
@ -70,10 +65,10 @@ namespace StardewModdingAPI
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion);
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4");
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.5");
/// <summary>The maximum supported version of Stardew Valley.</summary>
public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.5.4");
public static ISemanticVersion MaximumGameVersion { get; } = null;
/// <summary>The target game platform.</summary>
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
@ -240,18 +235,13 @@ namespace StardewModdingAPI
// The game assembly can have one of three names depending how the mod was compiled:
// - 'StardewValley': assembly name on Linux/macOS;
// - 'Stardew Valley': assembly name on Windows;
// - 'Netcode': an assembly that's separate on Windows only.
resolver.AddWithExplicitNames(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley"
#if !SMAPI_FOR_WINDOWS
, "Netcode"
#endif
);
// - 'Netcode': an assembly that was separate on Windows only before Stardew Valley 1.5.5.
resolver.AddWithExplicitNames(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley", "Netcode");
}
/// <summary>Get metadata for mapping assemblies to the current platform.</summary>
/// <param name="targetPlatform">The target game platform.</param>
/// <param name="framework">The game framework running the game.</param>
internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform, GameFramework framework)
internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform)
{
var removeAssemblyReferences = new List<string>();
var targetAssemblies = new List<Assembly>();
@ -260,61 +250,26 @@ namespace StardewModdingAPI
removeAssemblyReferences.Add("StardewModdingAPI.Toolkit.CoreInterfaces");
targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly);
// get changes for platform
if (Constants.Platform != Platform.Windows)
// XNA Framework before Stardew Valley 1.5.5
removeAssemblyReferences.AddRange(new[]
{
removeAssemblyReferences.AddRange(new[]
{
"Netcode",
"Stardew Valley"
});
targetAssemblies.Add(
typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/macOS
);
}
else
{
removeAssemblyReferences.Add(
"StardewValley"
);
targetAssemblies.AddRange(new[]
{
typeof(Netcode.NetBool).Assembly,
typeof(StardewValley.Game1).Assembly
});
}
"Microsoft.Xna.Framework",
"Microsoft.Xna.Framework.Game",
"Microsoft.Xna.Framework.Graphics",
"Microsoft.Xna.Framework.Xact"
});
targetAssemblies.Add(
typeof(Microsoft.Xna.Framework.Vector2).Assembly
);
// get changes for game framework
switch (framework)
{
case GameFramework.MonoGame:
removeAssemblyReferences.AddRange(new[]
{
"Microsoft.Xna.Framework",
"Microsoft.Xna.Framework.Game",
"Microsoft.Xna.Framework.Graphics",
"Microsoft.Xna.Framework.Xact"
});
targetAssemblies.Add(
typeof(Microsoft.Xna.Framework.Vector2).Assembly
);
break;
// `Netcode.dll` merged into the game assembly in Stardew Valley 1.5.5
removeAssemblyReferences.Add(
"Netcode"
);
case GameFramework.Xna:
removeAssemblyReferences.Add(
"MonoGame.Framework"
);
targetAssemblies.AddRange(new[]
{
typeof(Microsoft.Xna.Framework.Vector2).Assembly,
typeof(Microsoft.Xna.Framework.Game).Assembly,
typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly
});
break;
default:
throw new InvalidOperationException($"Unknown game framework '{framework}'.");
}
// Stardew Valley reference
removeAssemblyReferences.Add("StardewValley");
targetAssemblies.Add(typeof(StardewValley.Game1).Assembly);
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray());
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using xTile;
using xTile.Layers;
using xTile.Tiles;
@ -25,18 +26,17 @@ namespace StardewModdingAPI.Framework.Content
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
/// <inheritdoc />
/// <remarks>Derived from <see cref="StardewValley.GameLocation.ApplyMapOverride"/> with a few changes:
/// <remarks>Derived from <see cref="GameLocation.ApplyMapOverride(Map,string,Rectangle?,Rectangle?)"/> with a few changes:
/// - can be applied directly to the maps when loading, before the location is created;
/// - added support for source/target areas;
/// - added support for patch modes (overlay, replace by layer, or fully replace);
/// - added disambiguation if source has a modified version of the same tilesheet, instead of copying tiles into the target tilesheet;
/// - changed to always overwrite tiles within the target area (to avoid edge cases where some tiles are only partly applied);
/// - fixed copying tilesheets (avoid "The specified TileSheet was not created for use with this map" error);
/// - fixed tilesheets not added at the end (via z_ prefix), which can cause crashes in game code which depends on hardcoded tilesheet indexes;
/// - fixed issue where different tilesheets are linked by ID.
/// </remarks>
public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null)
public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay)
{
var target = this.Data;
Map target = this.Data;
// get areas
{
@ -84,10 +84,13 @@ namespace StardewModdingAPI.Framework.Content
tilesheetMap[sourceSheet] = targetSheet;
}
// get layer map
IDictionary<Layer, Layer> layerMap = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id));
// get target layers
IDictionary<Layer, Layer> sourceToTargetLayers = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id));
HashSet<Layer> orphanedTargetLayers = new HashSet<Layer>(target.Layers.Except(sourceToTargetLayers.Values));
// apply tiles
bool replaceAll = patchMode == PatchMapMode.Replace;
bool replaceByLayer = patchMode == PatchMapMode.ReplaceByLayer;
for (int x = 0; x < sourceArea.Value.Width; x++)
{
for (int y = 0; y < sourceArea.Value.Height; y++)
@ -96,47 +99,37 @@ namespace StardewModdingAPI.Framework.Content
Point sourcePos = new Point(sourceArea.Value.X + x, sourceArea.Value.Y + y);
Point targetPos = new Point(targetArea.Value.X + x, targetArea.Value.Y + y);
// replace tiles on target-only layers
if (replaceAll)
{
foreach (Layer targetLayer in orphanedTargetLayers)
targetLayer.Tiles[targetPos.X, targetPos.Y] = null;
}
// merge layers
foreach (Layer sourceLayer in source.Layers)
{
// get layer
Layer targetLayer = layerMap[sourceLayer];
Layer targetLayer = sourceToTargetLayers[sourceLayer];
if (targetLayer == null)
{
target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize));
layerMap[sourceLayer] = target.GetLayer(sourceLayer.Id);
sourceToTargetLayers[sourceLayer] = target.GetLayer(sourceLayer.Id);
}
// copy layer properties
targetLayer.Properties.CopyFrom(sourceLayer.Properties);
// copy tiles
// create new tile
Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y];
Tile targetTile;
switch (sourceTile)
{
case StaticTile _:
targetTile = new StaticTile(targetLayer, tilesheetMap[sourceTile.TileSheet], sourceTile.BlendMode, sourceTile.TileIndex);
break;
Tile newTile = sourceTile != null
? this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet])
: null;
newTile?.Properties.CopyFrom(sourceTile.Properties);
case AnimatedTile animatedTile:
{
StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length];
for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame)
{
StaticTile frameTile = animatedTile.TileFrames[frame];
tileFrames[frame] = new StaticTile(targetLayer, tilesheetMap[frameTile.TileSheet], frameTile.BlendMode, frameTile.TileIndex);
}
targetTile = new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval);
}
break;
default: // null or unhandled type
targetTile = null;
break;
}
targetTile?.Properties.CopyFrom(sourceTile.Properties);
targetLayer.Tiles[targetPos.X, targetPos.Y] = targetTile;
// replace tile
if (newTile != null || replaceByLayer || replaceAll)
targetLayer.Tiles[targetPos.X, targetPos.Y] = newTile;
}
}
}
@ -146,6 +139,33 @@ namespace StardewModdingAPI.Framework.Content
/*********
** Private methods
*********/
/// <summary>Create a new tile for the target map.</summary>
/// <param name="sourceTile">The source tile to copy.</param>
/// <param name="targetLayer">The target layer.</param>
/// <param name="targetSheet">The target tilesheet.</param>
private Tile CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet)
{
switch (sourceTile)
{
case StaticTile _:
return new StaticTile(targetLayer, targetSheet, sourceTile.BlendMode, sourceTile.TileIndex);
case AnimatedTile animatedTile:
{
StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length];
for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame)
{
StaticTile frameTile = animatedTile.TileFrames[frame];
tileFrames[frame] = new StaticTile(targetLayer, targetSheet, frameTile.BlendMode, frameTile.TileIndex);
}
return new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval);
}
default: // null or unhandled type
return null;
}
}
/// <summary>Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path.</summary>
/// <param name="path">The path to normalize.</param>
private string NormalizeTilesheetPathForComparison(string path)

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@ -18,9 +17,6 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>The underlying asset cache.</summary>
private readonly IDictionary<string, object> Cache;
/// <summary>Applies platform-specific asset key normalization so it's consistent with the underlying cache.</summary>
private readonly Func<string, string> NormalizeAssetNameForPlatform;
/*********
** Accessors
@ -48,17 +44,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="reflection">Simplifies access to private game code.</param>
public ContentCache(LocalizedContentManager contentManager, Reflector reflection)
{
// init
this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
// get key normalization logic
if (Constants.GameFramework == GameFramework.Xna)
{
IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath");
this.NormalizeAssetNameForPlatform = path => method.Invoke<string>(path);
}
else
this.NormalizeAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic
}
/****
@ -75,23 +61,24 @@ namespace StardewModdingAPI.Framework.Content
/****
** Normalize
****/
/// <summary>Normalize path separators in a file path. For asset keys, see <see cref="NormalizeKey"/> instead.</summary>
/// <summary>Normalize path separators in an asset name.</summary>
/// <param name="path">The file path to normalize.</param>
[Pure]
public string NormalizePathSeparators(string path)
{
return PathUtilities.NormalizePath(path);
return PathUtilities.NormalizeAssetName(path);
}
/// <summary>Normalize a cache key so it's consistent with the underlying cache.</summary>
/// <param name="key">The asset key.</param>
/// <remarks>This is equivalent to <see cref="NormalizePathSeparators"/> with added file extension logic.</remarks>
[Pure]
public string NormalizeKey(string key)
{
key = this.NormalizePathSeparators(key);
return key.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)
? key.Substring(0, key.Length - 4)
: this.NormalizeAssetNameForPlatform(key);
: key;
}
/****

View File

@ -64,6 +64,9 @@ namespace StardewModdingAPI.Framework
/// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary>
private readonly LocalizedContentManager VanillaContentManager;
/// <summary>The language enum values indexed by locale code.</summary>
private Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
/*********
** Accessors
@ -133,6 +136,7 @@ namespace StardewModdingAPI.Framework
this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, aggressiveMemoryOptimizations);
this.LocaleCodes = new Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>>(this.GetLocaleCodes);
}
/// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
@ -195,6 +199,10 @@ namespace StardewModdingAPI.Framework
/// <summary>Perform any cleanup needed when the locale changes.</summary>
public void OnLocaleChanged()
{
// rebuild locale cache (which may change due to custom mod languages)
this.LocaleCodes = new Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>>(this.GetLocaleCodes);
// reload affected content
this.ContentManagerLock.InReadLock(() =>
{
foreach (IContentManager contentManager in this.ContentManagers)
@ -408,6 +416,25 @@ namespace StardewModdingAPI.Framework
return tilesheets ?? new TilesheetReference[0];
}
/// <summary>Get the language enum which corresponds to a locale code (e.g. <see cref="LocalizedContentManager.LanguageCode.fr"/> given <c>fr-FR</c>).</summary>
/// <param name="locale">The locale code to search. This must exactly match the language; no fallback is performed.</param>
/// <param name="language">The matched language enum, if any.</param>
/// <returns>Returns whether a valid language was found.</returns>
public bool TryGetLanguageEnum(string locale, out LocalizedContentManager.LanguageCode language)
{
return this.LocaleCodes.Value.TryGetValue(locale, out language);
}
/// <summary>Get the locale code which corresponds to a language enum (e.g. <c>fr-FR</c> given <see cref="LocalizedContentManager.LanguageCode.fr"/>).</summary>
/// <param name="language">The language enum to search.</param>
public string GetLocaleCode(LocalizedContentManager.LanguageCode language)
{
if (language == LocalizedContentManager.LanguageCode.mod && LocalizedContentManager.CurrentModLanguage == null)
return null;
return Game1.content.LanguageCodeString(language);
}
/// <summary>Dispose held resources.</summary>
public void Dispose()
{
@ -457,5 +484,19 @@ namespace StardewModdingAPI.Framework
return false;
}
}
/// <summary>Get the language enums (like <see cref="LocalizedContentManager.LanguageCode.ja"/>) indexed by locale code (like <c>ja-JP</c>).</summary>
private IDictionary<string, LocalizedContentManager.LanguageCode> GetLocaleCodes()
{
IDictionary<string, LocalizedContentManager.LanguageCode> map = new Dictionary<string, LocalizedContentManager.LanguageCode>();
foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode)))
{
string locale = this.GetLocaleCode(code);
if (locale != null)
map[locale] = code;
}
return map;
}
}
}

View File

@ -38,9 +38,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>A callback to invoke when the content manager is being disposed.</summary>
private readonly Action<BaseContentManager> OnDisposing;
/// <summary>The language enum values indexed by locale code.</summary>
protected IDictionary<string, LanguageCode> LanguageCodes { get; }
/// <summary>A list of disposable assets.</summary>
private readonly List<WeakReference<IDisposable>> Disposables = new List<WeakReference<IDisposable>>();
@ -92,7 +89,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
// get asset data
this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.OrdinalIgnoreCase);
this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue();
}
@ -292,7 +288,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (lastSepIndex >= 0)
{
string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
if (this.LanguageCodes.ContainsKey(suffix))
if (this.Coordinator.TryGetLanguageEnum(suffix, out _))
{
assetName = cacheKey.Substring(0, lastSepIndex);
localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
@ -311,17 +307,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="language">The language to check.</param>
protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language);
/// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
private IDictionary<LanguageCode, string> GetKeyLocales()
{
// create locale => code map
IDictionary<LanguageCode, string> map = new Dictionary<LanguageCode, string>();
foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode)))
map[code] = this.GetLocale(code);
return map;
}
/// <summary>Get the asset name from a cache key.</summary>
/// <param name="cacheKey">The input cache key.</param>
private string GetAssetName(string cacheKey)

View File

@ -249,7 +249,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// extract language code
int splitIndex = rawAsset.LastIndexOf('.');
if (splitIndex != -1 && this.LanguageCodes.TryGetValue(rawAsset.Substring(splitIndex + 1), out language))
if (splitIndex != -1 && this.Coordinator.TryGetLanguageEnum(rawAsset.Substring(splitIndex + 1), out language))
{
assetName = rawAsset.Substring(0, splitIndex);
return true;

View File

@ -2,12 +2,12 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
using BmFont;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@ -130,6 +130,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
break;
// unpacked Bitmap font
case ".fnt":
{
string source = File.ReadAllText(file.FullName);
asset = (T)(object)new XmlSource(source);
}
break;
// unpacked data
case ".json":
{
@ -172,13 +180,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
break;
default:
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'.");
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', or '.xnb'.");
}
}
catch (Exception ex) when (!(ex is SContentLoadException))
{
if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib")
throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher.");
throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex);
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
@ -157,11 +158,8 @@ namespace StardewModdingAPI.Framework.Input
yield break;
// buttons
foreach (var pair in this.ButtonStates)
{
if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button))
yield return button.ToSButton();
}
foreach (Buttons button in this.GetPressedGamePadButtons())
yield return button.ToSButton();
// triggers
if (this.LeftTrigger > 0.2f)
@ -201,7 +199,7 @@ namespace StardewModdingAPI.Framework.Input
rightThumbStick: this.RightStickPos,
leftTrigger: this.LeftTrigger,
rightTrigger: this.RightTrigger,
buttons: this.GetButtonBitmask() // MonoGame requires one bitmask here; don't specify multiple values
buttons: this.GetPressedGamePadButtons().ToArray()
);
return this.State.Value;
@ -211,17 +209,14 @@ namespace StardewModdingAPI.Framework.Input
/*********
** Private methods
*********/
/// <summary>Get a bitmask representing the pressed buttons.</summary>
private Buttons GetButtonBitmask()
/// <summary>Get the pressed gamepad buttons.</summary>
private IEnumerable<Buttons> GetPressedGamePadButtons()
{
Buttons flag = 0;
foreach (var pair in this.ButtonStates)
{
if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button))
flag |= button;
yield return button;
}
return flag;
}
}
}

View File

@ -6,7 +6,6 @@ using System.Threading;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
using StardewValley.Menus;
namespace StardewModdingAPI.Framework
@ -150,11 +149,7 @@ namespace StardewModdingAPI.Framework
/// <param name="reflection">The reflection helper with which to access private fields.</param>
public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection)
{
string fieldName = Constants.GameFramework == GameFramework.Xna
? "inBeginEndPair"
: "_beginCalled";
return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue();
return reflection.GetField<bool>(spriteBatch, "_beginCalled").GetValue();
}
}
}

View File

@ -250,36 +250,7 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="exception">The exception details.</param>
public void LogFatalLaunchError(Exception exception)
{
switch (exception)
{
// audio crash
case InvalidOperationException ex when ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor"):
this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error);
this.Monitor.Log($"Technical details: {ex.GetLogSummary()}");
break;
// missing content folder exception
case FileNotFoundException ex when ex.Message == "Couldn't find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.": // path in error is hardcoded regardless of install path
this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error);
this.Monitor.Log($"Technical details: {ex.GetLogSummary()}");
break;
// path too long exception
case PathTooLongException _:
{
string[] affectedPaths = PathUtilities.GetTooLongPaths(Constants.ModsPath).ToArray();
string message = affectedPaths.Any()
? $"SMAPI can't launch because some of your mod files exceed the maximum path length on {Constants.Platform}.\nIf you need help fixing this error, see https://smapi.io/help\n\nAffected paths:\n {string.Join("\n ", affectedPaths)}"
: $"The game failed to launch: {exception.GetLogSummary()}";
this.MonitorForGame.Log(message, LogLevel.Error);
}
break;
// generic exception
default:
this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error);
break;
}
this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error);
}
/****
@ -290,7 +261,7 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="customSettings">The custom SMAPI settings.</param>
public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
{
// log platform & patches
// log platform
this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
// log basic info

View File

@ -53,16 +53,15 @@ namespace StardewModdingAPI.Framework.ModLoading
*********/
/// <summary>Construct an instance.</summary>
/// <param name="targetPlatform">The current game platform.</param>
/// <param name="framework">The game framework running the game.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
/// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param>
public AssemblyLoader(Platform targetPlatform, GameFramework framework, IMonitor monitor, bool paranoidMode, bool rewriteMods)
public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods)
{
this.Monitor = monitor;
this.ParanoidMode = paranoidMode;
this.RewriteMods = rewriteMods;
this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform, framework));
this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform));
// init resolver
this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver());

View File

@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics;
namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
{
/// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/macOS or Windows.</summary>
/// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility with mods written for XNA Framework before Stardew Valley 1.5.5.</summary>
/// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")]
[SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/macOS.")]
@ -18,14 +18,6 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
public SpriteBatchFacade(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
/****
** MonoGame signatures
****/
public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix)
{
base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity);
}
/****
** XNA signatures
****/

View File

@ -24,7 +24,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>Construct an instance.</summary>
public InterfaceProxyFactory()
{
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
}

View File

@ -11,14 +11,10 @@ using System.Runtime.ExceptionServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
#if SMAPI_FOR_WINDOWS
using Microsoft.Win32;
#endif
#if SMAPI_FOR_XNA
using System.Windows.Forms;
#endif
using Newtonsoft.Json;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
@ -224,10 +220,6 @@ namespace StardewModdingAPI.Framework
this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter);
// add error handlers
#if SMAPI_FOR_XNA
Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
#endif
AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error);
// add more lenient assembly resolver
@ -243,7 +235,7 @@ namespace StardewModdingAPI.Framework
monitor: this.Monitor,
reflection: this.Reflection,
eventManager: this.EventManager,
modHooks: new SModHooks(this.OnNewDayAfterFade),
modHooks: new SModHooks(this.OnNewDayAfterFade, this.Monitor),
multiplayer: this.Multiplayer,
exitGameImmediately: this.ExitGameImmediately,
@ -657,13 +649,6 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Game loader done.");
}
if (instance.NewDayTask?.Status == TaskStatus.Created)
{
this.Monitor.Log("New day task synchronizing...");
instance.NewDayTask.RunSynchronously();
this.Monitor.Log("New day task done.");
}
// While a background task is in progress, the game may make changes to the game
// state while mods are running their code. This is risky, because data changes can
// conflict (e.g. collection changed during enumeration errors) and data may change
@ -673,7 +658,7 @@ namespace StardewModdingAPI.Framework
// a small chance that the task will finish after we defer but before the game checks,
// which means technically events should be raised, but the effects of missing one
// update tick are negligible and not worth the complications of bypassing Game1.Update.
if (instance.NewDayTask != null || Game1.gameMode == Game1.loadingMode)
if (Game1.gameMode == Game1.loadingMode)
{
events.UnvalidatedUpdateTicking.RaiseEmpty();
runUpdate();
@ -766,7 +751,7 @@ namespace StardewModdingAPI.Framework
** Locale changed events
*********/
if (state.Locale.IsChanged)
this.Monitor.Log($"Context: locale set to {state.Locale.New}.");
this.Monitor.Log($"Context: locale set to {state.Locale.New} ({this.ContentCore.GetLocaleCode(state.Locale.New)}).");
/*********
** Load / return-to-title events
@ -776,7 +761,7 @@ namespace StardewModdingAPI.Framework
else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready)
{
// print context
string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}.";
string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.GetLocale()}.";
if (Context.IsMultiplayer)
{
int onlineCount = Game1.getOnlineFarmers().Count();
@ -1304,9 +1289,6 @@ namespace StardewModdingAPI.Framework
{
// create client
string url = this.Settings.WebApiBaseUrl;
#if !SMAPI_FOR_WINDOWS
url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/macOS
#endif
WebApiClient client = new WebApiClient(url, Constants.ApiVersion);
this.Monitor.Log("Checking for updates...");
@ -1491,7 +1473,7 @@ namespace StardewModdingAPI.Framework
// load mods
IList<IModMetadata> skippedMods = new List<IModMetadata>();
using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, Constants.GameFramework, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
{
// init
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
@ -1701,9 +1683,8 @@ namespace StardewModdingAPI.Framework
catch (Exception ex)
{
errorReasonPhrase = "its DLL couldn't be loaded.";
// re-enable in Stardew Valley 1.5.5
//if (ex is BadImageFormatException && !EnvironmentUtility.Is64BitAssembly(assemblyPath))
// errorReasonPhrase = "it needs to be updated for 64-bit mode.";
if (ex is BadImageFormatException && !EnvironmentUtility.Is64BitAssembly(assemblyPath))
errorReasonPhrase = "it needs to be updated for 64-bit mode.";
errorDetails = $"Error: {ex.GetLogSummary()}";
failReason = ModFailReason.LoadFailed;

View File

@ -252,7 +252,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Replicate the game's draw logic with some changes for SMAPI.</summary>
/// <param name="gameTime">A snapshot of the game timing state.</param>
/// <param name="target_screen">The render target, if any.</param>
/// <remarks>This implementation is identical to <see cref="Game1.Draw"/>, except for try..catch around menu draw code, private field references replaced by wrappers, and added events.</remarks>
/// <remarks>This implementation is identical to <see cref="Game1._draw"/>, except for try..catch around menu draw code, private field references replaced by wrappers, and added events.</remarks>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")]
@ -286,7 +286,7 @@ namespace StardewModdingAPI.Framework
IClickableMenu menu = Game1.activeClickableMenu;
if (menu != null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
events.Rendering.RaiseEmpty();
try
{
@ -304,7 +304,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.overlayMenu != null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
@ -315,7 +315,7 @@ namespace StardewModdingAPI.Framework
if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot)
{
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
events.Rendering.RaiseEmpty();
IClickableMenu curMenu = null;
@ -346,11 +346,11 @@ namespace StardewModdingAPI.Framework
}
if (Game1.gameMode == 11)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
events.Rendering.RaiseEmpty();
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Microsoft.Xna.Framework.Color.HotPink);
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Microsoft.Xna.Framework.Color(0, 255, 0));
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Microsoft.Xna.Framework.Color.White);
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink);
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, 255, 0));
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White);
events.Rendered.RaiseEmpty();
Game1.spriteBatch.End();
return;
@ -368,8 +368,8 @@ namespace StardewModdingAPI.Framework
if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause))
{
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha));
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha));
Game1.spriteBatch.End();
Game1.PopUIMode();
}
@ -388,7 +388,7 @@ namespace StardewModdingAPI.Framework
if (Game1.showingEndOfNightStuff)
{
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
events.Rendering.RaiseEmpty();
if (Game1.activeClickableMenu != null)
{
@ -417,16 +417,16 @@ namespace StardewModdingAPI.Framework
{
Game1.PushUIMode();
base.GraphicsDevice.Clear(Game1.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
events.Rendering.RaiseEmpty();
string addOn = "";
for (int i = 0; (double)i < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; i++)
{
addOn += ".";
}
string str = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688");
string msg = str + addOn;
string largestMessage = str + "... ";
string text = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688");
string msg = text + addOn;
string largestMessage = text + "... ";
int msgw = SpriteText.getWidthOfString(largestMessage);
int msgh = 64;
int msgx = 64;
@ -442,7 +442,7 @@ namespace StardewModdingAPI.Framework
byte batchOpens = 0; // used for rendering event
if (Game1.gameMode == 0)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
if (++batchOpens == 1)
events.Rendering.RaiseEmpty();
}
@ -456,7 +456,7 @@ namespace StardewModdingAPI.Framework
if (Game1.drawLighting)
{
Game1.SetRenderTarget(Game1.lightmap);
base.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.White * 0f);
base.GraphicsDevice.Clear(Color.White * 0f);
Matrix lighting_matrix = Matrix.Identity;
if (this.useUnscaledLighting)
{
@ -465,13 +465,13 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, lighting_matrix);
if (++batchOpens == 1)
events.Rendering.RaiseEmpty();
Microsoft.Xna.Framework.Color lighting = (Game1.currentLocation.Name.StartsWith("UndergroundMine") && Game1.currentLocation is MineShaft) ? (Game1.currentLocation as MineShaft).getLightingColor(gameTime) : ((Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors)) ? Game1.outdoorLight : Game1.ambientLight);
Color lighting = ((Game1.currentLocation.Name.StartsWith("UndergroundMine") && Game1.currentLocation is MineShaft) ? (Game1.currentLocation as MineShaft).getLightingColor(gameTime) : ((Game1.ambientLight.Equals(Color.White) || (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors)) ? Game1.outdoorLight : Game1.ambientLight));
float light_multiplier = 1f;
if (Game1.player.hasBuff(26))
{
if (lighting == Microsoft.Xna.Framework.Color.White)
if (lighting == Color.White)
{
lighting = new Microsoft.Xna.Framework.Color(0.75f, 0.75f, 0.75f);
lighting = new Color(0.75f, 0.75f, 0.75f);
}
else
{
@ -504,12 +504,8 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.End();
Game1.SetRenderTarget(target_screen);
}
if (Game1.bloomDay && Game1.bloom != null)
{
Game1.bloom.BeginDraw();
}
base.GraphicsDevice.Clear(Game1.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
if (++batchOpens == 1)
events.Rendering.RaiseEmpty();
events.RenderingWorld.RaiseEmpty();
@ -522,10 +518,10 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4);
Game1.currentLocation.drawWater(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp);
Game1.currentLocation.drawFloorDecorations(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
this._farmerShadows.Clear();
if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0)
{
@ -555,17 +551,17 @@ namespace StardewModdingAPI.Framework
{
if (!k.swimming && !k.HideShadow && !k.IsInvisible && !this.checkCharacterTilesForShadowDrawFlag(k))
{
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, k.GetShadowOffset() + k.Position + new Vector2((float)(k.GetSpriteWidthForPositioning() * 4) / 2f, k.GetBoundingBox().Height + ((!k.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)k.yJumpOffset / 40f) * (float)k.scale), SpriteEffects.None, Math.Max(0f, (float)k.getStandingY() / 10000f) - 1E-06f);
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, k.GetShadowOffset() + k.Position + new Vector2((float)(k.GetSpriteWidthForPositioning() * 4) / 2f, k.GetBoundingBox().Height + ((!k.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)k.yJumpOffset / 40f) * (float)k.scale), SpriteEffects.None, Math.Max(0f, (float)k.getStandingY() / 10000f) - 1E-06f);
}
}
}
else
{
foreach (NPC m in Game1.CurrentEvent.actors)
foreach (NPC l in Game1.CurrentEvent.actors)
{
if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(m)) && !m.swimming && !m.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(m))
if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(l)) && !l.swimming && !l.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(l))
{
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, m.GetShadowOffset() + m.Position + new Vector2((float)(m.GetSpriteWidthForPositioning() * 4) / 2f, m.GetBoundingBox().Height + ((!m.IsMonster) ? ((m.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, 4f + (float)m.yJumpOffset / 40f) * (float)m.scale, SpriteEffects.None, Math.Max(0f, (float)m.getStandingY() / 10000f) - 1E-06f);
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, l.GetShadowOffset() + l.Position + new Vector2((float)(l.GetSpriteWidthForPositioning() * 4) / 2f, l.GetBoundingBox().Height + ((!l.IsMonster) ? ((l.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, 4f + (float)l.yJumpOffset / 40f) * (float)l.scale, SpriteEffects.None, Math.Max(0f, (float)l.getStandingY() / 10000f) - 1E-06f);
}
}
}
@ -573,7 +569,7 @@ namespace StardewModdingAPI.Framework
{
if (!Game1.multiplayer.isDisconnecting(f3.UniqueMultiplayerID) && !f3.swimming && !f3.isRidingHorse() && !f3.IsSitting() && (Game1.currentLocation == null || !this.checkCharacterTilesForShadowDrawFlag(f3)))
{
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f3.GetShadowOffset() + f3.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f3.running || f3.UsingTool) && f3.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f3.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f);
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f3.GetShadowOffset() + f3.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f3.running || f3.UsingTool) && f3.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f3.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f);
}
}
}
@ -581,26 +577,26 @@ namespace StardewModdingAPI.Framework
building_layer.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4);
Game1.mapDisplayDevice.EndScene();
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp);
if (!Game1.currentLocation.shouldHideCharacters())
{
if (Game1.CurrentEvent == null)
{
foreach (NPC n in Game1.currentLocation.characters)
foreach (NPC m in Game1.currentLocation.characters)
{
if (!n.swimming && !n.HideShadow && !n.isInvisible && this.checkCharacterTilesForShadowDrawFlag(n))
if (!m.swimming && !m.HideShadow && !m.isInvisible && this.checkCharacterTilesForShadowDrawFlag(m))
{
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n.GetShadowOffset() + n.Position + new Vector2((float)(n.GetSpriteWidthForPositioning() * 4) / 2f, n.GetBoundingBox().Height + ((!n.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n.yJumpOffset / 40f) * (float)n.scale), SpriteEffects.None, Math.Max(0f, (float)n.getStandingY() / 10000f) - 1E-06f);
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, m.GetShadowOffset() + m.Position + new Vector2((float)(m.GetSpriteWidthForPositioning() * 4) / 2f, m.GetBoundingBox().Height + ((!m.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)m.yJumpOffset / 40f) * (float)m.scale), SpriteEffects.None, Math.Max(0f, (float)m.getStandingY() / 10000f) - 1E-06f);
}
}
}
else
{
foreach (NPC n2 in Game1.CurrentEvent.actors)
foreach (NPC n in Game1.CurrentEvent.actors)
{
if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n2)) && !n2.swimming && !n2.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n2))
if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n)) && !n.swimming && !n.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n))
{
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n2.GetShadowOffset() + n2.Position + new Vector2((float)(n2.GetSpriteWidthForPositioning() * 4) / 2f, n2.GetBoundingBox().Height + ((!n2.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n2.yJumpOffset / 40f) * (float)n2.scale), SpriteEffects.None, Math.Max(0f, (float)n2.getStandingY() / 10000f) - 1E-06f);
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n.GetShadowOffset() + n.Position + new Vector2((float)(n.GetSpriteWidthForPositioning() * 4) / 2f, n.GetBoundingBox().Height + ((!n.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n.yJumpOffset / 40f) * (float)n.scale), SpriteEffects.None, Math.Max(0f, (float)n.getStandingY() / 10000f) - 1E-06f);
}
}
}
@ -609,7 +605,7 @@ namespace StardewModdingAPI.Framework
float draw_layer = Math.Max(0.0001f, f4.getDrawLayer() + 0.00011f) - 0.0001f;
if (!f4.swimming && !f4.isRidingHorse() && !f4.IsSitting() && Game1.currentLocation != null && this.checkCharacterTilesForShadowDrawFlag(f4))
{
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f4.GetShadowOffset() + f4.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f4.running || f4.UsingTool) && f4.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f4.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, draw_layer);
Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f4.GetShadowOffset() + f4.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f4.running || f4.UsingTool) && f4.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f4.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, draw_layer);
}
}
}
@ -619,7 +615,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm"))
{
Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f);
Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f);
}
Game1.currentLocation.draw(Game1.spriteBatch);
foreach (Vector2 tile_position in Game1.crabPotOverlayTiles.Keys)
@ -646,14 +642,14 @@ namespace StardewModdingAPI.Framework
}
if (Game1.tvStation >= 0)
{
Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f);
Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f);
}
if (Game1.panMode)
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Lime * 0.75f);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f);
foreach (Warp w in Game1.currentLocation.warps)
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Red * 0.75f);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f);
}
}
Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
@ -661,7 +657,7 @@ namespace StardewModdingAPI.Framework
Game1.mapDisplayDevice.EndScene();
Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null)
{
Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
@ -670,7 +666,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.toolHold > 400f && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool)
{
Microsoft.Xna.Framework.Color barColor = Microsoft.Xna.Framework.Color.White;
Color barColor = Color.White;
switch ((int)(Game1.toolHold / 600f) + 2)
{
case 1:
@ -686,7 +682,7 @@ namespace StardewModdingAPI.Framework
barColor = Tool.iridiumColor;
break;
}
Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Microsoft.Xna.Framework.Color.Black);
Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Color.Black);
Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0), (int)(Game1.toolHold % 600f * 0.08f), 8), barColor);
}
if (!Game1.IsFakedBlackScreen())
@ -699,7 +695,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000)
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel);
}
if (Game1.screenGlow)
{
@ -711,51 +707,51 @@ namespace StardewModdingAPI.Framework
Game1.player.CurrentTool.draw(Game1.spriteBatch);
}
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp);
if (Game1.eventUp && Game1.currentLocation.currentEvent != null)
{
foreach (NPC l in Game1.currentLocation.currentEvent.actors)
foreach (NPC n2 in Game1.currentLocation.currentEvent.actors)
{
if (l.isEmoting)
if (n2.isEmoting)
{
Vector2 emotePosition = l.getLocalPosition(Game1.viewport);
if (l.NeedsBirdieEmoteHack())
Vector2 emotePosition = n2.getLocalPosition(Game1.viewport);
if (n2.NeedsBirdieEmoteHack())
{
emotePosition.X += 64f;
}
emotePosition.Y -= 140f;
if (l.Age == 2)
if (n2.Age == 2)
{
emotePosition.Y += 32f;
}
else if (l.Gender == 1)
else if (n2.Gender == 1)
{
emotePosition.Y += 10f;
}
Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(l.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, l.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)l.getStandingY() / 10000f);
Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(n2.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, n2.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)n2.getStandingY() / 10000f);
}
}
}
Game1.spriteBatch.End();
if (Game1.drawLighting && !Game1.IsFakedBlackScreen())
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp);
Viewport vp = base.GraphicsDevice.Viewport;
vp.Bounds = (target_screen?.Bounds ?? base.GraphicsDevice.PresentationParameters.Bounds);
vp.Bounds = target_screen?.Bounds ?? base.GraphicsDevice.PresentationParameters.Bounds;
base.GraphicsDevice.Viewport = vp;
float render_zoom = Game1.options.lightingQuality / 2;
if (this.useUnscaledLighting)
{
render_zoom /= Game1.options.zoomLevel;
}
Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, render_zoom, SpriteEffects.None, 1f);
Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Color.White, 0f, Vector2.Zero, render_zoom, SpriteEffects.None, 1f);
if (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))
{
Game1.spriteBatch.Draw(Game1.staminaRect, vp.Bounds, Microsoft.Xna.Framework.Color.OrangeRed * 0.45f);
Game1.spriteBatch.Draw(Game1.staminaRect, vp.Bounds, Color.OrangeRed * 0.45f);
}
Game1.spriteBatch.End();
}
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
events.RenderedWorld.RaiseEmpty();
if (Game1.drawGrid)
{
@ -763,11 +759,11 @@ namespace StardewModdingAPI.Framework
float startingY = -Game1.viewport.Y % 64;
for (int x = startingX; x < Game1.graphics.GraphicsDevice.Viewport.Width; x += 64)
{
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x, (int)startingY, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Microsoft.Xna.Framework.Color.Red * 0.5f);
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x, (int)startingY, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f);
}
for (float y = startingY; y < (float)Game1.graphics.GraphicsDevice.Viewport.Height; y += 64f)
{
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(startingX, (int)y, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Microsoft.Xna.Framework.Color.Red * 0.5f);
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(startingX, (int)y, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f);
}
}
if (Game1.ShouldShowOnscreenUsernames() && Game1.currentLocation != null)
@ -780,14 +776,14 @@ namespace StardewModdingAPI.Framework
}
if (!Game1.eventUp && Game1.farmEvent == null && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !this.takingMapScreenshot && Game1.isOutdoorMapSmallerThanViewport())
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -Math.Min(Game1.viewport.X, GameRunner.MaxTextureSize), Game1.graphics.GraphicsDevice.Viewport.Height), Microsoft.Xna.Framework.Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64, 0, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)), Game1.graphics.GraphicsDevice.Viewport.Height), Microsoft.Xna.Framework.Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, Game1.graphics.GraphicsDevice.Viewport.Width, -Math.Min(Game1.viewport.Y, GameRunner.MaxTextureSize)), Microsoft.Xna.Framework.Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, -Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64, Game1.graphics.GraphicsDevice.Viewport.Width, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Height - (-Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64))), Microsoft.Xna.Framework.Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -Math.Min(Game1.viewport.X, GameRunner.MaxTextureSize), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64, 0, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, Game1.graphics.GraphicsDevice.Viewport.Width, -Math.Min(Game1.viewport.Y, GameRunner.MaxTextureSize)), Color.Black);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, -Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64, Game1.graphics.GraphicsDevice.Viewport.Width, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Height - (-Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64))), Color.Black);
}
Game1.spriteBatch.End();
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
if ((Game1.displayHUD || Game1.eventUp) && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !Game1.freezeControls && !Game1.panMode && !Game1.HostPaused && !this.takingMapScreenshot)
{
events.RenderingHud.RaiseEmpty();
@ -807,13 +803,13 @@ namespace StardewModdingAPI.Framework
}
Game1.spriteBatch.End();
Game1.PopUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
}
if (Game1.farmEvent != null)
{
Game1.farmEvent.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
}
Game1.PushUIMode();
if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)) && !this.takingMapScreenshot)
@ -822,35 +818,35 @@ namespace StardewModdingAPI.Framework
}
if (Game1.progressBar && !this.takingMapScreenshot)
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Microsoft.Xna.Framework.Color.LightGray);
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(Game1.pauseAccumulator / Game1.pauseTime * (float)Game1.dialogueWidth), 32), Microsoft.Xna.Framework.Color.DimGray);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray);
Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(Game1.pauseAccumulator / Game1.pauseTime * (float)Game1.dialogueWidth), 32), Color.DimGray);
}
Game1.spriteBatch.End();
Game1.PopUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)
{
Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch);
}
if (!Game1.IsFakedBlackScreen() && Game1.IsRainingHere() && Game1.currentLocation != null && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))
{
Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Blue * 0.2f);
Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f);
}
if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause) && !this.takingMapScreenshot)
{
Game1.spriteBatch.End();
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha));
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha));
Game1.spriteBatch.End();
Game1.PopUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
}
else if (Game1.flashAlpha > 0f && !this.takingMapScreenshot)
{
if (Game1.options.screenFlash)
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.White * Math.Min(1f, Game1.flashAlpha));
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha));
}
Game1.flashAlpha -= 0.1f;
}
@ -866,14 +862,14 @@ namespace StardewModdingAPI.Framework
}
Game1.spriteBatch.End();
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
foreach (TemporaryAnimatedSprite uiOverlayTempSprite in Game1.uiOverlayTempSprites)
{
uiOverlayTempSprite.draw(Game1.spriteBatch, localPosition: true);
}
Game1.spriteBatch.End();
Game1.PopUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
}
if (Game1.debugMode)
{
@ -905,14 +901,14 @@ namespace StardewModdingAPI.Framework
sb.Append(Game1.getMouseY() + Game1.viewport.Y);
sb.Append(" debugOutput: ");
sb.Append(Game1.debugOutput);
Game1.spriteBatch.DrawString(Game1.smallFont, sb, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8), Microsoft.Xna.Framework.Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
Game1.spriteBatch.DrawString(Game1.smallFont, sb, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8), Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
}
Game1.spriteBatch.End();
Game1.PushUIMode();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
if (Game1.showKeyHelp && !this.takingMapScreenshot)
{
Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? (192 + (Game1.isQuestion ? (Game1.questionChoices.Count * 64) : 0)) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Microsoft.Xna.Framework.Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? (192 + (Game1.isQuestion ? (Game1.questionChoices.Count * 64) : 0)) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
}
if (Game1.activeClickableMenu != null && !this.takingMapScreenshot)
{

View File

@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using StardewValley;
namespace StardewModdingAPI.Framework
@ -12,15 +13,20 @@ namespace StardewModdingAPI.Framework
/// <summary>A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</summary>
private readonly Action BeforeNewDayAfterFade;
/// <summary>Writes messages to the console.</summary>
private readonly IMonitor Monitor;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="beforeNewDayAfterFade">A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</param>
public SModHooks(Action beforeNewDayAfterFade)
/// <param name="monitor">Writes messages to the console.</param>
public SModHooks(Action beforeNewDayAfterFade, IMonitor monitor)
{
this.BeforeNewDayAfterFade = beforeNewDayAfterFade;
this.Monitor = monitor;
}
/// <summary>A hook invoked when <see cref="Game1.newDayAfterFade"/> is called.</summary>
@ -30,5 +36,27 @@ namespace StardewModdingAPI.Framework
this.BeforeNewDayAfterFade?.Invoke();
action();
}
/// <summary>Start an asynchronous task for the game.</summary>
/// <param name="task">The task to start.</param>
/// <param name="id">A unique key which identifies the task.</param>
public override Task StartTask(Task task, string id)
{
this.Monitor.Log($"Synchronizing '{id}' task...");
task.RunSynchronously();
this.Monitor.Log(" task complete.");
return task;
}
/// <summary>Start an asynchronous task for the game.</summary>
/// <param name="task">The task to start.</param>
/// <param name="id">A unique key which identifies the task.</param>
public override Task<T> StartTask<T>(Task<T> task, string id)
{
this.Monitor.Log($"Synchronizing '{id}' task...");
task.RunSynchronously();
this.Monitor.Log(" task complete.");
return task;
}
}
}

View File

@ -1,12 +1,15 @@
using System;
namespace StardewModdingAPI
{
/// <summary>The game framework running the game.</summary>
public enum GameFramework
{
/// <summary>The XNA Framework on Windows.</summary>
/// <summary>The XNA Framework, previously used on Windows.</summary>
[Obsolete("Stardew Valley no longer uses XNA Framework on any supported platform.")]
Xna,
/// <summary>The MonoGame framework, usually on non-Windows platforms.</summary>
/// <summary>The MonoGame framework.</summary>
MonoGame
}
}

View File

@ -13,6 +13,7 @@ namespace StardewModdingAPI
/// <param name="source">The map from which to copy.</param>
/// <param name="sourceArea">The tile area within the source map to copy, or <c>null</c> for the entire source map size. This must be within the bounds of the <paramref name="source"/> map.</param>
/// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param>
void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null);
/// <param name="patchMode">Indicates how the map should be patched.</param>
void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay);
}
}

View File

@ -226,13 +226,8 @@ namespace StardewModdingAPI.Metadata
** Buildings
****/
case "buildings\\houses": // Farm
{
var field = reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures));
field.SetValue(
this.LoadAndDisposeIfNeeded(field.GetValue(), key)
);
return true;
}
Farm.houseTextures = this.LoadAndDisposeIfNeeded(Farm.houseTextures, key);
return true;
case "buildings\\houses_paintmask": // Farm
{
@ -447,10 +442,6 @@ namespace StardewModdingAPI.Metadata
Game1.objectSpriteSheet = content.Load<Texture2D>(key);
return true;
case "maps\\walls_and_floors": // Wallpaper
Wallpaper.wallpaperTexture = content.Load<Texture2D>(key);
return true;
/****
** Content\Minigames
****/

View File

@ -36,9 +36,6 @@ namespace StardewModdingAPI.Metadata
// rewrite for crossplatform compatibility
if (rewriteMods)
{
if (platformChanged)
yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade));
// rewrite for Stardew Valley 1.5
yield return new FieldReplaceRewriter(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture));
yield return new FieldReplaceRewriter(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
@ -48,9 +45,10 @@ namespace StardewModdingAPI.Metadata
yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies);
yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies);
// rewrite for 64-bit mode
// re-enable in Stardew Valley 1.5.5
//yield return new ArchitectureAssemblyRewriter();
// rewrite for Stardew Valley 1.5.5
if (platformChanged)
yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade));
yield return new ArchitectureAssemblyRewriter();
// detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update)
yield return new HarmonyRewriter();

15
src/SMAPI/PatchMapMode.cs Normal file
View File

@ -0,0 +1,15 @@
namespace StardewModdingAPI
{
/// <summary>Indicates how a map should be patched.</summary>
public enum PatchMapMode
{
/// <summary>Replace matching tiles. Target tiles missing in the source area are kept as-is.</summary>
Overlay,
/// <summary>Replace all tiles on layers that exist in the source map.</summary>
ReplaceByLayer,
/// <summary>Replace all tiles with the source map.</summary>
Replace
}
}

View File

@ -4,8 +4,8 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI
{
@ -26,7 +26,7 @@ namespace StardewModdingAPI
/// <param name="args">The command-line arguments.</param>
public static void Main(string[] args)
{
Console.Title = $"SMAPI {EarlyConstants.RawApiVersion} - {Console.Title}";
Console.Title = $"SMAPI {EarlyConstants.RawApiVersion}";
try
{
@ -34,9 +34,10 @@ namespace StardewModdingAPI
Program.AssertGamePresent();
Program.AssertGameVersion();
Program.AssertSmapiVersions();
Program.AssertDepsJson();
Program.Start(args);
}
catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // don't use EarlyConstants.GameAssemblyName, since we want to check both possible names
catch (BadImageFormatException ex) when (ex.FileName == EarlyConstants.GameAssemblyName)
{
Console.WriteLine($"SMAPI failed to initialize because your game's {ex.FileName}.exe seems to be invalid.\nThis may be a pirated version which modified the executable in an incompatible way; if so, you can try a different download or buy a legitimate version.\n\nTechnical details:\n{ex}");
}
@ -84,22 +85,10 @@ namespace StardewModdingAPI
}
catch (Exception ex)
{
// unofficial 64-bit
if (EarlyConstants.Platform == GamePlatform.Windows)
{
FileInfo linuxExecutable = new FileInfo(Path.Combine(EarlyConstants.ExecutionPath, "StardewValley.exe"));
if (linuxExecutable.Exists && LowLevelEnvironmentUtility.Is64BitAssembly(linuxExecutable.FullName))
Program.PrintErrorAndExit("Oops! You're running Stardew Valley in unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info.");
}
// file doesn't exist
if (!File.Exists(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe")))
Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder.");
// Stardew Valley 1.5.5+
if (File.Exists(Path.Combine(EarlyConstants.ExecutionPath, "Stardew Valley.dll")))
Program.PrintErrorAndExit("Oops! You're running Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io.");
// can't load file
Program.PrintErrorAndExit(
message: "Oops! SMAPI couldn't load the game executable. The technical details below may have more info.",
@ -143,6 +132,20 @@ namespace StardewModdingAPI
}
}
/// <summary>Assert that SMAPI's <c>StardewModdingAPI.deps.json</c> matches <c>Stardew Valley.deps.json</c>, fixing it if necessary.</summary>
/// <remarks>This is needed to resolve native DLLs like libSkiaSharp.</remarks>
private static void AssertDepsJson()
{
string sourcePath = Path.Combine(Constants.ExecutionPath, "Stardew Valley.deps.json");
string targetPath = Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.deps.json");
if (!File.Exists(targetPath) || FileUtilities.GetFileHash(sourcePath) != FileUtilities.GetFileHash(targetPath))
{
File.Copy(sourcePath, targetPath, overwrite: true);
Program.PrintErrorAndExit($"The '{Path.GetFileName(targetPath)}' file didn't match the game's version. SMAPI fixed it automatically, but you must restart SMAPI for the change to take effect.");
}
}
/// <summary>Initialize SMAPI and launch the game.</summary>
/// <param name="args">The command-line arguments.</param>
/// <remarks>This method is separate from <see cref="Main"/> because that can't contain any references to assemblies loaded by <see cref="CurrentDomain_AssemblyResolve"/> (e.g. via <see cref="Constants"/>), or Mono will incorrectly show an assembly resolution error before assembly resolution is set up.</remarks>

View File

@ -76,8 +76,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future
/**
* The base URL for SMAPI's web API, used to perform update checks.
* Note: the protocol will be changed to http:// on Linux/macOS due to OpenSSL issues with the
* game's bundled Mono.
*/
"WebApiBaseUrl": "https://smapi.io/api/",

View File

@ -3,13 +3,16 @@
<AssemblyName>StardewModdingAPI</AssemblyName>
<RootNamespace>StardewModdingAPI</RootNamespace>
<Description>The modding API for Stardew Valley.</Description>
<TargetFramework>net452</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<TargetFramework>net5.0</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<OutputType>Exe</OutputType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<LargeAddressAware Condition="'$(OS)' == 'Windows_NT'">true</LargeAddressAware>
<ApplicationIcon>icon.ico</ApplicationIcon>
<!--copy dependency DLLs to bin folder so we can include them in installer bundle -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<Import Project="..\..\build\common.targets" />
@ -19,43 +22,22 @@
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="MonoMod.Common" Version="21.6.21.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Platonymous.TMXTile" Version="1.5.8" />
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="..\..\build\0Harmony.dll" Private="True" />
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
<Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" />
<Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" />
<Reference Include="System.Numerics" Private="True" />
<Reference Include="System.Runtime.Caching" Private="True" />
<Reference Include="BmFont" HintPath="$(GamePath)\BmFont.dll" Private="False" />
<Reference Include="GalaxyCSharp" HintPath="$(GamePath)\GalaxyCSharp.dll" Private="False" />
<Reference Include="Lidgren.Network" HintPath="$(GamePath)\Lidgren.Network.dll" Private="False" />
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" />
</ItemGroup>
<!-- Windows only -->
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<!-- Game framework -->
<Choose>
<When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))">
<ItemGroup>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />