Merge branch 'develop' into stable
This commit is contained in:
commit
d0dc3ea6f6
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<!--set properties -->
|
<!--set properties -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>3.8.4</Version>
|
<Version>3.9.0</Version>
|
||||||
<Product>SMAPI</Product>
|
<Product>SMAPI</Product>
|
||||||
|
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
@ -37,16 +37,24 @@
|
||||||
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
|
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
|
||||||
</Target>
|
</Target>
|
||||||
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'">
|
|
||||||
|
<Target Name="CopyDefaultMods" Condition="'$(MSBuildProjectName)' == 'SMAPI.Mods.ConsoleCommands' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.ErrorHandler' OR '$(MSBuildProjectName)' == 'SMAPI.Mods.SaveBackup'">
|
||||||
|
<ItemGroup>
|
||||||
|
<TranslationFiles Include="$(TargetDir)\i18n\*.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" />
|
||||||
<Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
|
<Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)" />
|
||||||
|
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\Mods\$(AssemblyName)\i18n" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="CopyToolkit" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
|
<Target Name="CopyToolkit" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit.CoreInterfaces' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
|
<Target Name="CopyToolkitCoreInterfaces" Condition="'$(MSBuildProjectName)' == 'SMAPI.Toolkit.CoreInterfaces' AND $(TargetFramework) == 'net4.5'" AfterTargets="PostBuildEvent">
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
|
|
|
@ -20,15 +20,6 @@
|
||||||
</When>
|
</When>
|
||||||
<When Condition="$(OS) == 'Windows_NT'">
|
<When Condition="$(OS) == 'Windows_NT'">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- default paths -->
|
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GalaxyClient\Games\Stardew Valley</GamePath>
|
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Galaxy\Games\Stardew Valley</GamePath>
|
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Stardew Valley</GamePath>
|
|
||||||
|
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath>
|
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley</GamePath>
|
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath>
|
|
||||||
|
|
||||||
<!-- registry paths -->
|
<!-- registry paths -->
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath>
|
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath>
|
||||||
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath>
|
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath>
|
||||||
|
@ -36,6 +27,17 @@
|
||||||
<!-- derive from Steam library path -->
|
<!-- derive from Steam library path -->
|
||||||
<_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32))</_SteamLibraryPath>
|
<_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32))</_SteamLibraryPath>
|
||||||
<GamePath Condition="!Exists('$(GamePath)') AND '$(_SteamLibraryPath)' != ''">$(_SteamLibraryPath)\steamapps\common\Stardew Valley</GamePath>
|
<GamePath Condition="!Exists('$(GamePath)') AND '$(_SteamLibraryPath)' != ''">$(_SteamLibraryPath)\steamapps\common\Stardew Valley</GamePath>
|
||||||
|
|
||||||
|
<!-- default paths -->
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GalaxyClient\Games\Stardew Valley</GamePath>
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Galaxy\Games\Stardew Valley</GamePath>
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\GOG Games\Stardew Valley</GamePath>
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Stardew Valley</GamePath>
|
||||||
|
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath>
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley</GamePath>
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GOG Games\Stardew Valley</GamePath>
|
||||||
|
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</When>
|
</When>
|
||||||
</Choose>
|
</Choose>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<SmapiBin>$(BuildRootPath)\SMAPI\bin\$(Configuration)</SmapiBin>
|
<SmapiBin>$(BuildRootPath)\SMAPI\bin\$(Configuration)</SmapiBin>
|
||||||
<ToolkitBin>$(BuildRootPath)\SMAPI.Toolkit\bin\$(Configuration)\net4.5</ToolkitBin>
|
<ToolkitBin>$(BuildRootPath)\SMAPI.Toolkit\bin\$(Configuration)\net4.5</ToolkitBin>
|
||||||
<ConsoleCommandsBin>$(BuildRootPath)\SMAPI.Mods.ConsoleCommands\bin\$(Configuration)</ConsoleCommandsBin>
|
<ConsoleCommandsBin>$(BuildRootPath)\SMAPI.Mods.ConsoleCommands\bin\$(Configuration)</ConsoleCommandsBin>
|
||||||
|
<ErrorHandlerBin>$(BuildRootPath)\SMAPI.Mods.ErrorHandler\bin\$(Configuration)</ErrorHandlerBin>
|
||||||
<SaveBackupBin>$(BuildRootPath)\SMAPI.Mods.SaveBackup\bin\$(Configuration)</SaveBackupBin>
|
<SaveBackupBin>$(BuildRootPath)\SMAPI.Mods.SaveBackup\bin\$(Configuration)</SaveBackupBin>
|
||||||
|
|
||||||
<PackagePath>$(OutRootPath)\SMAPI installer</PackagePath>
|
<PackagePath>$(OutRootPath)\SMAPI installer</PackagePath>
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TranslationFiles Include="$(SmapiBin)\i18n\*.json" />
|
<TranslationFiles Include="$(SmapiBin)\i18n\*.json" />
|
||||||
|
<ErrorHandlerTranslationFiles Include="$(ErrorHandlerBin)\i18n\*.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- reset package directory -->
|
<!-- reset package directory -->
|
||||||
|
@ -64,6 +66,10 @@
|
||||||
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.dll" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
|
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.dll" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
|
||||||
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
|
<Copy SourceFiles="$(ConsoleCommandsBin)\ConsoleCommands.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
|
||||||
<Copy SourceFiles="$(ConsoleCommandsBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
|
<Copy SourceFiles="$(ConsoleCommandsBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\ConsoleCommands" />
|
||||||
|
<Copy SourceFiles="$(ErrorHandlerBin)\ErrorHandler.dll" DestinationFolder="$(PackagePath)\bundle\Mods\ErrorHandler" />
|
||||||
|
<Copy SourceFiles="$(ErrorHandlerBin)\ErrorHandler.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\ErrorHandler" />
|
||||||
|
<Copy SourceFiles="$(ErrorHandlerBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\ErrorHandler" />
|
||||||
|
<Copy SourceFiles="@(ErrorHandlerTranslationFiles)" DestinationFolder="$(PackagePath)\bundle\Mods\ErrorHandler\i18n" />
|
||||||
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.dll" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.dll" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
||||||
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
<Copy SourceFiles="$(SaveBackupBin)\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
||||||
<Copy SourceFiles="$(SaveBackupBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
<Copy SourceFiles="$(SaveBackupBin)\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
||||||
|
|
|
@ -7,6 +7,46 @@
|
||||||
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
|
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 3.9
|
||||||
|
Released 22 January 2021 for Stardew Valley 1.5.4 or later.
|
||||||
|
|
||||||
|
* For players:
|
||||||
|
* Updated for Stardew Valley 1.5.4.
|
||||||
|
* Improved game detection in the installer:
|
||||||
|
* The installer now prefers paths registered by Steam or GOG Galaxy.
|
||||||
|
* The installer now detects default manual GOG installs.
|
||||||
|
* Added clearer error text for empty mod folders created by Vortex.
|
||||||
|
* Fixed the game's map changes not always reapplied correctly after mods change certain maps, which caused issues like the community center resetting to its non-repaired texture.
|
||||||
|
* Fixed compatibility for very old content packs which still load maps from `.xnb` files. These were broken by map loading changes in Stardew Valley 1.5, but SMAPI now corrects them automatically.
|
||||||
|
* Fixed some broken mods incorrectly listed as XNB mods under 'skipped mods'.
|
||||||
|
|
||||||
|
* For modders:
|
||||||
|
* Added new input APIs:
|
||||||
|
* Added an [API for multi-key bindings](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList).
|
||||||
|
* Added a new [`Input.ButtonsChanged` event](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged).
|
||||||
|
* Added a `buttonState.IsDown()` extension.
|
||||||
|
* Added a `helper.Input.SuppressActiveKeybinds` method to suppress the active buttons in a keybind list.
|
||||||
|
* Improved multiplayer APIs:
|
||||||
|
* `PerScreen<T>` now lets you get/set the value for any screen, get all active values, or clear all values.
|
||||||
|
* Peer data from the multiplayer API/events now includes `IsSplitScreen` and `ScreenID` fields.
|
||||||
|
* Fixed network messages through the multiplayer API being sent to players who don't have SMAPI installed in some cases.
|
||||||
|
* Improved asset propagation:
|
||||||
|
* Updated map propagation for the changes in Stardew Valley 1.5.4.
|
||||||
|
* Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names).
|
||||||
|
* Fixed quarry bridge not fixed if the mountain map was reloaded.
|
||||||
|
* Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This prevents older mods from loading, but bypasses a Visual Studio crash when debugging.
|
||||||
|
* Game errors shown in the chatbox are now logged.
|
||||||
|
* Moved vanilla error-handling into a new Error Handler mod. This simplifies the core SMAPI logic, and lets users disable it if needed.
|
||||||
|
|
||||||
|
* For the Console Commands mod:
|
||||||
|
* Removed the `inf` option for `player_sethealth`, `player_setmoney`, and `player_setstamina`. You can use mods like [CJB Cheats Menu](https://www.nexusmods.com/stardewvalley/mods/4) instead for that.
|
||||||
|
|
||||||
|
* For the Error Handler mod:
|
||||||
|
* Added a detailed message for the _Input string was not in a correct format_ error when the game fails to parse an item text description.
|
||||||
|
|
||||||
|
* For the web UI:
|
||||||
|
* Fixed JSON validator incorrectly marking some manifest update keys as invalid.
|
||||||
|
|
||||||
## 3.8.4
|
## 3.8.4
|
||||||
Released 15 January 2021 for Stardew Valley 1.5.3 or later.
|
Released 15 January 2021 for Stardew Valley 1.5.3 or later.
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ namespace StardewModdingApi.Installer
|
||||||
/// <summary>The mod IDs which the installer should allow as bundled mods.</summary>
|
/// <summary>The mod IDs which the installer should allow as bundled mods.</summary>
|
||||||
private readonly string[] BundledModIDs = {
|
private readonly string[] BundledModIDs = {
|
||||||
"SMAPI.SaveBackup",
|
"SMAPI.SaveBackup",
|
||||||
"SMAPI.ConsoleCommands"
|
"SMAPI.ConsoleCommands",
|
||||||
|
"SMAPI.ErrorHandler"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Get the absolute file or folder paths to remove when uninstalling SMAPI.</summary>
|
/// <summary>Get the absolute file or folder paths to remove when uninstalling SMAPI.</summary>
|
||||||
|
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
||||||
{
|
{
|
||||||
/// <summary>The base implementation for a trainer command.</summary>
|
/// <summary>The base implementation for a console command.</summary>
|
||||||
internal abstract class TrainerCommand : ITrainerCommand
|
internal abstract class ConsoleCommand : IConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Accessors
|
** Accessors
|
||||||
|
@ -50,7 +50,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
||||||
/// <param name="description">The command description.</param>
|
/// <param name="description">The command description.</param>
|
||||||
/// <param name="mayNeedInput">Whether the command may need to perform logic when the player presses a button.</param>
|
/// <param name="mayNeedInput">Whether the command may need to perform logic when the player presses a button.</param>
|
||||||
/// <param name="mayNeedUpdate">Whether the command may need to perform logic when the game updates.</param>
|
/// <param name="mayNeedUpdate">Whether the command may need to perform logic when the game updates.</param>
|
||||||
protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false)
|
protected ConsoleCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false)
|
||||||
{
|
{
|
||||||
this.Name = name;
|
this.Name = name;
|
||||||
this.Description = description;
|
this.Description = description;
|
|
@ -1,7 +1,7 @@
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
||||||
{
|
{
|
||||||
/// <summary>A console command to register.</summary>
|
/// <summary>A console command to register.</summary>
|
||||||
internal interface ITrainerCommand
|
internal interface IConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Accessors
|
** Accessors
|
|
@ -5,7 +5,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||||
{
|
{
|
||||||
/// <summary>A command which runs one of the game's save migrations.</summary>
|
/// <summary>A command which runs one of the game's save migrations.</summary>
|
||||||
internal class ApplySaveFixCommand : TrainerCommand
|
internal class ApplySaveFixCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||||
{
|
{
|
||||||
/// <summary>A command which sends a debug command to the game.</summary>
|
/// <summary>A command which sends a debug command to the game.</summary>
|
||||||
internal class DebugCommand : TrainerCommand
|
internal class DebugCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||||
{
|
{
|
||||||
/// <summary>A command which shows the data files.</summary>
|
/// <summary>A command which shows the data files.</summary>
|
||||||
internal class ShowDataFilesCommand : TrainerCommand
|
internal class ShowDataFilesCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||||
{
|
{
|
||||||
/// <summary>A command which shows the game files.</summary>
|
/// <summary>A command which shows the game files.</summary>
|
||||||
internal class ShowGameFilesCommand : TrainerCommand
|
internal class ShowGameFilesCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||||
{
|
{
|
||||||
/// <summary>A command which logs the keys being pressed for 30 seconds once enabled.</summary>
|
/// <summary>A command which logs the keys being pressed for 30 seconds once enabled.</summary>
|
||||||
internal class TestInputCommand : TrainerCommand
|
internal class TestInputCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -7,7 +7,7 @@ using Object = StardewValley.Object;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which adds an item to the player inventory.</summary>
|
/// <summary>A command which adds an item to the player inventory.</summary>
|
||||||
internal class AddCommand : TrainerCommand
|
internal class AddCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which list item types.</summary>
|
/// <summary>A command which list item types.</summary>
|
||||||
internal class ListItemTypesCommand : TrainerCommand
|
internal class ListItemTypesCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -6,7 +6,7 @@ using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which list items available to spawn.</summary>
|
/// <summary>A command which list items available to spawn.</summary>
|
||||||
internal class ListItemsCommand : TrainerCommand
|
internal class ListItemsCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the color of a player feature.</summary>
|
/// <summary>A command which edits the color of a player feature.</summary>
|
||||||
internal class SetColorCommand : TrainerCommand
|
internal class SetColorCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,21 +4,14 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's current health.</summary>
|
/// <summary>A command which edits the player's current health.</summary>
|
||||||
internal class SetHealthCommand : TrainerCommand
|
internal class SetHealthCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
|
||||||
** Fields
|
|
||||||
*********/
|
|
||||||
/// <summary>Whether to keep the player's health at its maximum.</summary>
|
|
||||||
private bool InfiniteHealth;
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
public SetHealthCommand()
|
public SetHealthCommand()
|
||||||
: base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { }
|
: base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount.") { }
|
||||||
|
|
||||||
/// <summary>Handle the command.</summary>
|
/// <summary>Handle the command.</summary>
|
||||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||||
|
@ -29,20 +22,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
// no-argument mode
|
// no-argument mode
|
||||||
if (!args.Any())
|
if (!args.Any())
|
||||||
{
|
{
|
||||||
monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info);
|
monitor.Log($"You currently have {Game1.player.health} health. Specify a value to change it.", LogLevel.Info);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle
|
// handle
|
||||||
string amountStr = args[0];
|
string amountStr = args[0];
|
||||||
if (amountStr == "inf")
|
|
||||||
{
|
|
||||||
this.InfiniteHealth = true;
|
|
||||||
monitor.Log("OK, you now have infinite health.", LogLevel.Info);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.InfiniteHealth = false;
|
|
||||||
if (int.TryParse(amountStr, out int amount))
|
if (int.TryParse(amountStr, out int amount))
|
||||||
{
|
{
|
||||||
Game1.player.health = amount;
|
Game1.player.health = amount;
|
||||||
|
@ -52,13 +37,4 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
this.LogArgumentNotInt(monitor);
|
this.LogArgumentNotInt(monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Perform any logic needed on update tick.</summary>
|
|
||||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
|
||||||
public override void OnUpdated(IMonitor monitor)
|
|
||||||
{
|
|
||||||
if (this.InfiniteHealth && Context.IsWorldReady)
|
|
||||||
Game1.player.health = Game1.player.maxHealth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's current immunity.</summary>
|
/// <summary>A command which edits the player's current immunity.</summary>
|
||||||
internal class SetImmunityCommand : TrainerCommand
|
internal class SetImmunityCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's maximum health.</summary>
|
/// <summary>A command which edits the player's maximum health.</summary>
|
||||||
internal class SetMaxHealthCommand : TrainerCommand
|
internal class SetMaxHealthCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's maximum stamina.</summary>
|
/// <summary>A command which edits the player's maximum stamina.</summary>
|
||||||
internal class SetMaxStaminaCommand : TrainerCommand
|
internal class SetMaxStaminaCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,21 +4,14 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's current money.</summary>
|
/// <summary>A command which edits the player's current money.</summary>
|
||||||
internal class SetMoneyCommand : TrainerCommand
|
internal class SetMoneyCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
|
||||||
** Fields
|
|
||||||
*********/
|
|
||||||
/// <summary>Whether to keep the player's money at a set value.</summary>
|
|
||||||
private bool InfiniteMoney;
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
public SetMoneyCommand()
|
public SetMoneyCommand()
|
||||||
: base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { }
|
: base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount.") { }
|
||||||
|
|
||||||
/// <summary>Handle the command.</summary>
|
/// <summary>Handle the command.</summary>
|
||||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||||
|
@ -29,20 +22,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
// validate
|
// validate
|
||||||
if (!args.Any())
|
if (!args.Any())
|
||||||
{
|
{
|
||||||
monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info);
|
monitor.Log($"You currently have {Game1.player.Money} gold. Specify a value to change it.", LogLevel.Info);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle
|
// handle
|
||||||
string amountStr = args[0];
|
string amountStr = args[0];
|
||||||
if (amountStr == "inf")
|
|
||||||
{
|
|
||||||
this.InfiniteMoney = true;
|
|
||||||
monitor.Log("OK, you now have infinite money.", LogLevel.Info);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.InfiniteMoney = false;
|
|
||||||
if (int.TryParse(amountStr, out int amount))
|
if (int.TryParse(amountStr, out int amount))
|
||||||
{
|
{
|
||||||
Game1.player.Money = amount;
|
Game1.player.Money = amount;
|
||||||
|
@ -52,13 +37,4 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
this.LogArgumentNotInt(monitor);
|
this.LogArgumentNotInt(monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Perform any logic needed on update tick.</summary>
|
|
||||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
|
||||||
public override void OnUpdated(IMonitor monitor)
|
|
||||||
{
|
|
||||||
if (this.InfiniteMoney && Context.IsWorldReady)
|
|
||||||
Game1.player.Money = 999999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's name.</summary>
|
/// <summary>A command which edits the player's name.</summary>
|
||||||
internal class SetNameCommand : TrainerCommand
|
internal class SetNameCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,21 +4,14 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits the player's current stamina.</summary>
|
/// <summary>A command which edits the player's current stamina.</summary>
|
||||||
internal class SetStaminaCommand : TrainerCommand
|
internal class SetStaminaCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
|
||||||
** Fields
|
|
||||||
*********/
|
|
||||||
/// <summary>Whether to keep the player's stamina at its maximum.</summary>
|
|
||||||
private bool InfiniteStamina;
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
public SetStaminaCommand()
|
public SetStaminaCommand()
|
||||||
: base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { }
|
: base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount.") { }
|
||||||
|
|
||||||
/// <summary>Handle the command.</summary>
|
/// <summary>Handle the command.</summary>
|
||||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||||
|
@ -29,20 +22,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
// validate
|
// validate
|
||||||
if (!args.Any())
|
if (!args.Any())
|
||||||
{
|
{
|
||||||
monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info);
|
monitor.Log($"You currently have {Game1.player.Stamina} stamina. Specify a value to change it.", LogLevel.Info);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle
|
// handle
|
||||||
string amountStr = args[0];
|
string amountStr = args[0];
|
||||||
if (amountStr == "inf")
|
|
||||||
{
|
|
||||||
this.InfiniteStamina = true;
|
|
||||||
monitor.Log("OK, you now have infinite stamina.", LogLevel.Info);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.InfiniteStamina = false;
|
|
||||||
if (int.TryParse(amountStr, out int amount))
|
if (int.TryParse(amountStr, out int amount))
|
||||||
{
|
{
|
||||||
Game1.player.Stamina = amount;
|
Game1.player.Stamina = amount;
|
||||||
|
@ -52,13 +37,4 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
this.LogArgumentNotInt(monitor);
|
this.LogArgumentNotInt(monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Perform any logic needed on update tick.</summary>
|
|
||||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
|
||||||
public override void OnUpdated(IMonitor monitor)
|
|
||||||
{
|
|
||||||
if (this.InfiniteStamina && Context.IsWorldReady)
|
|
||||||
Game1.player.stamina = Game1.player.MaxStamina;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||||
{
|
{
|
||||||
/// <summary>A command which edits a player style.</summary>
|
/// <summary>A command which edits a player style.</summary>
|
||||||
internal class SetStyleCommand : TrainerCommand
|
internal class SetStyleCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -10,7 +10,7 @@ using SObject = StardewValley.Object;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which clears in-game objects.</summary>
|
/// <summary>A command which clears in-game objects.</summary>
|
||||||
internal class ClearCommand : TrainerCommand
|
internal class ClearCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley.Locations;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which moves the player to the next mine level.</summary>
|
/// <summary>A command which moves the player to the next mine level.</summary>
|
||||||
internal class DownMineLevelCommand : TrainerCommand
|
internal class DownMineLevelCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which freezes the current time.</summary>
|
/// <summary>A command which freezes the current time.</summary>
|
||||||
internal class FreezeTimeCommand : TrainerCommand
|
internal class FreezeTimeCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -5,7 +5,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which sets the current day.</summary>
|
/// <summary>A command which sets the current day.</summary>
|
||||||
internal class SetDayCommand : TrainerCommand
|
internal class SetDayCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -4,7 +4,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which moves the player to the given mine level.</summary>
|
/// <summary>A command which moves the player to the given mine level.</summary>
|
||||||
internal class SetMineLevelCommand : TrainerCommand
|
internal class SetMineLevelCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -5,7 +5,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which sets the current season.</summary>
|
/// <summary>A command which sets the current season.</summary>
|
||||||
internal class SetSeasonCommand : TrainerCommand
|
internal class SetSeasonCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
|
|
|
@ -5,7 +5,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which sets the current time.</summary>
|
/// <summary>A command which sets the current time.</summary>
|
||||||
internal class SetTimeCommand : TrainerCommand
|
internal class SetTimeCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -5,7 +5,7 @@ using StardewValley;
|
||||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||||
{
|
{
|
||||||
/// <summary>A command which sets the current year.</summary>
|
/// <summary>A command which sets the current year.</summary>
|
||||||
internal class SetYearCommand : TrainerCommand
|
internal class SetYearCommand : ConsoleCommand
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -13,13 +13,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
||||||
** Fields
|
** Fields
|
||||||
*********/
|
*********/
|
||||||
/// <summary>The commands to handle.</summary>
|
/// <summary>The commands to handle.</summary>
|
||||||
private ITrainerCommand[] Commands;
|
private IConsoleCommand[] Commands;
|
||||||
|
|
||||||
/// <summary>The commands which may need to handle update ticks.</summary>
|
/// <summary>The commands which may need to handle update ticks.</summary>
|
||||||
private ITrainerCommand[] UpdateHandlers;
|
private IConsoleCommand[] UpdateHandlers;
|
||||||
|
|
||||||
/// <summary>The commands which may need to handle input.</summary>
|
/// <summary>The commands which may need to handle input.</summary>
|
||||||
private ITrainerCommand[] InputHandlers;
|
private IConsoleCommand[] InputHandlers;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
@ -31,7 +31,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
||||||
{
|
{
|
||||||
// register commands
|
// register commands
|
||||||
this.Commands = this.ScanForCommands().ToArray();
|
this.Commands = this.ScanForCommands().ToArray();
|
||||||
foreach (ITrainerCommand command in this.Commands)
|
foreach (IConsoleCommand command in this.Commands)
|
||||||
helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args));
|
helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args));
|
||||||
|
|
||||||
// cache commands
|
// cache commands
|
||||||
|
@ -52,7 +52,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
||||||
/// <param name="e">The event arguments.</param>
|
/// <param name="e">The event arguments.</param>
|
||||||
private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
|
private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
|
||||||
{
|
{
|
||||||
foreach (ITrainerCommand command in this.InputHandlers)
|
foreach (IConsoleCommand command in this.InputHandlers)
|
||||||
command.OnButtonPressed(this.Monitor, e.Button);
|
command.OnButtonPressed(this.Monitor, e.Button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
||||||
/// <param name="e">The event arguments.</param>
|
/// <param name="e">The event arguments.</param>
|
||||||
private void OnUpdateTicked(object sender, EventArgs e)
|
private void OnUpdateTicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
foreach (ITrainerCommand command in this.UpdateHandlers)
|
foreach (IConsoleCommand command in this.UpdateHandlers)
|
||||||
command.OnUpdated(this.Monitor);
|
command.OnUpdated(this.Monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,19 +69,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
||||||
/// <param name="command">The command to invoke.</param>
|
/// <param name="command">The command to invoke.</param>
|
||||||
/// <param name="commandName">The command name specified by the user.</param>
|
/// <param name="commandName">The command name specified by the user.</param>
|
||||||
/// <param name="args">The command arguments.</param>
|
/// <param name="args">The command arguments.</param>
|
||||||
private void HandleCommand(ITrainerCommand command, string commandName, string[] args)
|
private void HandleCommand(IConsoleCommand command, string commandName, string[] args)
|
||||||
{
|
{
|
||||||
ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor);
|
ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor);
|
||||||
command.Handle(this.Monitor, commandName, argParser);
|
command.Handle(this.Monitor, commandName, argParser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Find all commands in the assembly.</summary>
|
/// <summary>Find all commands in the assembly.</summary>
|
||||||
private IEnumerable<ITrainerCommand> ScanForCommands()
|
private IEnumerable<IConsoleCommand> ScanForCommands()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
from type in this.GetType().Assembly.GetTypes()
|
from type in this.GetType().Assembly.GetTypes()
|
||||||
where !type.IsAbstract && typeof(ITrainerCommand).IsAssignableFrom(type)
|
where !type.IsAbstract && typeof(IConsoleCommand).IsAssignableFrom(type)
|
||||||
select (ITrainerCommand)Activator.CreateInstance(type)
|
select (IConsoleCommand)Activator.CreateInstance(type)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Console Commands",
|
"Name": "Console Commands",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.8.4",
|
"Version": "3.9.0",
|
||||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||||
"UniqueID": "SMAPI.ConsoleCommands",
|
"UniqueID": "SMAPI.ConsoleCommands",
|
||||||
"EntryDll": "ConsoleCommands.dll",
|
"EntryDll": "ConsoleCommands.dll",
|
||||||
"MinimumApiVersion": "3.8.4"
|
"MinimumApiVersion": "3.9.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using StardewModdingAPI.Events;
|
||||||
|
using StardewModdingAPI.Framework;
|
||||||
|
using StardewModdingAPI.Framework.Logging;
|
||||||
|
using StardewModdingAPI.Framework.Patching;
|
||||||
|
using StardewModdingAPI.Mods.ErrorHandler.Patches;
|
||||||
|
using StardewValley;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Mods.ErrorHandler
|
||||||
|
{
|
||||||
|
/// <summary>The main entry point for the mod.</summary>
|
||||||
|
public class ModEntry : Mod
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Whether custom content was removed from the save data to avoid a crash.</summary>
|
||||||
|
private bool IsSaveContentRemoved;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
|
||||||
|
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||||
|
public override void Entry(IModHelper helper)
|
||||||
|
{
|
||||||
|
// get SMAPI core types
|
||||||
|
SCore core = SCore.Instance;
|
||||||
|
LogManager logManager = core.GetType().GetField("LogManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(core) as LogManager;
|
||||||
|
if (logManager == null)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Can't access SMAPI's internal log manager. Error-handling patches won't be applied.", LogLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply patches
|
||||||
|
new GamePatcher(this.Monitor).Apply(
|
||||||
|
new EventErrorPatch(logManager.MonitorForGame),
|
||||||
|
new DialogueErrorPatch(logManager.MonitorForGame, this.Helper.Reflection),
|
||||||
|
new ObjectErrorPatch(),
|
||||||
|
new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved),
|
||||||
|
new ScheduleErrorPatch(logManager.MonitorForGame),
|
||||||
|
new UtilityErrorPatches()
|
||||||
|
);
|
||||||
|
|
||||||
|
// hook events
|
||||||
|
this.Helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Raised after custom content is removed from the save data to avoid a crash.</summary>
|
||||||
|
internal void OnSaveContentRemoved()
|
||||||
|
{
|
||||||
|
this.IsSaveContentRemoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The method invoked when a save is loaded.</summary>
|
||||||
|
/// <param name="sender">The event sender.</param>
|
||||||
|
/// <param name="e">The event arguments.</param>
|
||||||
|
public void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
|
||||||
|
{
|
||||||
|
// show in-game warning for removed save content
|
||||||
|
if (this.IsSaveContentRemoved)
|
||||||
|
{
|
||||||
|
this.IsSaveContentRemoved = false;
|
||||||
|
Game1.addHUDMessage(new HUDMessage(this.Helper.Translation.Get("warn.invalid-content-removed"), HUDMessage.error_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using StardewModdingAPI.Framework.Patching;
|
using StardewModdingAPI.Framework.Patching;
|
||||||
using StardewModdingAPI.Framework.Reflection;
|
|
||||||
using StardewValley;
|
using StardewValley;
|
||||||
#if HARMONY_2
|
#if HARMONY_2
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
@ -12,7 +11,7 @@ using System.Reflection;
|
||||||
using Harmony;
|
using Harmony;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace StardewModdingAPI.Patches
|
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||||
{
|
{
|
||||||
/// <summary>A Harmony patch for the <see cref="Dialogue"/> constructor which intercepts invalid dialogue lines and logs an error instead of crashing.</summary>
|
/// <summary>A Harmony patch for the <see cref="Dialogue"/> constructor which intercepts invalid dialogue lines and logs an error instead of crashing.</summary>
|
||||||
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
||||||
|
@ -27,7 +26,7 @@ namespace StardewModdingAPI.Patches
|
||||||
private static IMonitor MonitorForGame;
|
private static IMonitor MonitorForGame;
|
||||||
|
|
||||||
/// <summary>Simplifies access to private code.</summary>
|
/// <summary>Simplifies access to private code.</summary>
|
||||||
private static Reflector Reflection;
|
private static IReflectionHelper Reflection;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
@ -43,7 +42,7 @@ namespace StardewModdingAPI.Patches
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param>
|
/// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param>
|
||||||
/// <param name="reflector">Simplifies access to private code.</param>
|
/// <param name="reflector">Simplifies access to private code.</param>
|
||||||
public DialogueErrorPatch(IMonitor monitorForGame, Reflector reflector)
|
public DialogueErrorPatch(IMonitor monitorForGame, IReflectionHelper reflector)
|
||||||
{
|
{
|
||||||
DialogueErrorPatch.MonitorForGame = monitorForGame;
|
DialogueErrorPatch.MonitorForGame = monitorForGame;
|
||||||
DialogueErrorPatch.Reflection = reflector;
|
DialogueErrorPatch.Reflection = reflector;
|
||||||
|
@ -167,7 +166,7 @@ namespace StardewModdingAPI.Patches
|
||||||
/// <returns>Returns whether to execute the original method.</returns>
|
/// <returns>Returns whether to execute the original method.</returns>
|
||||||
private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, MethodInfo __originalMethod)
|
private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, MethodInfo __originalMethod)
|
||||||
{
|
{
|
||||||
const string key = nameof(Before_NPC_CurrentDialogue);
|
const string key = nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue);
|
||||||
if (!PatchHelper.StartIntercept(key))
|
if (!PatchHelper.StartIntercept(key))
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -9,7 +9,7 @@ using Harmony;
|
||||||
using StardewModdingAPI.Framework.Patching;
|
using StardewModdingAPI.Framework.Patching;
|
||||||
using StardewValley;
|
using StardewValley;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Patches
|
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||||
{
|
{
|
||||||
/// <summary>A Harmony patch for <see cref="GameLocation.checkEventPrecondition"/> which intercepts invalid preconditions and logs an error instead of crashing.</summary>
|
/// <summary>A Harmony patch for <see cref="GameLocation.checkEventPrecondition"/> which intercepts invalid preconditions and logs an error instead of crashing.</summary>
|
||||||
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
||||||
|
@ -89,7 +89,7 @@ namespace StardewModdingAPI.Patches
|
||||||
/// <returns>Returns whether to execute the original method.</returns>
|
/// <returns>Returns whether to execute the original method.</returns>
|
||||||
private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod)
|
private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod)
|
||||||
{
|
{
|
||||||
const string key = nameof(Before_GameLocation_CheckEventPrecondition);
|
const string key = nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition);
|
||||||
if (!PatchHelper.StartIntercept(key))
|
if (!PatchHelper.StartIntercept(key))
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -13,7 +13,7 @@ using StardewValley;
|
||||||
using StardewValley.Buildings;
|
using StardewValley.Buildings;
|
||||||
using StardewValley.Locations;
|
using StardewValley.Locations;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Patches
|
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||||
{
|
{
|
||||||
/// <summary>A Harmony patch for <see cref="SaveGame"/> which prevents some errors due to broken save data.</summary>
|
/// <summary>A Harmony patch for <see cref="SaveGame"/> which prevents some errors due to broken save data.</summary>
|
||||||
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
|
@ -12,7 +12,7 @@ using System.Reflection;
|
||||||
using Harmony;
|
using Harmony;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace StardewModdingAPI.Patches
|
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||||
{
|
{
|
||||||
/// <summary>A Harmony patch for <see cref="SObject.getDescription"/> which intercepts crashes due to the item no longer existing.</summary>
|
/// <summary>A Harmony patch for <see cref="SObject.getDescription"/> which intercepts crashes due to the item no longer existing.</summary>
|
||||||
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
||||||
|
@ -103,7 +103,7 @@ namespace StardewModdingAPI.Patches
|
||||||
/// <returns>Returns whether to execute the original method.</returns>
|
/// <returns>Returns whether to execute the original method.</returns>
|
||||||
private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod)
|
private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod)
|
||||||
{
|
{
|
||||||
const string key = nameof(Before_Object_loadDisplayName);
|
const string key = nameof(ObjectErrorPatch.Before_Object_loadDisplayName);
|
||||||
if (!PatchHelper.StartIntercept(key))
|
if (!PatchHelper.StartIntercept(key))
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -11,7 +11,7 @@ using System.Reflection;
|
||||||
using Harmony;
|
using Harmony;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace StardewModdingAPI.Patches
|
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||||
{
|
{
|
||||||
/// <summary>A Harmony patch for <see cref="NPC.parseMasterSchedule"/> which intercepts crashes due to invalid schedule data.</summary>
|
/// <summary>A Harmony patch for <see cref="NPC.parseMasterSchedule"/> which intercepts crashes due to invalid schedule data.</summary>
|
||||||
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
||||||
|
@ -90,7 +90,7 @@ namespace StardewModdingAPI.Patches
|
||||||
/// <returns>Returns whether to execute the original method.</returns>
|
/// <returns>Returns whether to execute the original method.</returns>
|
||||||
private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, MethodInfo __originalMethod)
|
private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, MethodInfo __originalMethod)
|
||||||
{
|
{
|
||||||
const string key = nameof(Before_NPC_parseMasterSchedule);
|
const string key = nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule);
|
||||||
if (!PatchHelper.StartIntercept(key))
|
if (!PatchHelper.StartIntercept(key))
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
#if HARMONY_2
|
||||||
|
using System;
|
||||||
|
using HarmonyLib;
|
||||||
|
#else
|
||||||
|
using Harmony;
|
||||||
|
#endif
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Reflection;
|
||||||
|
using StardewModdingAPI.Framework.Patching;
|
||||||
|
using StardewValley;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||||
|
{
|
||||||
|
/// <summary>A Harmony patch for <see cref="Utility"/> methods to log more detailed errors.</summary>
|
||||||
|
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
|
||||||
|
[SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
|
||||||
|
internal class UtilityErrorPatches : IHarmonyPatch
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => nameof(UtilityErrorPatches);
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <inheritdoc />
|
||||||
|
#if HARMONY_2
|
||||||
|
public void Apply(Harmony harmony)
|
||||||
|
{
|
||||||
|
harmony.Patch(
|
||||||
|
original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)),
|
||||||
|
finalizer: new HarmonyMethod(this.GetType(), nameof(UtilityErrorPatches.Finalize_Utility_GetItemFromStandardTextDescription))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
public void Apply(HarmonyInstance harmony)
|
||||||
|
{
|
||||||
|
harmony.Patch(
|
||||||
|
original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)),
|
||||||
|
prefix: new HarmonyMethod(this.GetType(), nameof(UtilityErrorPatches.Before_Utility_GetItemFromStandardTextDescription))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
#if HARMONY_2
|
||||||
|
/// <summary>The method to call instead of <see cref="Utility.getItemFromStandardTextDescription"/>.</summary>
|
||||||
|
/// <param name="description">The item text description to parse.</param>
|
||||||
|
/// <param name="delimiter">The delimiter by which to split the text description.</param>
|
||||||
|
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||||
|
/// <returns>Returns the exception to throw, if any.</returns>
|
||||||
|
private static Exception Finalize_Utility_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception)
|
||||||
|
{
|
||||||
|
return __exception != null
|
||||||
|
? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// <summary>The method to call instead of <see cref="Utility.getItemFromStandardTextDescription"/>.</summary>
|
||||||
|
/// <param name="__result">The return value of the original method.</param>
|
||||||
|
/// <param name="description">The item text description to parse.</param>
|
||||||
|
/// <param name="who">The player for which the item is being parsed.</param>
|
||||||
|
/// <param name="delimiter">The delimiter by which to split the text description.</param>
|
||||||
|
/// <param name="__originalMethod">The method being wrapped.</param>
|
||||||
|
/// <returns>Returns whether to execute the original method.</returns>
|
||||||
|
private static bool Before_Utility_GetItemFromStandardTextDescription(ref Item __result, string description, Farmer who, char delimiter, MethodInfo __originalMethod)
|
||||||
|
{
|
||||||
|
const string key = nameof(UtilityErrorPatches.Before_Utility_GetItemFromStandardTextDescription);
|
||||||
|
if (!PatchHelper.StartIntercept(key))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
__result = (Item)__originalMethod.Invoke(null, new object[] { description, who, delimiter });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (TargetInvocationException ex)
|
||||||
|
{
|
||||||
|
throw new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", ex.InnerException);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
PatchHelper.StopIntercept(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AssemblyName>ErrorHandler</AssemblyName>
|
||||||
|
<RootNamespace>StardewModdingAPI.Mods.ErrorHandler</RootNamespace>
|
||||||
|
<TargetFramework>net45</TargetFramework>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SMAPI\SMAPI.csproj" Private="False" />
|
||||||
|
<Reference Include="..\..\build\0Harmony.dll" Private="False" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Choose>
|
||||||
|
<!-- Windows -->
|
||||||
|
<When Condition="$(OS) == 'Windows_NT'">
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Linux/Mac -->
|
||||||
|
<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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
|
||||||
|
<Import Project="..\..\build\common.targets" />
|
||||||
|
</Project>
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)"
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)"
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)."
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// warning messages
|
||||||
|
"warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Name": "Error Handler",
|
||||||
|
"Author": "SMAPI",
|
||||||
|
"Version": "3.9.0",
|
||||||
|
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
||||||
|
"UniqueID": "SMAPI.ErrorHandler",
|
||||||
|
"EntryDll": "ErrorHandler.dll",
|
||||||
|
"MinimumApiVersion": "3.9.0"
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Save Backup",
|
"Name": "Save Backup",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.8.4",
|
"Version": "3.9.0",
|
||||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||||
"UniqueID": "SMAPI.SaveBackup",
|
"UniqueID": "SMAPI.SaveBackup",
|
||||||
"EntryDll": "SaveBackup.dll",
|
"EntryDll": "SaveBackup.dll",
|
||||||
"MinimumApiVersion": "3.8.4"
|
"MinimumApiVersion": "3.9.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using StardewModdingAPI;
|
||||||
|
using StardewModdingAPI.Utilities;
|
||||||
|
|
||||||
|
namespace SMAPI.Tests.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>Unit tests for <see cref="KeybindList"/>.</summary>
|
||||||
|
[TestFixture]
|
||||||
|
internal class KeybindListTests
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Unit tests
|
||||||
|
*********/
|
||||||
|
/****
|
||||||
|
** TryParse
|
||||||
|
****/
|
||||||
|
/// <summary>Assert the parsed fields when constructed from a simple single-key string.</summary>
|
||||||
|
[TestCaseSource(nameof(KeybindListTests.GetAllButtons))]
|
||||||
|
public void TryParse_SimpleValue(SButton button)
|
||||||
|
{
|
||||||
|
// act
|
||||||
|
bool success = KeybindList.TryParse($"{button}", out KeybindList parsed, out string[] errors);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsTrue(success, "Parsing unexpectedly failed.");
|
||||||
|
Assert.IsNotNull(parsed, "The parsed result should not be null.");
|
||||||
|
Assert.AreEqual(parsed.ToString(), $"{button}");
|
||||||
|
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||||
|
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Assert the parsed fields when constructed from multi-key values.</summary>
|
||||||
|
[TestCase("", ExpectedResult = "None")]
|
||||||
|
[TestCase(" ", ExpectedResult = "None")]
|
||||||
|
[TestCase(null, ExpectedResult = "None")]
|
||||||
|
[TestCase("A + B", ExpectedResult = "A + B")]
|
||||||
|
[TestCase("A+B", ExpectedResult = "A + B")]
|
||||||
|
[TestCase(" A+ B ", ExpectedResult = "A + B")]
|
||||||
|
[TestCase("a +b", ExpectedResult = "A + B")]
|
||||||
|
[TestCase("a +b, LEFTcontrol + leftALT + LeftSHifT + delete", ExpectedResult = "A + B, LeftControl + LeftAlt + LeftShift + Delete")]
|
||||||
|
|
||||||
|
[TestCase(",", ExpectedResult = "None")]
|
||||||
|
[TestCase("A,", ExpectedResult = "A")]
|
||||||
|
[TestCase(",A", ExpectedResult = "A")]
|
||||||
|
public string TryParse_MultiValues(string input)
|
||||||
|
{
|
||||||
|
// act
|
||||||
|
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsTrue(success, "Parsing unexpectedly failed.");
|
||||||
|
Assert.IsNotNull(parsed, "The parsed result should not be null.");
|
||||||
|
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||||
|
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
|
||||||
|
return parsed.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Assert invalid values are rejected.</summary>
|
||||||
|
[TestCase("+", "Invalid empty button value")]
|
||||||
|
[TestCase("A+", "Invalid empty button value")]
|
||||||
|
[TestCase("+C", "Invalid empty button value")]
|
||||||
|
[TestCase("A + B +, C", "Invalid empty button value")]
|
||||||
|
[TestCase("A, TotallyInvalid", "Invalid button value 'TotallyInvalid'")]
|
||||||
|
[TestCase("A + TotallyInvalid", "Invalid button value 'TotallyInvalid'")]
|
||||||
|
public void TryParse_InvalidValues(string input, string expectedError)
|
||||||
|
{
|
||||||
|
// act
|
||||||
|
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsFalse(success, "Parsing unexpectedly succeeded.");
|
||||||
|
Assert.IsNull(parsed, "The parsed result should be null.");
|
||||||
|
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||||
|
Assert.AreEqual(expectedError, string.Join("; ", errors), "The errors don't match the expected ones.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/****
|
||||||
|
** GetState
|
||||||
|
****/
|
||||||
|
/// <summary>Assert that <see cref="KeybindList.GetState"/> returns the expected result for a given input state.</summary>
|
||||||
|
// single value
|
||||||
|
[TestCase("A", "A:Held", ExpectedResult = SButtonState.Held)]
|
||||||
|
[TestCase("A", "A:Pressed", ExpectedResult = SButtonState.Pressed)]
|
||||||
|
[TestCase("A", "A:Released", ExpectedResult = SButtonState.Released)]
|
||||||
|
[TestCase("A", "A:None", ExpectedResult = SButtonState.None)]
|
||||||
|
|
||||||
|
// multiple values
|
||||||
|
[TestCase("A + B + C, D", "A:Released, B:None, C:None, D:Pressed", ExpectedResult = SButtonState.Pressed)] // right pressed => pressed
|
||||||
|
[TestCase("A + B + C, D", "A:Pressed, B:Held, C:Pressed, D:None", ExpectedResult = SButtonState.Pressed)] // left pressed => pressed
|
||||||
|
[TestCase("A + B + C, D", "A:Pressed, B:Pressed, C:Released, D:None", ExpectedResult = SButtonState.None)] // one key released but other keys weren't down last tick => none
|
||||||
|
[TestCase("A + B + C, D", "A:Held, B:Held, C:Released, D:None", ExpectedResult = SButtonState.Released)] // all three keys were down last tick and now one is released => released
|
||||||
|
|
||||||
|
// transitive
|
||||||
|
[TestCase("A, B", "A: Released, B: Pressed", ExpectedResult = SButtonState.Held)]
|
||||||
|
public SButtonState GetState(string input, string stateMap)
|
||||||
|
{
|
||||||
|
// act
|
||||||
|
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
|
||||||
|
if (success && parsed?.Keybinds != null)
|
||||||
|
{
|
||||||
|
foreach (var keybind in parsed.Keybinds)
|
||||||
|
#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests
|
||||||
|
keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
|
||||||
|
#pragma warning restore 618
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsTrue(success, "Parsing unexpected failed");
|
||||||
|
Assert.IsNotNull(parsed, "The parsed result should not be null.");
|
||||||
|
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||||
|
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
|
||||||
|
return parsed.GetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get all defined buttons.</summary>
|
||||||
|
private static IEnumerable<SButton> GetAllButtons()
|
||||||
|
{
|
||||||
|
foreach (SButton button in Enum.GetValues(typeof(SButton)))
|
||||||
|
yield return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the button state defined by a mapping string.</summary>
|
||||||
|
/// <param name="button">The button to check.</param>
|
||||||
|
/// <param name="stateMap">The state map.</param>
|
||||||
|
private SButtonState GetStateFromMap(SButton button, string stateMap)
|
||||||
|
{
|
||||||
|
foreach (string rawPair in stateMap.Split(','))
|
||||||
|
{
|
||||||
|
// parse values
|
||||||
|
string[] parts = rawPair.Split(new[] { ':' }, 2);
|
||||||
|
if (!Enum.TryParse(parts[0], ignoreCase: true, out SButton curButton))
|
||||||
|
Assert.Fail($"The state map is invalid: unknown button value '{parts[0].Trim()}'");
|
||||||
|
if (!Enum.TryParse(parts[1], ignoreCase: true, out SButtonState state))
|
||||||
|
Assert.Fail($"The state map is invalid: unknown state value '{parts[1].Trim()}'");
|
||||||
|
|
||||||
|
// get state
|
||||||
|
if (curButton == button)
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Fail($"The state map doesn't define button value '{button}'.");
|
||||||
|
return SButtonState.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,14 +90,6 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
||||||
|
|
||||||
case Platform.Windows:
|
case Platform.Windows:
|
||||||
{
|
{
|
||||||
// Windows
|
|
||||||
foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
|
|
||||||
{
|
|
||||||
yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
|
|
||||||
yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley";
|
|
||||||
yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows registry
|
// Windows registry
|
||||||
#if SMAPI_FOR_WINDOWS
|
#if SMAPI_FOR_WINDOWS
|
||||||
IDictionary<string, string> registryKeys = new Dictionary<string, string>
|
IDictionary<string, string> registryKeys = new Dictionary<string, string>
|
||||||
|
@ -113,10 +105,19 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
|
||||||
}
|
}
|
||||||
|
|
||||||
// via Steam library path
|
// via Steam library path
|
||||||
string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
|
string steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
|
||||||
if (steampath != null)
|
if (steamPath != null)
|
||||||
yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
|
yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// default paths
|
||||||
|
foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
|
||||||
|
{
|
||||||
|
yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
|
||||||
|
yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley";
|
||||||
|
yield return $@"{programFiles}\GOG Games\Stardew Valley";
|
||||||
|
yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
/// <summary>The folder is empty or contains only ignored files.</summary>
|
/// <summary>The folder is empty or contains only ignored files.</summary>
|
||||||
EmptyFolder,
|
EmptyFolder,
|
||||||
|
|
||||||
|
/// <summary>The folder is an empty folder managed by Vortex.</summary>
|
||||||
|
EmptyVortexFolder,
|
||||||
|
|
||||||
/// <summary>The folder is ignored by convention.</summary>
|
/// <summary>The folder is ignored by convention.</summary>
|
||||||
IgnoredFolder,
|
IgnoredFolder,
|
||||||
|
|
||||||
|
|
|
@ -58,20 +58,28 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
".lnk"
|
".lnk"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>The extensions for files which an XNB mod may contain. If a mod doesn't have a <c>manifest.json</c> and contains *only* these file extensions, it should be considered an XNB mod.</summary>
|
/// <summary>The extensions for packed content files.</summary>
|
||||||
private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private readonly HashSet<string> StrictXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
// XNB files
|
|
||||||
".xgs",
|
".xgs",
|
||||||
".xnb",
|
".xnb",
|
||||||
".xsb",
|
".xsb",
|
||||||
".xwb",
|
".xwb"
|
||||||
|
};
|
||||||
|
|
||||||
// unpacking artifacts
|
/// <summary>The extensions for files which an XNB mod may contain, in addition to <see cref="StrictXnbModExtensions"/>.</summary>
|
||||||
|
private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
".json",
|
".json",
|
||||||
".yaml"
|
".yaml"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>The name of the marker file added by Vortex to indicate it's managing the folder.</summary>
|
||||||
|
private readonly string VortexMarkerFileName = "__folder_managed_by_vortex";
|
||||||
|
|
||||||
|
/// <summary>The name for a mod's configuration JSON file.</summary>
|
||||||
|
private readonly string ConfigFileName = "config.json";
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
@ -111,18 +119,24 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
// set appropriate invalid-mod error
|
// set appropriate invalid-mod error
|
||||||
if (manifestFile == null)
|
if (manifestFile == null)
|
||||||
{
|
{
|
||||||
FileInfo[] files = this.RecursivelyGetRelevantFiles(searchFolder).ToArray();
|
FileInfo[] files = this.RecursivelyGetFiles(searchFolder).ToArray();
|
||||||
|
FileInfo[] relevantFiles = files.Where(this.IsRelevant).ToArray();
|
||||||
|
|
||||||
|
// empty Vortex folder
|
||||||
|
// (this filters relevant files internally so it can check for the normally-ignored Vortex marker file)
|
||||||
|
if (this.IsEmptyVortexFolder(files))
|
||||||
|
return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyVortexFolder, "it's an empty Vortex folder (is the mod disabled in Vortex?).");
|
||||||
|
|
||||||
// empty folder
|
// empty folder
|
||||||
if (!files.Any())
|
if (!relevantFiles.Any())
|
||||||
return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder.");
|
return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder.");
|
||||||
|
|
||||||
// XNB mod
|
// XNB mod
|
||||||
if (files.All(this.IsPotentialXnbFile))
|
if (this.IsXnbMod(relevantFiles))
|
||||||
return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
|
return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
|
||||||
|
|
||||||
// SMAPI installer
|
// SMAPI installer
|
||||||
if (files.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on Mac.command" || p.Name == "install on Windows.bat"))
|
if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on Mac.command" || p.Name == "install on Windows.bat"))
|
||||||
return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file).");
|
return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file).");
|
||||||
|
|
||||||
// not a mod?
|
// not a mod?
|
||||||
|
@ -270,13 +284,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
return subfolders.Any() && !files.Any();
|
return subfolders.Any() && !files.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Recursively get all relevant files in a folder based on the result of <see cref="IsRelevant"/>.</summary>
|
/// <summary>Recursively get all files in a folder.</summary>
|
||||||
/// <param name="folder">The root folder to search.</param>
|
/// <param name="folder">The root folder to search.</param>
|
||||||
private IEnumerable<FileInfo> RecursivelyGetRelevantFiles(DirectoryInfo folder)
|
private IEnumerable<FileInfo> RecursivelyGetFiles(DirectoryInfo folder)
|
||||||
{
|
{
|
||||||
foreach (FileSystemInfo entry in folder.GetFileSystemInfos())
|
foreach (FileSystemInfo entry in folder.GetFileSystemInfos())
|
||||||
{
|
{
|
||||||
if (!this.IsRelevant(entry))
|
if (entry is DirectoryInfo && !this.IsRelevant(entry))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (entry is FileInfo file)
|
if (entry is FileInfo file)
|
||||||
|
@ -284,7 +298,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
|
|
||||||
if (entry is DirectoryInfo subfolder)
|
if (entry is DirectoryInfo subfolder)
|
||||||
{
|
{
|
||||||
foreach (FileInfo subfolderFile in this.RecursivelyGetRelevantFiles(subfolder))
|
foreach (FileInfo subfolderFile in this.RecursivelyGetFiles(subfolder))
|
||||||
yield return subfolderFile;
|
yield return subfolderFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,14 +316,46 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
return !this.IgnoreFilesystemNames.Any(p => p.IsMatch(entry.Name));
|
return !this.IgnoreFilesystemNames.Any(p => p.IsMatch(entry.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get whether a file is potentially part of an XNB mod.</summary>
|
/// <summary>Get whether a set of files looks like an XNB mod.</summary>
|
||||||
/// <param name="entry">The file.</param>
|
/// <param name="files">The files in the mod.</param>
|
||||||
private bool IsPotentialXnbFile(FileInfo entry)
|
private bool IsXnbMod(IEnumerable<FileInfo> files)
|
||||||
{
|
{
|
||||||
if (!this.IsRelevant(entry))
|
bool hasXnbFile = false;
|
||||||
return true;
|
|
||||||
|
|
||||||
return this.PotentialXnbModExtensions.Contains(entry.Extension); // use EndsWith to handle cases like image..png
|
foreach (FileInfo file in files.Where(this.IsRelevant))
|
||||||
|
{
|
||||||
|
if (this.StrictXnbModExtensions.Contains(file.Extension))
|
||||||
|
{
|
||||||
|
hasXnbFile = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.PotentialXnbModExtensions.Contains(file.Extension))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasXnbFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get whether a set of files looks like an XNB mod.</summary>
|
||||||
|
/// <param name="files">The files in the mod.</param>
|
||||||
|
private bool IsEmptyVortexFolder(IEnumerable<FileInfo> files)
|
||||||
|
{
|
||||||
|
bool hasVortexMarker = false;
|
||||||
|
|
||||||
|
foreach (FileInfo file in files)
|
||||||
|
{
|
||||||
|
if (file.Name == this.VortexMarkerFileName)
|
||||||
|
{
|
||||||
|
hasVortexMarker = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.IsRelevant(file) && file.Name != this.ConfigFileName)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasVortexMarker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Strip newlines from a string.</summary>
|
/// <summary>Strip newlines from a string.</summary>
|
||||||
|
|
|
@ -293,12 +293,12 @@ namespace StardewModdingAPI.Toolkit
|
||||||
return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherPlatformRelease, otherTag).ToString(), StringComparison.OrdinalIgnoreCase);
|
return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherPlatformRelease, otherTag).ToString(), StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CompareToRaw() switch
|
int result = CompareToRaw();
|
||||||
{
|
if (result < 0)
|
||||||
(< 0) => curOlder,
|
return curOlder;
|
||||||
(> 0) => curNewer,
|
if (result > 0)
|
||||||
_ => same
|
return curNewer;
|
||||||
};
|
return same;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Assert that the current version is valid.</summary>
|
/// <summary>Assert that the current version is valid.</summary>
|
||||||
|
|
|
@ -18,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
|
||||||
** Accessors
|
** Accessors
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Get whether this converter can read JSON.</summary>
|
/// <summary>Get whether this converter can read JSON.</summary>
|
||||||
public override bool CanRead => true;
|
public override bool CanRead { get; } = true;
|
||||||
|
|
||||||
/// <summary>Get whether this converter can write JSON.</summary>
|
/// <summary>Get whether this converter can write JSON.</summary>
|
||||||
public override bool CanWrite => true;
|
public override bool CanWrite { get; } = true;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$",
|
"pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)$",
|
||||||
"@errorMessages": {
|
"@errorMessages": {
|
||||||
"pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info."
|
"pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info."
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Installer", "SMAPI.Installer\SMAPI.Installer.csproj", "{0A9BB24F-15FF-4C26-B1A2-81F7AE316518}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Installer", "SMAPI.Installer\SMAPI.Installer.csproj", "{0A9BB24F-15FF-4C26-B1A2-81F7AE316518}"
|
||||||
ProjectSection(ProjectDependencies) = postProject
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}
|
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}
|
||||||
|
{491E775B-EAD0-44D4-B6CA-F1FC3E316D33} = {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}
|
||||||
{CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {CD53AD6F-97F4-4872-A212-50C2A0FD3601}
|
{CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {CD53AD6F-97F4-4872-A212-50C2A0FD3601}
|
||||||
{E6DA2198-7686-4F1D-B312-4A4DC70884C0} = {E6DA2198-7686-4F1D-B312-4A4DC70884C0}
|
{E6DA2198-7686-4F1D-B312-4A4DC70884C0} = {E6DA2198-7686-4F1D-B312-4A4DC70884C0}
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
|
@ -71,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyz
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\SMAPI.Mods.ConsoleCommands.csproj", "{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\SMAPI.Mods.ConsoleCommands.csproj", "{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ErrorHandler", "SMAPI.Mods.ErrorHandler\SMAPI.Mods.ErrorHandler.csproj", "{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}"
|
||||||
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\SMAPI.Mods.SaveBackup.csproj", "{CD53AD6F-97F4-4872-A212-50C2A0FD3601}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\SMAPI.Mods.SaveBackup.csproj", "{CD53AD6F-97F4-4872-A212-50C2A0FD3601}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit", "SMAPI.Toolkit\SMAPI.Toolkit.csproj", "{08184F74-60AD-4EEE-A78C-F4A35ADE6246}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit", "SMAPI.Toolkit\SMAPI.Toolkit.csproj", "{08184F74-60AD-4EEE-A78C-F4A35ADE6246}"
|
||||||
|
@ -83,6 +86,7 @@ Global
|
||||||
GlobalSection(SharedMSBuildProjectFiles) = preSolution
|
GlobalSection(SharedMSBuildProjectFiles) = preSolution
|
||||||
SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5
|
SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5
|
||||||
SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5
|
SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5
|
||||||
|
SMAPI.Internal\SMAPI.Internal.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5
|
||||||
SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5
|
SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5
|
||||||
SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13
|
SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13
|
||||||
SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5
|
SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5
|
||||||
|
@ -121,6 +125,10 @@ Global
|
||||||
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
@ -151,6 +159,7 @@ Global
|
||||||
{680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
|
{680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
|
||||||
{AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
|
{AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
|
||||||
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5}
|
{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5}
|
||||||
|
{491E775B-EAD0-44D4-B6CA-F1FC3E316D33} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5}
|
||||||
{CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5}
|
{CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
|
|
@ -39,6 +39,8 @@
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Hangfire/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Hangfire/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=initializers/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=initializers/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Junimo/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Junimo/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Keybind/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=keybinds/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=modder/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=modder/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=modders/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=modders/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mongo/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mongo/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|
|
@ -54,10 +54,10 @@ namespace StardewModdingAPI
|
||||||
** Public
|
** Public
|
||||||
****/
|
****/
|
||||||
/// <summary>SMAPI's current semantic version.</summary>
|
/// <summary>SMAPI's current semantic version.</summary>
|
||||||
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.4");
|
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.0");
|
||||||
|
|
||||||
/// <summary>The minimum supported version of Stardew Valley.</summary>
|
/// <summary>The minimum supported version of Stardew Valley.</summary>
|
||||||
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.3");
|
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4");
|
||||||
|
|
||||||
/// <summary>The maximum supported version of Stardew Valley.</summary>
|
/// <summary>The maximum supported version of Stardew Valley.</summary>
|
||||||
public static ISemanticVersion MaximumGameVersion { get; } = null;
|
public static ISemanticVersion MaximumGameVersion { get; } = null;
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using StardewModdingAPI.Framework.Input;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Events
|
||||||
|
{
|
||||||
|
/// <summary>Event arguments when any buttons were pressed or released.</summary>
|
||||||
|
public class ButtonsChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>The buttons that were pressed, held, or released since the previous tick.</summary>
|
||||||
|
private readonly Lazy<Dictionary<SButtonState, SButton[]>> ButtonsByState;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The current cursor position.</summary>
|
||||||
|
public ICursorPosition Cursor { get; }
|
||||||
|
|
||||||
|
/// <summary>The buttons which were pressed since the previous tick.</summary>
|
||||||
|
public IEnumerable<SButton> Pressed => this.ButtonsByState.Value[SButtonState.Pressed];
|
||||||
|
|
||||||
|
/// <summary>The buttons which were held since the previous tick.</summary>
|
||||||
|
public IEnumerable<SButton> Held => this.ButtonsByState.Value[SButtonState.Held];
|
||||||
|
|
||||||
|
/// <summary>The buttons which were released since the previous tick.</summary>
|
||||||
|
public IEnumerable<SButton> Released => this.ButtonsByState.Value[SButtonState.Released];
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="cursor">The cursor position.</param>
|
||||||
|
/// <param name="inputState">The game's current input state.</param>
|
||||||
|
internal ButtonsChangedEventArgs(ICursorPosition cursor, SInputState inputState)
|
||||||
|
{
|
||||||
|
this.Cursor = cursor;
|
||||||
|
this.ButtonsByState = new Lazy<Dictionary<SButtonState, SButton[]>>(() => this.GetButtonsByState(inputState));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get the buttons that were pressed, held, or released since the previous tick.</summary>
|
||||||
|
/// <param name="inputState">The game's current input state.</param>
|
||||||
|
private Dictionary<SButtonState, SButton[]> GetButtonsByState(SInputState inputState)
|
||||||
|
{
|
||||||
|
Dictionary<SButtonState, SButton[]> lookup = inputState.ButtonStates
|
||||||
|
.GroupBy(p => p.Value)
|
||||||
|
.ToDictionary(p => p.Key, p => p.Select(p => p.Key).ToArray());
|
||||||
|
|
||||||
|
foreach (var state in new[] { SButtonState.Pressed, SButtonState.Held, SButtonState.Released })
|
||||||
|
{
|
||||||
|
if (!lookup.ContainsKey(state))
|
||||||
|
lookup[state] = new SButton[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,9 @@ namespace StardewModdingAPI.Events
|
||||||
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
|
/// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
|
||||||
public interface IInputEvents
|
public interface IInputEvents
|
||||||
{
|
{
|
||||||
|
/// <summary>Raised after the player presses or releases any buttons on the keyboard, controller, or mouse.</summary>
|
||||||
|
event EventHandler<ButtonsChangedEventArgs> ButtonsChanged;
|
||||||
|
|
||||||
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
|
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
|
||||||
event EventHandler<ButtonPressedEventArgs> ButtonPressed;
|
event EventHandler<ButtonPressedEventArgs> ButtonPressed;
|
||||||
|
|
||||||
|
|
|
@ -414,7 +414,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
|
|
||||||
int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id);
|
int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id);
|
||||||
string reason = loadedIndex != -1
|
string reason = loadedIndex != -1
|
||||||
? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewcommunitywiki.com/Modding:Maps#Tilesheet_order for help."
|
? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."
|
||||||
: $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes.";
|
: $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes.";
|
||||||
|
|
||||||
SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval);
|
SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval);
|
||||||
|
|
|
@ -127,7 +127,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
if (asset is Map map)
|
if (asset is Map map)
|
||||||
{
|
{
|
||||||
map.assetPath = assetName;
|
map.assetPath = assetName;
|
||||||
this.FixTilesheetPaths(map, relativeMapPath: assetName);
|
this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -168,7 +168,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
FormatManager formatManager = FormatManager.Instance;
|
FormatManager formatManager = FormatManager.Instance;
|
||||||
Map map = formatManager.LoadMap(file.FullName);
|
Map map = formatManager.LoadMap(file.FullName);
|
||||||
map.assetPath = assetName;
|
map.assetPath = assetName;
|
||||||
this.FixTilesheetPaths(map, relativeMapPath: assetName);
|
this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: false);
|
||||||
asset = (T)(object)map;
|
asset = (T)(object)map;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -260,8 +260,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
|
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
|
||||||
/// <param name="map">The map whose tilesheets to fix.</param>
|
/// <param name="map">The map whose tilesheets to fix.</param>
|
||||||
/// <param name="relativeMapPath">The relative map path within the mod folder.</param>
|
/// <param name="relativeMapPath">The relative map path within the mod folder.</param>
|
||||||
|
/// <param name="fixEagerPathPrefixes">Whether to undo the game's eager tilesheet path prefixing for maps loaded from an <c>.xnb</c> file, which incorrectly prefixes tilesheet paths with the map's local asset key folder.</param>
|
||||||
/// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception>
|
/// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception>
|
||||||
private void FixTilesheetPaths(Map map, string relativeMapPath)
|
private void FixTilesheetPaths(Map map, string relativeMapPath, bool fixEagerPathPrefixes)
|
||||||
{
|
{
|
||||||
// get map info
|
// get map info
|
||||||
relativeMapPath = this.AssertAndNormalizeAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators
|
relativeMapPath = this.AssertAndNormalizeAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators
|
||||||
|
@ -270,12 +271,16 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
// fix tilesheets
|
// fix tilesheets
|
||||||
foreach (TileSheet tilesheet in map.TileSheets)
|
foreach (TileSheet tilesheet in map.TileSheets)
|
||||||
{
|
{
|
||||||
|
// get image source
|
||||||
tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource);
|
tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource);
|
||||||
|
|
||||||
string imageSource = tilesheet.ImageSource;
|
string imageSource = tilesheet.ImageSource;
|
||||||
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
|
|
||||||
|
// reverse incorrect eager tilesheet path prefixing
|
||||||
|
if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource.StartsWith(relativeMapFolder))
|
||||||
|
imageSource = imageSource.Substring(relativeMapFolder.Length + 1);
|
||||||
|
|
||||||
// validate tilesheet path
|
// validate tilesheet path
|
||||||
|
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
|
||||||
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
|
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
|
||||||
throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
|
throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,9 @@ namespace StardewModdingAPI.Framework.Events
|
||||||
/****
|
/****
|
||||||
** Input
|
** Input
|
||||||
****/
|
****/
|
||||||
|
/// <summary>Raised after the player presses or releases any buttons on the keyboard, controller, or mouse.</summary>
|
||||||
|
public readonly ManagedEvent<ButtonsChangedEventArgs> ButtonsChanged;
|
||||||
|
|
||||||
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
|
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
|
||||||
public readonly ManagedEvent<ButtonPressedEventArgs> ButtonPressed;
|
public readonly ManagedEvent<ButtonPressedEventArgs> ButtonPressed;
|
||||||
|
|
||||||
|
@ -212,6 +215,7 @@ namespace StardewModdingAPI.Framework.Events
|
||||||
this.TimeChanged = ManageEventOf<TimeChangedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged));
|
this.TimeChanged = ManageEventOf<TimeChangedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged));
|
||||||
this.ReturnedToTitle = ManageEventOf<ReturnedToTitleEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle));
|
this.ReturnedToTitle = ManageEventOf<ReturnedToTitleEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle));
|
||||||
|
|
||||||
|
this.ButtonsChanged = ManageEventOf<ButtonsChangedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonsChanged));
|
||||||
this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed));
|
this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed));
|
||||||
this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased));
|
this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased));
|
||||||
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true);
|
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true);
|
||||||
|
|
|
@ -9,6 +9,13 @@ namespace StardewModdingAPI.Framework.Events
|
||||||
/*********
|
/*********
|
||||||
** Accessors
|
** Accessors
|
||||||
*********/
|
*********/
|
||||||
|
/// <summary>Raised after the player presses or releases any buttons on the keyboard, controller, or mouse.</summary>
|
||||||
|
public event EventHandler<ButtonsChangedEventArgs> ButtonsChanged
|
||||||
|
{
|
||||||
|
add => this.EventManager.ButtonsChanged.Add(value, this.Mod);
|
||||||
|
remove => this.EventManager.ButtonsChanged.Remove(value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
|
/// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
|
||||||
public event EventHandler<ButtonPressedEventArgs> ButtonPressed
|
public event EventHandler<ButtonPressedEventArgs> ButtonPressed
|
||||||
{
|
{
|
||||||
|
|
|
@ -286,12 +286,15 @@ namespace StardewModdingAPI.Framework.Logging
|
||||||
/// <summary>Log details for settings that don't match the default.</summary>
|
/// <summary>Log details for settings that don't match the default.</summary>
|
||||||
/// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
|
/// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
|
||||||
/// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param>
|
/// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param>
|
||||||
public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates)
|
/// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param>
|
||||||
|
public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates, bool rewriteMods)
|
||||||
{
|
{
|
||||||
if (isDeveloperMode)
|
if (isDeveloperMode)
|
||||||
this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
|
this.Monitor.Log("You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI.", LogLevel.Info);
|
||||||
if (!checkForUpdates)
|
if (!checkForUpdates)
|
||||||
this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn);
|
this.Monitor.Log("You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI.", LogLevel.Warn);
|
||||||
|
if (!rewriteMods)
|
||||||
|
this.Monitor.Log("You configured SMAPI to not rewrite broken mods. Many older mods may fail to load. You can undo this by reinstalling SMAPI.", LogLevel.Warn);
|
||||||
if (!this.Monitor.WriteToConsole)
|
if (!this.Monitor.WriteToConsole)
|
||||||
this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
|
this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
|
||||||
this.Monitor.VerboseLog("Verbose logging enabled.");
|
this.Monitor.VerboseLog("Verbose logging enabled.");
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using StardewModdingAPI.Framework.Input;
|
using StardewModdingAPI.Framework.Input;
|
||||||
|
using StardewModdingAPI.Utilities;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework.ModHelpers
|
namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
{
|
{
|
||||||
|
@ -49,6 +50,19 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
this.CurrentInputState().OverrideButton(button, setDown: false);
|
this.CurrentInputState().OverrideButton(button, setDown: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SuppressActiveKeybinds(KeybindList keybindList)
|
||||||
|
{
|
||||||
|
foreach (Keybind keybind in keybindList.Keybinds)
|
||||||
|
{
|
||||||
|
if (!keybind.GetState().IsDown())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (SButton button in keybind.Buttons)
|
||||||
|
this.Suppress(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public SButtonState GetState(SButton button)
|
public SButtonState GetState(SButton button)
|
||||||
{
|
{
|
||||||
|
|
|
@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <summary>The objects to dispose as part of this instance.</summary>
|
/// <summary>The objects to dispose as part of this instance.</summary>
|
||||||
private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>();
|
private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>();
|
||||||
|
|
||||||
|
/// <summary>Whether to rewrite mods for compatibility.</summary>
|
||||||
|
private readonly bool RewriteMods;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
@ -45,10 +48,12 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <param name="targetPlatform">The current game platform.</param>
|
/// <param name="targetPlatform">The current game platform.</param>
|
||||||
/// <param name="monitor">Encapsulates monitoring and logging.</param>
|
/// <param name="monitor">Encapsulates monitoring and logging.</param>
|
||||||
/// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
|
/// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
|
||||||
public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode)
|
/// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param>
|
||||||
|
public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods)
|
||||||
{
|
{
|
||||||
this.Monitor = monitor;
|
this.Monitor = monitor;
|
||||||
this.ParanoidMode = paranoidMode;
|
this.ParanoidMode = paranoidMode;
|
||||||
|
this.RewriteMods = rewriteMods;
|
||||||
this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform));
|
this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform));
|
||||||
|
|
||||||
// init resolver
|
// init resolver
|
||||||
|
@ -308,7 +313,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
// find or rewrite code
|
// find or rewrite code
|
||||||
IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged).ToArray();
|
IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged, this.RewriteMods).ToArray();
|
||||||
RecursiveRewriter rewriter = new RecursiveRewriter(
|
RecursiveRewriter rewriter = new RecursiveRewriter(
|
||||||
module: module,
|
module: module,
|
||||||
rewriteType: (type, replaceWith) =>
|
rewriteType: (type, replaceWith) =>
|
||||||
|
|
|
@ -20,13 +20,15 @@ namespace StardewModdingAPI.Framework.Models
|
||||||
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
|
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
|
||||||
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
|
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
|
||||||
[nameof(VerboseLogging)] = false,
|
[nameof(VerboseLogging)] = false,
|
||||||
[nameof(LogNetworkTraffic)] = false
|
[nameof(LogNetworkTraffic)] = false,
|
||||||
|
[nameof(RewriteMods)] = true
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
|
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
|
||||||
private static readonly HashSet<string> DefaultSuppressUpdateChecks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> DefaultSuppressUpdateChecks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"SMAPI.ConsoleCommands",
|
"SMAPI.ConsoleCommands",
|
||||||
|
"SMAPI.ErrorHandler",
|
||||||
"SMAPI.SaveBackup"
|
"SMAPI.SaveBackup"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,6 +57,9 @@ namespace StardewModdingAPI.Framework.Models
|
||||||
/// <summary>Whether SMAPI should log more information about the game context.</summary>
|
/// <summary>Whether SMAPI should log more information about the game context.</summary>
|
||||||
public bool VerboseLogging { get; set; }
|
public bool VerboseLogging { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Whether SMAPI should rewrite mods for compatibility.</summary>
|
||||||
|
public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
|
||||||
|
|
||||||
/// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary>
|
/// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary>
|
||||||
public bool LogNetworkTraffic { get; set; }
|
public bool LogNetworkTraffic { get; set; }
|
||||||
|
|
||||||
|
@ -68,7 +73,7 @@ namespace StardewModdingAPI.Framework.Models
|
||||||
/********
|
/********
|
||||||
** Public methods
|
** Public methods
|
||||||
********/
|
********/
|
||||||
/// <summary>Get the settings which have been customised by the player.</summary>
|
/// <summary>Get the settings which have been customized by the player.</summary>
|
||||||
public IDictionary<string, object> GetCustomSettings()
|
public IDictionary<string, object> GetCustomSettings()
|
||||||
{
|
{
|
||||||
IDictionary<string, object> custom = new Dictionary<string, object>();
|
IDictionary<string, object> custom = new Dictionary<string, object>();
|
||||||
|
|
|
@ -24,9 +24,15 @@ namespace StardewModdingAPI.Framework.Networking
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsHost { get; }
|
public bool IsHost { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsSplitScreen => this.ScreenID != null;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool HasSmapi => this.ApiVersion != null;
|
public bool HasSmapi => this.ApiVersion != null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int? ScreenID { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public GamePlatform? Platform { get; }
|
public GamePlatform? Platform { get; }
|
||||||
|
|
||||||
|
@ -45,12 +51,14 @@ namespace StardewModdingAPI.Framework.Networking
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="playerID">The player's unique ID.</param>
|
/// <param name="playerID">The player's unique ID.</param>
|
||||||
|
/// <param name="screenID">The player's screen ID, if applicable.</param>
|
||||||
/// <param name="model">The metadata to copy.</param>
|
/// <param name="model">The metadata to copy.</param>
|
||||||
/// <param name="sendMessage">A method which sends a message to the peer.</param>
|
/// <param name="sendMessage">A method which sends a message to the peer.</param>
|
||||||
/// <param name="isHost">Whether this is a connection to the host player.</param>
|
/// <param name="isHost">Whether this is a connection to the host player.</param>
|
||||||
public MultiplayerPeer(long playerID, RemoteContextModel model, Action<OutgoingMessage> sendMessage, bool isHost)
|
public MultiplayerPeer(long playerID, int? screenID, RemoteContextModel model, Action<OutgoingMessage> sendMessage, bool isHost)
|
||||||
{
|
{
|
||||||
this.PlayerID = playerID;
|
this.PlayerID = playerID;
|
||||||
|
this.ScreenID = screenID;
|
||||||
this.IsHost = isHost;
|
this.IsHost = isHost;
|
||||||
if (model != null)
|
if (model != null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
using StardewValley;
|
||||||
|
using StardewValley.Menus;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework
|
||||||
|
{
|
||||||
|
/// <summary>SMAPI's implementation of the chatbox which intercepts errors for logging.</summary>
|
||||||
|
internal class SChatBox : ChatBox
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>Encapsulates monitoring and logging.</summary>
|
||||||
|
private readonly IMonitor Monitor;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="monitor">Encapsulates monitoring and logging.</param>
|
||||||
|
public SChatBox(IMonitor monitor)
|
||||||
|
{
|
||||||
|
this.Monitor = monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void runCommand(string command)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"> chat command: {command}");
|
||||||
|
base.runCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void receiveChatMessage(long sourceFarmer, int chatKind, LocalizedContentManager.LanguageCode language, string message)
|
||||||
|
{
|
||||||
|
if (chatKind == ChatBox.errorMessage)
|
||||||
|
{
|
||||||
|
// log error
|
||||||
|
this.Monitor.Log(message, LogLevel.Error);
|
||||||
|
|
||||||
|
// add event details if applicable
|
||||||
|
if (Game1.CurrentEvent != null && message.StartsWith("Event script error:"))
|
||||||
|
this.Monitor.Log($"In event #{Game1.CurrentEvent.id} for location {Game1.currentLocation?.NameOrUniqueName}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.receiveChatMessage(sourceFarmer, chatKind, language, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -124,9 +124,6 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary>
|
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary>
|
||||||
private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
|
private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
|
||||||
|
|
||||||
/// <summary>Whether custom content was removed from the save data to avoid a crash.</summary>
|
|
||||||
private bool IsSaveContentRemoved;
|
|
||||||
|
|
||||||
/// <summary>Asset interceptors added or removed since the last tick.</summary>
|
/// <summary>Asset interceptors added or removed since the last tick.</summary>
|
||||||
private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>();
|
private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>();
|
||||||
|
|
||||||
|
@ -135,7 +132,7 @@ namespace StardewModdingAPI.Framework
|
||||||
private readonly ConcurrentQueue<string> RawCommandQueue = new ConcurrentQueue<string>();
|
private readonly ConcurrentQueue<string> RawCommandQueue = new ConcurrentQueue<string>();
|
||||||
|
|
||||||
/// <summary>A list of commands to execute on each screen.</summary>
|
/// <summary>A list of commands to execute on each screen.</summary>
|
||||||
private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new(() => new());
|
private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new PerScreen<List<Tuple<Command, string, string[]>>>(() => new List<Tuple<Command, string, string[]>>());
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
@ -145,6 +142,10 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks>
|
/// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks>
|
||||||
internal static DeprecationManager DeprecationManager { get; private set; }
|
internal static DeprecationManager DeprecationManager { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>The singleton instance.</summary>
|
||||||
|
/// <remarks>This is only intended for use by external code like the Error Handler mod.</remarks>
|
||||||
|
internal static SCore Instance { get; private set; }
|
||||||
|
|
||||||
/// <summary>The number of update ticks which have already executed. This is similar to <see cref="Game1.ticks"/>, but incremented more consistently for every tick.</summary>
|
/// <summary>The number of update ticks which have already executed. This is similar to <see cref="Game1.ticks"/>, but incremented more consistently for every tick.</summary>
|
||||||
internal static uint TicksElapsed { get; private set; }
|
internal static uint TicksElapsed { get; private set; }
|
||||||
|
|
||||||
|
@ -157,6 +158,8 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
|
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
|
||||||
public SCore(string modsPath, bool writeToConsole)
|
public SCore(string modsPath, bool writeToConsole)
|
||||||
{
|
{
|
||||||
|
SCore.Instance = this;
|
||||||
|
|
||||||
// init paths
|
// init paths
|
||||||
this.VerifyPath(modsPath);
|
this.VerifyPath(modsPath);
|
||||||
this.VerifyPath(Constants.LogDir);
|
this.VerifyPath(Constants.LogDir);
|
||||||
|
@ -205,6 +208,7 @@ namespace StardewModdingAPI.Framework
|
||||||
{
|
{
|
||||||
JsonConverter[] converters = {
|
JsonConverter[] converters = {
|
||||||
new ColorConverter(),
|
new ColorConverter(),
|
||||||
|
new KeybindConverter(),
|
||||||
new PointConverter(),
|
new PointConverter(),
|
||||||
new Vector2Converter(),
|
new Vector2Converter(),
|
||||||
new RectangleConverter()
|
new RectangleConverter()
|
||||||
|
@ -245,12 +249,7 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
// apply game patches
|
// apply game patches
|
||||||
new GamePatcher(this.Monitor).Apply(
|
new GamePatcher(this.Monitor).Apply(
|
||||||
new EventErrorPatch(this.LogManager.MonitorForGame),
|
new LoadContextPatch(this.Reflection, this.OnLoadStageChanged)
|
||||||
new DialogueErrorPatch(this.LogManager.MonitorForGame, this.Reflection),
|
|
||||||
new ObjectErrorPatch(),
|
|
||||||
new LoadContextPatch(this.Reflection, this.OnLoadStageChanged),
|
|
||||||
new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved),
|
|
||||||
new ScheduleErrorPatch(this.LogManager.MonitorForGame)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// add exit handler
|
// add exit handler
|
||||||
|
@ -278,7 +277,7 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
// log basic info
|
// log basic info
|
||||||
this.LogManager.HandleMarkerFiles();
|
this.LogManager.HandleMarkerFiles();
|
||||||
this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates);
|
this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates, this.Settings.RewriteMods);
|
||||||
|
|
||||||
// set window titles
|
// set window titles
|
||||||
this.SetWindowTitles(
|
this.SetWindowTitles(
|
||||||
|
@ -517,15 +516,6 @@ namespace StardewModdingAPI.Framework
|
||||||
this.ScreenCommandQueue.GetValueForScreen(screenId).Add(Tuple.Create(command, name, args));
|
this.ScreenCommandQueue.GetValueForScreen(screenId).Add(Tuple.Create(command, name, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*********
|
|
||||||
** Show in-game warnings (for main player only)
|
|
||||||
*********/
|
|
||||||
// save content removed
|
|
||||||
if (this.IsSaveContentRemoved && Context.IsWorldReady)
|
|
||||||
{
|
|
||||||
this.IsSaveContentRemoved = false;
|
|
||||||
Game1.addHUDMessage(new HUDMessage(this.Translator.Get("warn.invalid-content-removed"), HUDMessage.error_type));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Run game update
|
** Run game update
|
||||||
|
@ -827,6 +817,10 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
|
|
||||||
// raise input button events
|
// raise input button events
|
||||||
|
if (inputState.ButtonStates.Count > 0)
|
||||||
|
{
|
||||||
|
events.ButtonsChanged.Raise(new ButtonsChangedEventArgs(cursor, inputState));
|
||||||
|
|
||||||
foreach (var pair in inputState.ButtonStates)
|
foreach (var pair in inputState.ButtonStates)
|
||||||
{
|
{
|
||||||
SButton button = pair.Key;
|
SButton button = pair.Key;
|
||||||
|
@ -849,6 +843,7 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Menu events
|
** Menu events
|
||||||
|
@ -1065,6 +1060,13 @@ namespace StardewModdingAPI.Framework
|
||||||
this.OnReturnedToTitle();
|
this.OnReturnedToTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// override chatbox
|
||||||
|
if (newStage == LoadStage.Loaded)
|
||||||
|
{
|
||||||
|
Game1.onScreenMenus.Remove(Game1.chatBox);
|
||||||
|
Game1.onScreenMenus.Add(Game1.chatBox = new SChatBox(this.LogManager.MonitorForGame));
|
||||||
|
}
|
||||||
|
|
||||||
// raise events
|
// raise events
|
||||||
this.EventManager.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage));
|
this.EventManager.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage));
|
||||||
if (newStage == LoadStage.None)
|
if (newStage == LoadStage.None)
|
||||||
|
@ -1105,12 +1107,6 @@ namespace StardewModdingAPI.Framework
|
||||||
Game1.CustomData[migrationKey] = Constants.ApiVersion.ToString();
|
Game1.CustomData[migrationKey] = Constants.ApiVersion.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Raised after custom content is removed from the save data to avoid a crash.</summary>
|
|
||||||
internal void OnSaveContentRemoved()
|
|
||||||
{
|
|
||||||
this.IsSaveContentRemoved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary>
|
/// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary>
|
||||||
protected void OnNewDayAfterFade()
|
protected void OnNewDayAfterFade()
|
||||||
{
|
{
|
||||||
|
@ -1406,7 +1402,7 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
// load mods
|
// load mods
|
||||||
IList<IModMetadata> skippedMods = new List<IModMetadata>();
|
IList<IModMetadata> skippedMods = new List<IModMetadata>();
|
||||||
using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings))
|
using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
|
||||||
{
|
{
|
||||||
// init
|
// init
|
||||||
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
|
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
|
@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Input;
|
||||||
using StardewModdingAPI.Framework.Reflection;
|
using StardewModdingAPI.Framework.Reflection;
|
||||||
using StardewModdingAPI.Framework.StateTracking.Snapshots;
|
using StardewModdingAPI.Framework.StateTracking.Snapshots;
|
||||||
using StardewModdingAPI.Framework.Utilities;
|
using StardewModdingAPI.Framework.Utilities;
|
||||||
|
using StardewModdingAPI.Utilities;
|
||||||
using StardewValley;
|
using StardewValley;
|
||||||
using StardewValley.BellsAndWhistles;
|
using StardewValley.BellsAndWhistles;
|
||||||
using StardewValley.Locations;
|
using StardewValley.Locations;
|
||||||
|
@ -81,6 +82,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="IGameLoopEvents.SaveCreating"/>.</summary>
|
/// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="IGameLoopEvents.SaveCreating"/>.</summary>
|
||||||
public bool IsBetweenCreateEvents { get; set; }
|
public bool IsBetweenCreateEvents { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The cached <see cref="Farmer.UniqueMultiplayerID"/> value for this instance's player.</summary>
|
||||||
|
public long? PlayerId { get; private set; }
|
||||||
|
|
||||||
/// <summary>Construct a content manager to read game content files.</summary>
|
/// <summary>Construct a content manager to read game content files.</summary>
|
||||||
/// <remarks>This must be static because the game accesses it before the <see cref="SGame"/> constructor is called.</remarks>
|
/// <remarks>This must be static because the game accesses it before the <see cref="SGame"/> constructor is called.</remarks>
|
||||||
[NonInstancedStatic]
|
[NonInstancedStatic]
|
||||||
|
@ -121,6 +125,18 @@ namespace StardewModdingAPI.Framework
|
||||||
this.OnUpdating = onUpdating;
|
this.OnUpdating = onUpdating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the current input state for a button.</summary>
|
||||||
|
/// <param name="button">The button to check.</param>
|
||||||
|
/// <remarks>This is intended for use by <see cref="Keybind"/> and shouldn't be used directly in most cases.</remarks>
|
||||||
|
internal static SButtonState GetInputState(SButton button)
|
||||||
|
{
|
||||||
|
SInputState input = Game1.input as SInputState;
|
||||||
|
if (input == null)
|
||||||
|
throw new InvalidOperationException("SMAPI's input state is not in a ready state yet.");
|
||||||
|
|
||||||
|
return input.GetState(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Protected methods
|
** Protected methods
|
||||||
|
@ -167,6 +183,7 @@ namespace StardewModdingAPI.Framework
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.OnUpdating(this, gameTime, () => base.Update(gameTime));
|
this.OnUpdating(this, gameTime, () => base.Update(gameTime));
|
||||||
|
this.PlayerId = Game1.player?.UniqueMultiplayerID;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using StardewModdingAPI.Framework.Events;
|
using StardewModdingAPI.Framework.Events;
|
||||||
|
@ -46,6 +47,13 @@ namespace StardewModdingAPI.Framework
|
||||||
private readonly Action OnGameExiting;
|
private readonly Action OnGameExiting;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>The singleton instance.</summary>
|
||||||
|
public static SGameRunner Instance => (SGameRunner)GameRunner.instance;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
@ -99,15 +107,24 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void RemoveGameInstance(Game1 instance)
|
public override void RemoveGameInstance(Game1 gameInstance)
|
||||||
{
|
{
|
||||||
base.RemoveGameInstance(instance);
|
base.RemoveGameInstance(gameInstance);
|
||||||
|
|
||||||
if (this.gameInstances.Count <= 1)
|
if (this.gameInstances.Count <= 1)
|
||||||
EarlyConstants.LogScreenId = null;
|
EarlyConstants.LogScreenId = null;
|
||||||
this.UpdateForSplitScreenChanges();
|
this.UpdateForSplitScreenChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the screen ID for a given player ID, if the player is local.</summary>
|
||||||
|
/// <param name="playerId">The player ID to check.</param>
|
||||||
|
public int? GetScreenId(long playerId)
|
||||||
|
{
|
||||||
|
return this.gameInstances
|
||||||
|
.FirstOrDefault(p => ((SGame)p).PlayerId == playerId)
|
||||||
|
?.instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Protected methods
|
** Protected methods
|
||||||
|
@ -136,6 +153,7 @@ namespace StardewModdingAPI.Framework
|
||||||
this.OnGameUpdating(gameTime, () => base.Update(gameTime));
|
this.OnGameUpdating(gameTime, () => base.Update(gameTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Update metadata when a split screen is added or removed.</summary>
|
||||||
private void UpdateForSplitScreenChanges()
|
private void UpdateForSplitScreenChanges()
|
||||||
{
|
{
|
||||||
HashSet<int> oldScreenIds = new HashSet<int>(Context.ActiveScreenIds);
|
HashSet<int> oldScreenIds = new HashSet<int>(Context.ActiveScreenIds);
|
||||||
|
|
|
@ -56,10 +56,10 @@ namespace StardewModdingAPI.Framework
|
||||||
private readonly bool LogNetworkTraffic;
|
private readonly bool LogNetworkTraffic;
|
||||||
|
|
||||||
/// <summary>The backing field for <see cref="Peers"/>.</summary>
|
/// <summary>The backing field for <see cref="Peers"/>.</summary>
|
||||||
private readonly PerScreen<IDictionary<long, MultiplayerPeer>> PeersImpl = new(() => new Dictionary<long, MultiplayerPeer>());
|
private readonly PerScreen<IDictionary<long, MultiplayerPeer>> PeersImpl = new PerScreen<IDictionary<long, MultiplayerPeer>>(() => new Dictionary<long, MultiplayerPeer>());
|
||||||
|
|
||||||
/// <summary>The backing field for <see cref="HostPeer"/>.</summary>
|
/// <summary>The backing field for <see cref="HostPeer"/>.</summary>
|
||||||
private readonly PerScreen<MultiplayerPeer> HostPeerImpl = new();
|
private readonly PerScreen<MultiplayerPeer> HostPeerImpl = new PerScreen<MultiplayerPeer>();
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
@ -196,7 +196,13 @@ namespace StardewModdingAPI.Framework
|
||||||
this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
|
this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
|
||||||
|
|
||||||
// store peer
|
// store peer
|
||||||
MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false);
|
MultiplayerPeer newPeer = new MultiplayerPeer(
|
||||||
|
playerID: message.FarmerID,
|
||||||
|
screenID: this.GetScreenId(message.FarmerID),
|
||||||
|
model: model,
|
||||||
|
sendMessage: sendMessage,
|
||||||
|
isHost: false
|
||||||
|
);
|
||||||
if (this.Peers.ContainsKey(message.FarmerID))
|
if (this.Peers.ContainsKey(message.FarmerID))
|
||||||
{
|
{
|
||||||
this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info);
|
this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info);
|
||||||
|
@ -238,7 +244,13 @@ namespace StardewModdingAPI.Framework
|
||||||
if (!this.Peers.ContainsKey(message.FarmerID))
|
if (!this.Peers.ContainsKey(message.FarmerID))
|
||||||
{
|
{
|
||||||
this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace);
|
this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace);
|
||||||
MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false);
|
MultiplayerPeer peer = new MultiplayerPeer(
|
||||||
|
playerID: message.FarmerID,
|
||||||
|
screenID: this.GetScreenId(message.FarmerID),
|
||||||
|
model: null,
|
||||||
|
sendMessage: sendMessage,
|
||||||
|
isHost: false
|
||||||
|
);
|
||||||
this.AddPeer(peer, canBeHost: false);
|
this.AddPeer(peer, canBeHost: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +292,13 @@ namespace StardewModdingAPI.Framework
|
||||||
this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
|
this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
|
||||||
|
|
||||||
// store peer
|
// store peer
|
||||||
MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null);
|
MultiplayerPeer peer = new MultiplayerPeer(
|
||||||
|
playerID: message.FarmerID,
|
||||||
|
screenID: this.GetScreenId(message.FarmerID),
|
||||||
|
model: model,
|
||||||
|
sendMessage: sendMessage,
|
||||||
|
isHost: model?.IsHost ?? this.HostPeer == null
|
||||||
|
);
|
||||||
if (peer.IsHost && this.HostPeer != null)
|
if (peer.IsHost && this.HostPeer != null)
|
||||||
{
|
{
|
||||||
this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error);
|
this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error);
|
||||||
|
@ -297,7 +315,14 @@ namespace StardewModdingAPI.Framework
|
||||||
if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null)
|
if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null)
|
||||||
{
|
{
|
||||||
this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace);
|
this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace);
|
||||||
this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false);
|
var peer = new MultiplayerPeer(
|
||||||
|
playerID: message.FarmerID,
|
||||||
|
screenID: this.GetScreenId(message.FarmerID),
|
||||||
|
model: null,
|
||||||
|
sendMessage: sendMessage,
|
||||||
|
isHost: true
|
||||||
|
);
|
||||||
|
this.AddPeer(peer, canBeHost: false);
|
||||||
}
|
}
|
||||||
resume();
|
resume();
|
||||||
break;
|
break;
|
||||||
|
@ -309,7 +334,13 @@ namespace StardewModdingAPI.Framework
|
||||||
// store peer
|
// store peer
|
||||||
if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer))
|
if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer))
|
||||||
{
|
{
|
||||||
peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null);
|
peer = new MultiplayerPeer(
|
||||||
|
playerID: message.FarmerID,
|
||||||
|
screenID: this.GetScreenId(message.FarmerID),
|
||||||
|
model: null,
|
||||||
|
sendMessage: sendMessage,
|
||||||
|
isHost: this.HostPeer == null
|
||||||
|
);
|
||||||
this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace);
|
this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace);
|
||||||
this.AddPeer(peer, canBeHost: true);
|
this.AddPeer(peer, canBeHost: true);
|
||||||
}
|
}
|
||||||
|
@ -361,34 +392,24 @@ namespace StardewModdingAPI.Framework
|
||||||
if (string.IsNullOrWhiteSpace(fromModID))
|
if (string.IsNullOrWhiteSpace(fromModID))
|
||||||
throw new ArgumentNullException(nameof(fromModID));
|
throw new ArgumentNullException(nameof(fromModID));
|
||||||
|
|
||||||
// get target players
|
// get valid peers
|
||||||
long curPlayerId = Game1.player.UniqueMultiplayerID;
|
var sendToPeers = this.Peers.Values.Where(p => p.HasSmapi).ToList();
|
||||||
bool sendToSelf = false;
|
bool sendToSelf = true;
|
||||||
List<MultiplayerPeer> sendToPeers = new List<MultiplayerPeer>();
|
|
||||||
if (toPlayerIDs == null)
|
// filter by player ID
|
||||||
|
if (toPlayerIDs != null)
|
||||||
{
|
{
|
||||||
sendToSelf = true;
|
var ids = new HashSet<long>(toPlayerIDs);
|
||||||
sendToPeers.AddRange(this.Peers.Values);
|
sendToPeers.RemoveAll(peer => !ids.Contains(peer.PlayerID));
|
||||||
}
|
sendToSelf = ids.Contains(Game1.player.UniqueMultiplayerID);
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (long id in toPlayerIDs.Distinct())
|
|
||||||
{
|
|
||||||
if (id == curPlayerId)
|
|
||||||
sendToSelf = true;
|
|
||||||
else if (this.Peers.TryGetValue(id, out MultiplayerPeer peer) && peer.HasSmapi)
|
|
||||||
sendToPeers.Add(peer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter by mod ID
|
// filter by mod ID
|
||||||
if (toModIDs != null)
|
if (toModIDs != null)
|
||||||
{
|
{
|
||||||
HashSet<string> sendToMods = new HashSet<string>(toModIDs, StringComparer.OrdinalIgnoreCase);
|
var ids = new HashSet<string>(toModIDs, StringComparer.OrdinalIgnoreCase);
|
||||||
if (sendToSelf && toModIDs.All(id => this.ModRegistry.Get(id) == null))
|
sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !ids.Contains(mod.ID)));
|
||||||
sendToSelf = false;
|
sendToSelf = sendToSelf && toModIDs.Any(id => this.ModRegistry.Get(id) != null);
|
||||||
|
|
||||||
sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !sendToMods.Contains(mod.ID)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate recipients
|
// validate recipients
|
||||||
|
@ -505,6 +526,13 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the screen ID for a given player ID, if the player is local.</summary>
|
||||||
|
/// <param name="playerId">The player ID to check.</param>
|
||||||
|
private int? GetScreenId(long playerId)
|
||||||
|
{
|
||||||
|
return SGameRunner.Instance.GetScreenId(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Get all connected player IDs, including the current player.</summary>
|
/// <summary>Get all connected player IDs, including the current player.</summary>
|
||||||
private IEnumerable<long> GetKnownPlayerIDs()
|
private IEnumerable<long> GetKnownPlayerIDs()
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using StardewModdingAPI.Toolkit.Serialization;
|
||||||
|
using StardewModdingAPI.Utilities;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework.Serialization
|
||||||
|
{
|
||||||
|
/// <summary>Handles deserialization of <see cref="Keybind"/> and <see cref="KeybindList"/> models.</summary>
|
||||||
|
internal class KeybindConverter : JsonConverter
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanRead { get; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanWrite { get; } = true;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get whether this instance can convert the specified object type.</summary>
|
||||||
|
/// <param name="objectType">The object type.</param>
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
typeof(Keybind).IsAssignableFrom(objectType)
|
||||||
|
|| typeof(KeybindList).IsAssignableFrom(objectType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads the JSON representation of the object.</summary>
|
||||||
|
/// <param name="reader">The JSON reader.</param>
|
||||||
|
/// <param name="objectType">The object type.</param>
|
||||||
|
/// <param name="existingValue">The object being read.</param>
|
||||||
|
/// <param name="serializer">The calling serializer.</param>
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
string path = reader.Path;
|
||||||
|
|
||||||
|
switch (reader.TokenType)
|
||||||
|
{
|
||||||
|
case JsonToken.Null:
|
||||||
|
return objectType == typeof(Keybind)
|
||||||
|
? (object)new Keybind()
|
||||||
|
: new KeybindList();
|
||||||
|
|
||||||
|
case JsonToken.String:
|
||||||
|
{
|
||||||
|
string str = JToken.Load(reader).Value<string>();
|
||||||
|
|
||||||
|
if (objectType == typeof(Keybind))
|
||||||
|
{
|
||||||
|
return Keybind.TryParse(str, out Keybind parsed, out string[] errors)
|
||||||
|
? parsed
|
||||||
|
: throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors)
|
||||||
|
? parsed
|
||||||
|
: throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new SParseException($"Can't parse {objectType} from unexpected {reader.TokenType} node (path: {reader.Path}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes the JSON representation of the object.</summary>
|
||||||
|
/// <param name="writer">The JSON writer.</param>
|
||||||
|
/// <param name="value">The value.</param>
|
||||||
|
/// <param name="serializer">The calling serializer.</param>
|
||||||
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
writer.WriteValue(value?.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
using StardewModdingAPI.Utilities;
|
||||||
|
|
||||||
namespace StardewModdingAPI
|
namespace StardewModdingAPI
|
||||||
{
|
{
|
||||||
/// <summary>Provides an API for checking and changing input state.</summary>
|
/// <summary>Provides an API for checking and changing input state.</summary>
|
||||||
|
@ -18,6 +20,10 @@ namespace StardewModdingAPI
|
||||||
/// <param name="button">The button to suppress.</param>
|
/// <param name="button">The button to suppress.</param>
|
||||||
void Suppress(SButton button);
|
void Suppress(SButton button);
|
||||||
|
|
||||||
|
/// <summary>Suppress the keybinds which are currently down.</summary>
|
||||||
|
/// <param name="keybindList">The keybind list whose active keybinds to suppress.</param>
|
||||||
|
void SuppressActiveKeybinds(KeybindList keybindList);
|
||||||
|
|
||||||
/// <summary>Get the state of a button.</summary>
|
/// <summary>Get the state of a button.</summary>
|
||||||
/// <param name="button">The button to check.</param>
|
/// <param name="button">The button to check.</param>
|
||||||
SButtonState GetState(SButton button);
|
SButtonState GetState(SButton button);
|
||||||
|
|
|
@ -14,9 +14,16 @@ namespace StardewModdingAPI
|
||||||
/// <summary>Whether this is a connection to the host player.</summary>
|
/// <summary>Whether this is a connection to the host player.</summary>
|
||||||
bool IsHost { get; }
|
bool IsHost { get; }
|
||||||
|
|
||||||
|
/// <summary>Whether this a local player on the same computer in split-screen mote.</summary>
|
||||||
|
bool IsSplitScreen { get; }
|
||||||
|
|
||||||
/// <summary>Whether the player has SMAPI installed.</summary>
|
/// <summary>Whether the player has SMAPI installed.</summary>
|
||||||
bool HasSmapi { get; }
|
bool HasSmapi { get; }
|
||||||
|
|
||||||
|
/// <summary>The player's screen ID, if applicable.</summary>
|
||||||
|
/// <remarks>See <see cref="Context.ScreenId"/> for details. This is only visible to players in split-screen mode. A remote player won't see this value, even if the other players are in split-screen mode.</remarks>
|
||||||
|
int? ScreenID { get; }
|
||||||
|
|
||||||
/// <summary>The player's OS platform, if <see cref="HasSmapi"/> is true.</summary>
|
/// <summary>The player's OS platform, if <see cref="HasSmapi"/> is true.</summary>
|
||||||
GamePlatform? Platform { get; }
|
GamePlatform? Platform { get; }
|
||||||
|
|
||||||
|
|
|
@ -373,52 +373,6 @@ namespace StardewModdingAPI.Metadata
|
||||||
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
|
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
|
||||||
return this.ReloadSuspensionBridges(content, key);
|
return this.ReloadSuspensionBridges(content, key);
|
||||||
|
|
||||||
/****
|
|
||||||
** Content\TileSheets
|
|
||||||
****/
|
|
||||||
case "tilesheets\\chairtiles": // Game1.LoadContent
|
|
||||||
MapSeat.mapChairTexture = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\critters": // Critter constructor
|
|
||||||
return this.ReloadCritterTextures(content, key) > 0;
|
|
||||||
|
|
||||||
case "tilesheets\\crops": // Game1.LoadContent
|
|
||||||
Game1.cropSpriteSheet = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\debris": // Game1.LoadContent
|
|
||||||
Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\emotes": // Game1.LoadContent
|
|
||||||
Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\furniture": // Game1.LoadContent
|
|
||||||
Furniture.furnitureTexture = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\furniturefront": // Game1.LoadContent
|
|
||||||
Furniture.furnitureFrontTexture = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\projectiles": // Game1.LoadContent
|
|
||||||
Projectile.projectileSheet = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\rain": // Game1.LoadContent
|
|
||||||
Game1.rainTexture = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\tools": // Game1.ResetToolSpriteSheet
|
|
||||||
Game1.ResetToolSpriteSheet();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case "tilesheets\\weapons": // Game1.LoadContent
|
|
||||||
Tool.weaponsTexture = content.Load<Texture2D>(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Content\Maps
|
** Content\Maps
|
||||||
****/
|
****/
|
||||||
|
@ -454,6 +408,12 @@ namespace StardewModdingAPI.Metadata
|
||||||
case "minigames\\titlebuttons": // TitleMenu
|
case "minigames\\titlebuttons": // TitleMenu
|
||||||
return this.ReloadTitleButtons(content, key);
|
return this.ReloadTitleButtons(content, key);
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Content\Strings
|
||||||
|
****/
|
||||||
|
case "strings\\stringsfromcsfiles":
|
||||||
|
return this.ReloadStringsFromCsFiles(content);
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Content\TileSheets
|
** Content\TileSheets
|
||||||
****/
|
****/
|
||||||
|
@ -469,14 +429,57 @@ namespace StardewModdingAPI.Metadata
|
||||||
Bush.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
Bush.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\chairtiles": // Game1.LoadContent
|
||||||
|
MapSeat.mapChairTexture = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
case "tilesheets\\craftables": // Game1.LoadContent
|
case "tilesheets\\craftables": // Game1.LoadContent
|
||||||
Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
|
Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\critters": // Critter constructor
|
||||||
|
return this.ReloadCritterTextures(content, key) > 0;
|
||||||
|
|
||||||
|
case "tilesheets\\crops": // Game1.LoadContent
|
||||||
|
Game1.cropSpriteSheet = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\debris": // Game1.LoadContent
|
||||||
|
Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\emotes": // Game1.LoadContent
|
||||||
|
Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
case "tilesheets\\fruittrees": // FruitTree
|
case "tilesheets\\fruittrees": // FruitTree
|
||||||
FruitTree.texture = content.Load<Texture2D>(key);
|
FruitTree.texture = content.Load<Texture2D>(key);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\furniture": // Game1.LoadContent
|
||||||
|
Furniture.furnitureTexture = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\furniturefront": // Game1.LoadContent
|
||||||
|
Furniture.furnitureFrontTexture = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\projectiles": // Game1.LoadContent
|
||||||
|
Projectile.projectileSheet = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\rain": // Game1.LoadContent
|
||||||
|
Game1.rainTexture = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\tools": // Game1.ResetToolSpriteSheet
|
||||||
|
Game1.ResetToolSpriteSheet();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "tilesheets\\weapons": // Game1.LoadContent
|
||||||
|
Tool.weaponsTexture = content.Load<Texture2D>(key);
|
||||||
|
return true;
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Content\TerrainFeatures
|
** Content\TerrainFeatures
|
||||||
****/
|
****/
|
||||||
|
@ -528,6 +531,9 @@ namespace StardewModdingAPI.Metadata
|
||||||
return this.ReloadTreeTextures(content, key, Tree.pineTree);
|
return this.ReloadTreeTextures(content, key, Tree.pineTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Dynamic assets
|
||||||
|
****/
|
||||||
// dynamic textures
|
// dynamic textures
|
||||||
if (this.KeyStartsWith(key, "animals\\cat"))
|
if (this.KeyStartsWith(key, "animals\\cat"))
|
||||||
return this.ReloadPetOrHorseSprites<Cat>(content, key);
|
return this.ReloadPetOrHorseSprites<Cat>(content, key);
|
||||||
|
@ -778,26 +784,10 @@ namespace StardewModdingAPI.Metadata
|
||||||
/// <param name="location">The location whose map to reload.</param>
|
/// <param name="location">The location whose map to reload.</param>
|
||||||
private void ReloadMap(GameLocation location)
|
private void ReloadMap(GameLocation location)
|
||||||
{
|
{
|
||||||
// reset patch caches
|
// reload map
|
||||||
switch (location)
|
|
||||||
{
|
|
||||||
case Town _:
|
|
||||||
this.Reflection.GetField<bool>(location, "ccRefurbished").SetValue(false);
|
|
||||||
this.Reflection.GetField<bool>(location, "isShowingDestroyedJoja").SetValue(false);
|
|
||||||
this.Reflection.GetField<bool>(location, "isShowingUpgradedPamHouse").SetValue(false);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Beach _:
|
|
||||||
case BeachNightMarket _:
|
|
||||||
case Forest _:
|
|
||||||
this.Reflection.GetField<bool>(location, "hasShownCCUpgrade").SetValue(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// general updates
|
|
||||||
location.reloadMap();
|
location.reloadMap();
|
||||||
location.updateSeasonalTileSheets();
|
|
||||||
location.updateWarps();
|
location.updateWarps();
|
||||||
|
location.MakeMapModifications(force: true);
|
||||||
|
|
||||||
// update interior doors
|
// update interior doors
|
||||||
location.interiorDoors.Clear();
|
location.interiorDoors.Clear();
|
||||||
|
@ -1028,6 +1018,27 @@ namespace StardewModdingAPI.Metadata
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Reload cached translations from the <c>Strings\StringsFromCSFiles</c> asset.</summary>
|
||||||
|
/// <param name="content">The content manager through which to reload the asset.</param>
|
||||||
|
/// <returns>Returns whether any data was reloaded.</returns>
|
||||||
|
/// <remarks>Derived from the <see cref="Game1.TranslateFields"/>.</remarks>
|
||||||
|
private bool ReloadStringsFromCsFiles(LocalizedContentManager content)
|
||||||
|
{
|
||||||
|
Game1.samBandName = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2156");
|
||||||
|
Game1.elliottBookName = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2157");
|
||||||
|
|
||||||
|
string[] dayNames = this.Reflection.GetField<string[]>(typeof(Game1), "_shortDayDisplayName").GetValue();
|
||||||
|
dayNames[0] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3042");
|
||||||
|
dayNames[1] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3043");
|
||||||
|
dayNames[2] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3044");
|
||||||
|
dayNames[3] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3045");
|
||||||
|
dayNames[4] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3046");
|
||||||
|
dayNames[5] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3047");
|
||||||
|
dayNames[6] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3048");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Helpers
|
** Helpers
|
||||||
****/
|
****/
|
||||||
|
|
|
@ -27,12 +27,15 @@ namespace StardewModdingAPI.Metadata
|
||||||
/// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary>
|
/// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary>
|
||||||
/// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
|
/// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
|
||||||
/// <param name="platformChanged">Whether the assembly was rewritten for crossplatform compatibility.</param>
|
/// <param name="platformChanged">Whether the assembly was rewritten for crossplatform compatibility.</param>
|
||||||
public IEnumerable<IInstructionHandler> GetHandlers(bool paranoidMode, bool platformChanged)
|
/// <param name="rewriteMods">Whether to get handlers which rewrite mods for compatibility.</param>
|
||||||
|
public IEnumerable<IInstructionHandler> GetHandlers(bool paranoidMode, bool platformChanged, bool rewriteMods)
|
||||||
{
|
{
|
||||||
/****
|
/****
|
||||||
** rewrite CIL to fix incompatible code
|
** rewrite CIL to fix incompatible code
|
||||||
****/
|
****/
|
||||||
// rewrite for crossplatform compatibility
|
// rewrite for crossplatform compatibility
|
||||||
|
if (rewriteMods)
|
||||||
|
{
|
||||||
if (platformChanged)
|
if (platformChanged)
|
||||||
yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade));
|
yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade));
|
||||||
|
|
||||||
|
@ -46,9 +49,10 @@ namespace StardewModdingAPI.Metadata
|
||||||
yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies);
|
yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies);
|
||||||
|
|
||||||
#if HARMONY_2
|
#if HARMONY_2
|
||||||
// rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update)
|
// rewrite for SMAPI 3.x (Harmony 1.x => 2.0 update)
|
||||||
yield return new Harmony1AssemblyRewriter();
|
yield return new Harmony1AssemblyRewriter();
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** detect mod issues
|
** detect mod issues
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("SMAPI.Tests")]
|
[assembly: InternalsVisibleTo("SMAPI.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("ErrorHandler")]
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
|
||||||
|
|
|
@ -17,7 +17,7 @@ namespace StardewModdingAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Extension methods for <see cref="SButtonState"/>.</summary>
|
/// <summary>Extension methods for <see cref="SButtonState"/>.</summary>
|
||||||
internal static class InputStatusExtensions
|
public static class InputStatusExtensions
|
||||||
{
|
{
|
||||||
/// <summary>Whether the button was pressed or held.</summary>
|
/// <summary>Whether the button was pressed or held.</summary>
|
||||||
/// <param name="state">The button state.</param>
|
/// <param name="state">The button state.</param>
|
||||||
|
|
|
@ -33,6 +33,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
|
||||||
*/
|
*/
|
||||||
"DeveloperMode": true,
|
"DeveloperMode": true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether SMAPI should rewrite mods for compatibility. This may prevent older mods from
|
||||||
|
* loading, but bypasses a Visual Studio crash when debugging.
|
||||||
|
*/
|
||||||
|
"RewriteMods": true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to add a section to the 'mod issues' list for mods which directly use potentially
|
* Whether to add a section to the 'mod issues' list for mods which directly use potentially
|
||||||
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as
|
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as
|
||||||
|
@ -113,6 +119,7 @@ copy all the settings, or you may cause bugs due to overridden changes in future
|
||||||
*/
|
*/
|
||||||
"SuppressUpdateChecks": [
|
"SuppressUpdateChecks": [
|
||||||
"SMAPI.ConsoleCommands",
|
"SMAPI.ConsoleCommands",
|
||||||
|
"SMAPI.ErrorHandler",
|
||||||
"SMAPI.SaveBackup"
|
"SMAPI.SaveBackup"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using StardewModdingAPI.Framework;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>A single multi-key binding which can be triggered by the player.</summary>
|
||||||
|
/// <remarks>NOTE: this is part of <see cref="KeybindList"/>, and usually shouldn't be used directly.</remarks>
|
||||||
|
public class Keybind
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>Get the current input state for a button.</summary>
|
||||||
|
[Obsolete("This property should only be used for unit tests.")]
|
||||||
|
internal Func<SButton, SButtonState> GetButtonState { get; set; } = SGame.GetInputState;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The buttons that must be down to activate the keybind.</summary>
|
||||||
|
public SButton[] Buttons { get; }
|
||||||
|
|
||||||
|
/// <summary>Whether any keys are bound.</summary>
|
||||||
|
public bool IsBound { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="buttons">The buttons that must be down to activate the keybind.</param>
|
||||||
|
public Keybind(params SButton[] buttons)
|
||||||
|
{
|
||||||
|
this.Buttons = buttons;
|
||||||
|
this.IsBound = buttons.Any(p => p != SButton.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parse a keybind string, if it's valid.</summary>
|
||||||
|
/// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
|
||||||
|
/// <param name="parsed">The parsed keybind, if valid.</param>
|
||||||
|
/// <param name="errors">The parse errors, if any.</param>
|
||||||
|
public static bool TryParse(string input, out Keybind parsed, out string[] errors)
|
||||||
|
{
|
||||||
|
// empty input
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
parsed = new Keybind(SButton.None);
|
||||||
|
errors = new string[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse buttons
|
||||||
|
string[] rawButtons = input.Split('+');
|
||||||
|
SButton[] buttons = new SButton[rawButtons.Length];
|
||||||
|
List<string> rawErrors = new List<string>();
|
||||||
|
for (int i = 0; i < buttons.Length; i++)
|
||||||
|
{
|
||||||
|
string rawButton = rawButtons[i].Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(rawButton))
|
||||||
|
rawErrors.Add("Invalid empty button value");
|
||||||
|
else if (!Enum.TryParse(rawButton, ignoreCase: true, out SButton button))
|
||||||
|
{
|
||||||
|
string error = $"Invalid button value '{rawButton}'";
|
||||||
|
|
||||||
|
switch (rawButton.ToLower())
|
||||||
|
{
|
||||||
|
case "shift":
|
||||||
|
error += $" (did you mean {SButton.LeftShift}?)";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ctrl":
|
||||||
|
case "control":
|
||||||
|
error += $" (did you mean {SButton.LeftControl}?)";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "alt":
|
||||||
|
error += $" (did you mean {SButton.LeftAlt}?)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawErrors.Add(error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
buttons[i] = button;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build result
|
||||||
|
if (rawErrors.Any())
|
||||||
|
{
|
||||||
|
parsed = null;
|
||||||
|
errors = rawErrors.ToArray();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parsed = new Keybind(buttons);
|
||||||
|
errors = new string[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the keybind state relative to the previous tick.</summary>
|
||||||
|
public SButtonState GetState()
|
||||||
|
{
|
||||||
|
SButtonState[] states = this.Buttons.Select(this.GetButtonState).Distinct().ToArray();
|
||||||
|
|
||||||
|
// single state
|
||||||
|
if (states.Length == 1)
|
||||||
|
return states[0];
|
||||||
|
|
||||||
|
// if any key has no state, the whole set wasn't enabled last tick
|
||||||
|
if (states.Contains(SButtonState.None))
|
||||||
|
return SButtonState.None;
|
||||||
|
|
||||||
|
// mix of held + pressed => pressed
|
||||||
|
if (states.All(p => p == SButtonState.Pressed || p == SButtonState.Held))
|
||||||
|
return SButtonState.Pressed;
|
||||||
|
|
||||||
|
// mix of held + released => released
|
||||||
|
if (states.All(p => p == SButtonState.Held || p == SButtonState.Released))
|
||||||
|
return SButtonState.Released;
|
||||||
|
|
||||||
|
// not down last tick or now
|
||||||
|
return SButtonState.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a string representation of the keybind.</summary>
|
||||||
|
/// <remarks>A keybind is serialized to a string like <c>LeftControl + S</c>, where each key is separated with <c>+</c>. The key order is commutative, so <c>LeftControl + S</c> and <c>S + LeftControl</c> are identical.</remarks>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return this.Buttons.Length > 0
|
||||||
|
? string.Join(" + ", this.Buttons)
|
||||||
|
: SButton.None.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using StardewModdingAPI.Toolkit.Serialization;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>A set of multi-key bindings which can be triggered by the player.</summary>
|
||||||
|
public class KeybindList
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The individual keybinds.</summary>
|
||||||
|
public Keybind[] Keybinds { get; }
|
||||||
|
|
||||||
|
/// <summary>Whether any keys are bound.</summary>
|
||||||
|
public bool IsBound { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="keybinds">The underlying keybinds.</param>
|
||||||
|
/// <remarks>See <see cref="Parse"/> or <see cref="TryParse"/> to parse it from a string representation. You can also use this type directly in your config or JSON data models, and it'll be parsed by SMAPI.</remarks>
|
||||||
|
public KeybindList(params Keybind[] keybinds)
|
||||||
|
{
|
||||||
|
this.Keybinds = keybinds.Where(p => p.IsBound).ToArray();
|
||||||
|
this.IsBound = this.Keybinds.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parse a keybind list from a string, and throw an exception if it's not valid.</summary>
|
||||||
|
/// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
|
||||||
|
/// <exception cref="FormatException">The <paramref name="input"/> format is invalid.</exception>
|
||||||
|
public static KeybindList Parse(string input)
|
||||||
|
{
|
||||||
|
return KeybindList.TryParse(input, out KeybindList parsed, out string[] errors)
|
||||||
|
? parsed
|
||||||
|
: throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{input}'.\n{string.Join("\n", errors)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Try to parse a keybind list from a string.</summary>
|
||||||
|
/// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
|
||||||
|
/// <param name="parsed">The parsed keybind list, if valid.</param>
|
||||||
|
/// <param name="errors">The errors that occurred while parsing the input, if any.</param>
|
||||||
|
public static bool TryParse(string input, out KeybindList parsed, out string[] errors)
|
||||||
|
{
|
||||||
|
// empty input
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
parsed = new KeybindList();
|
||||||
|
errors = new string[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse buttons
|
||||||
|
var rawErrors = new List<string>();
|
||||||
|
var keybinds = new List<Keybind>();
|
||||||
|
foreach (string rawSet in input.Split(','))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawSet))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!Keybind.TryParse(rawSet, out Keybind keybind, out string[] curErrors))
|
||||||
|
rawErrors.AddRange(curErrors);
|
||||||
|
else
|
||||||
|
keybinds.Add(keybind);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build result
|
||||||
|
if (rawErrors.Any())
|
||||||
|
{
|
||||||
|
parsed = null;
|
||||||
|
errors = rawErrors.Distinct().ToArray();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parsed = new KeybindList(keybinds.ToArray());
|
||||||
|
errors = new string[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a keybind list for a single keybind.</summary>
|
||||||
|
/// <param name="buttons">The buttons that must be down to activate the keybind.</param>
|
||||||
|
public static KeybindList ForSingle(params SButton[] buttons)
|
||||||
|
{
|
||||||
|
return new KeybindList(
|
||||||
|
new Keybind(buttons)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the overall keybind list state relative to the previous tick.</summary>
|
||||||
|
/// <remarks>States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'.</remarks>
|
||||||
|
public SButtonState GetState()
|
||||||
|
{
|
||||||
|
bool wasPressed = false;
|
||||||
|
bool isPressed = false;
|
||||||
|
|
||||||
|
foreach (Keybind keybind in this.Keybinds)
|
||||||
|
{
|
||||||
|
switch (keybind.GetState())
|
||||||
|
{
|
||||||
|
case SButtonState.Pressed:
|
||||||
|
isPressed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SButtonState.Held:
|
||||||
|
wasPressed = true;
|
||||||
|
isPressed = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SButtonState.Released:
|
||||||
|
wasPressed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasPressed == isPressed)
|
||||||
|
{
|
||||||
|
return wasPressed
|
||||||
|
? SButtonState.Held
|
||||||
|
: SButtonState.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasPressed
|
||||||
|
? SButtonState.Released
|
||||||
|
: SButtonState.Pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get whether any of the button sets are pressed.</summary>
|
||||||
|
public bool IsDown()
|
||||||
|
{
|
||||||
|
SButtonState state = this.GetState();
|
||||||
|
return state == SButtonState.Pressed || state == SButtonState.Held;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get whether the input binding was just pressed this tick.</summary>
|
||||||
|
public bool JustPressed()
|
||||||
|
{
|
||||||
|
return this.GetState() == SButtonState.Pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned.</summary>
|
||||||
|
public Keybind GetKeybindCurrentlyDown()
|
||||||
|
{
|
||||||
|
return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a string representation of the input binding.</summary>
|
||||||
|
/// <remarks>A keybind list is serialized to a string like <c>LeftControl + S, LeftAlt + S</c>, where each multi-key binding is separated with <c>,</c> and the keys within each keybind are separated with <c>+</c>. The key order is commutative, so <c>LeftControl + S</c> and <c>S + LeftControl</c> are identical.</remarks>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return this.Keybinds.Length > 0
|
||||||
|
? string.Join(", ", this.Keybinds.Select(p => p.ToString()))
|
||||||
|
: SButton.None.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,10 @@ namespace StardewModdingAPI.Utilities
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Create the initial value for a player.</summary>
|
/// <summary>Create the initial value for a screen.</summary>
|
||||||
private readonly Func<T> CreateNewState;
|
private readonly Func<T> CreateNewState;
|
||||||
|
|
||||||
/// <summary>The tracked values for each player.</summary>
|
/// <summary>The tracked values for each screen.</summary>
|
||||||
private readonly IDictionary<int, T> States = new Dictionary<int, T>();
|
private readonly IDictionary<int, T> States = new Dictionary<int, T>();
|
||||||
|
|
||||||
/// <summary>The last <see cref="Context.LastRemovedScreenId"/> value for which this instance was updated.</summary>
|
/// <summary>The last <see cref="Context.LastRemovedScreenId"/> value for which this instance was updated.</summary>
|
||||||
|
@ -24,8 +24,8 @@ namespace StardewModdingAPI.Utilities
|
||||||
/*********
|
/*********
|
||||||
** Accessors
|
** Accessors
|
||||||
*********/
|
*********/
|
||||||
/// <summary>The value for the current player.</summary>
|
/// <summary>The value for the current screen.</summary>
|
||||||
/// <remarks>The value is initialized the first time it's requested for that player, unless it's set manually first.</remarks>
|
/// <remarks>The value is initialized the first time it's requested for that screen, unless it's set manually first.</remarks>
|
||||||
public T Value
|
public T Value
|
||||||
{
|
{
|
||||||
get => this.GetValueForScreen(Context.ScreenId);
|
get => this.GetValueForScreen(Context.ScreenId);
|
||||||
|
@ -41,47 +41,66 @@ namespace StardewModdingAPI.Utilities
|
||||||
: this(null) { }
|
: this(null) { }
|
||||||
|
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="createNewState">Create the initial state for a player screen.</param>
|
/// <param name="createNewState">Create the initial state for a screen.</param>
|
||||||
public PerScreen(Func<T> createNewState)
|
public PerScreen(Func<T> createNewState)
|
||||||
{
|
{
|
||||||
this.CreateNewState = createNewState ?? (() => default);
|
this.CreateNewState = createNewState ?? (() => default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get all active values by screen ID. This doesn't initialize the value for a screen ID if it's not created yet.</summary>
|
||||||
|
public IEnumerable<KeyValuePair<int, T>> GetActiveValues()
|
||||||
|
{
|
||||||
|
this.RemoveDeadScreens();
|
||||||
|
return this.States.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Get the value for a given screen ID, creating it if needed.</summary>
|
/// <summary>Get the value for a given screen ID, creating it if needed.</summary>
|
||||||
/// <param name="screenId">The screen ID to check.</param>
|
/// <param name="screenId">The screen ID to check.</param>
|
||||||
internal T GetValueForScreen(int screenId)
|
public T GetValueForScreen(int screenId)
|
||||||
{
|
{
|
||||||
this.RemoveDeadPlayers();
|
this.RemoveDeadScreens();
|
||||||
return this.States.TryGetValue(screenId, out T state)
|
return this.States.TryGetValue(screenId, out T state)
|
||||||
? state
|
? state
|
||||||
: this.States[screenId] = this.CreateNewState();
|
: this.States[screenId] = this.CreateNewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Set the value for a given screen ID, creating it if needed.</summary>
|
/// <summary>Set the value for a given screen ID.</summary>
|
||||||
/// <param name="screenId">The screen ID whose value set.</param>
|
/// <param name="screenId">The screen ID whose value set.</param>
|
||||||
/// <param name="value">The value to set.</param>
|
/// <param name="value">The value to set.</param>
|
||||||
internal void SetValueForScreen(int screenId, T value)
|
public void SetValueForScreen(int screenId, T value)
|
||||||
{
|
{
|
||||||
this.RemoveDeadPlayers();
|
this.RemoveDeadScreens();
|
||||||
this.States[screenId] = value;
|
this.States[screenId] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Remove all active values.</summary>
|
||||||
|
public void ResetAllScreens()
|
||||||
|
{
|
||||||
|
this.RemoveScreens(p => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** Private methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Remove players who are no longer have a split-screen index.</summary>
|
/// <summary>Remove screens which are no longer active.</summary>
|
||||||
/// <returns>Returns whether any players were removed.</returns>
|
private void RemoveDeadScreens()
|
||||||
private void RemoveDeadPlayers()
|
|
||||||
{
|
{
|
||||||
if (this.LastRemovedScreenId == Context.LastRemovedScreenId)
|
if (this.LastRemovedScreenId == Context.LastRemovedScreenId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.LastRemovedScreenId = Context.LastRemovedScreenId;
|
this.LastRemovedScreenId = Context.LastRemovedScreenId;
|
||||||
foreach (int id in this.States.Keys.ToArray())
|
|
||||||
|
this.RemoveScreens(id => !Context.HasScreenId(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Remove screens matching a condition.</summary>
|
||||||
|
/// <param name="shouldRemove">Returns whether a screen ID should be removed.</param>
|
||||||
|
private void RemoveScreens(Func<int, bool> shouldRemove)
|
||||||
{
|
{
|
||||||
if (!Context.HasScreenId(id))
|
foreach (var pair in this.States.ToArray())
|
||||||
this.States.Remove(id);
|
{
|
||||||
|
if (shouldRemove(pair.Key))
|
||||||
|
this.States.Remove(pair.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen).",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{season}} {{day}}",
|
"generic.date": "{{season}} {{day}}",
|
||||||
"generic.date-with-year": "{{season}} {{day}} im Jahr {{year}}"
|
"generic.date-with-year": "{{season}} {{day}} im Jahr {{year}}"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info).",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{season}} {{day}}",
|
"generic.date": "{{season}} {{day}}",
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información).",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{seasonLowercase}} {{day}}",
|
"generic.date": "{{seasonLowercase}} {{day}}",
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations).",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{day}} {{seasonLowercase}}",
|
"generic.date": "{{day}} {{seasonLowercase}}",
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon).",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{season}} {{day}}",
|
"generic.date": "{{season}} {{day}}",
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni).",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{day}} {{season}}",
|
"generic.date": "{{day}} {{season}}",
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
{
|
{
|
||||||
// error messages
|
|
||||||
"warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)",
|
|
||||||
|
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{season}} {{day}}日",
|
"generic.date": "{{season}} {{day}}日",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue