From 89ca5952c51128c23c40b6264aa125ad7e5e2305 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 19:24:31 -0400 Subject: update web API to .NET Core 2.0 so we can use more packages, update all packages (#336) --- src/StardewModdingAPI.Tests/packages.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI.Tests/packages.config') diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/StardewModdingAPI.Tests/packages.config index 6f04e625..5fdfebdb 100644 --- a/src/StardewModdingAPI.Tests/packages.config +++ b/src/StardewModdingAPI.Tests/packages.config @@ -3,5 +3,5 @@ - - \ No newline at end of file + + -- cgit From 929dccb75a1405737975d76648e015a3e7c00177 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:07:10 -0400 Subject: reorganise repo structure --- .editorconfig | 68 + CONTRIBUTING.md | 17 - LICENSE | 165 -- README.md | 179 -- build/GlobalAssemblyInfo.cs | 6 + build/common.targets | 107 + build/prepare-install-package.targets | 49 + docs/CONTRIBUTING.md | 17 + docs/LICENSE.md | 165 ++ docs/README.md | 179 ++ docs/release-notes.md | 471 +++++ release-notes.md | 471 ----- src/.editorconfig | 68 - src/GlobalAssemblyInfo.cs | 6 - src/ModBuildConfig/README.md | 121 ++ src/ModBuildConfig/assets/nuget-icon.pdn | Bin 0 -> 7401 bytes src/ModBuildConfig/assets/nuget-icon.png | Bin 0 -> 5054 bytes src/ModBuildConfig/build/smapi.targets | 273 +++ src/ModBuildConfig/package.nuspec | 22 + src/ModBuildConfig/release-notes.md | 28 + .../Properties/AssemblyInfo.cs | 7 + src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs | 59 + .../StardewModdingAPI.AssemblyRewriters.csproj | 44 + src/SMAPI.Installer/Enums/Platform.cs | 12 + src/SMAPI.Installer/Enums/ScriptAction.cs | 12 + src/SMAPI.Installer/InteractiveInstaller.cs | 740 +++++++ src/SMAPI.Installer/Program.cs | 17 + src/SMAPI.Installer/Properties/AssemblyInfo.cs | 6 + .../StardewModdingAPI.Installer.csproj | 56 + src/SMAPI.Installer/readme.txt | 44 + src/SMAPI.Models/ModInfoModel.cs | 48 + src/SMAPI.Models/ModSeachModel.cs | 30 + .../StardewModdingAPI.Models.projitems | 15 + src/SMAPI.Models/StardewModdingAPI.Models.shproj | 13 + src/SMAPI.Tests/Core/ModResolverTests.cs | 556 ++++++ src/SMAPI.Tests/Core/TranslationTests.cs | 356 ++++ src/SMAPI.Tests/Properties/AssemblyInfo.cs | 6 + src/SMAPI.Tests/Sample.cs | 30 + src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 69 + src/SMAPI.Tests/Utilities/SDateTests.cs | 255 +++ src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 302 +++ src/SMAPI.Tests/packages.config | 7 + src/SMAPI.Web/Controllers/ModsController.cs | 162 ++ .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 74 + .../Framework/InternalControllerFeatureProvider.cs | 27 + .../Framework/ModRepositories/BaseRepository.cs | 51 + .../ModRepositories/ChucklefishRepository.cs | 92 + .../Framework/ModRepositories/GitHubRepository.cs | 97 + .../Framework/ModRepositories/IModRepository.cs | 24 + .../Framework/ModRepositories/NexusRepository.cs | 89 + src/SMAPI.Web/Framework/RewriteSubdomainRule.cs | 30 + src/SMAPI.Web/Framework/VersionConstraint.cs | 15 + src/SMAPI.Web/Program.cs | 26 + src/SMAPI.Web/Properties/AssemblyInfo.cs | 4 + src/SMAPI.Web/Properties/launchSettings.json | 29 + src/SMAPI.Web/StardewModdingAPI.Web.csproj | 26 + src/SMAPI.Web/Startup.cs | 70 + src/SMAPI.Web/appsettings.Development.json | 10 + src/SMAPI.Web/appsettings.json | 30 + src/SMAPI.sln | 138 ++ src/SMAPI.sln.DotSettings | 19 + src/SMAPI/App.config | 9 + src/SMAPI/Constants.cs | 169 ++ src/SMAPI/ContentSource.cs | 12 + src/SMAPI/Context.cs | 37 + src/SMAPI/Events/ChangeType.cs | 15 + src/SMAPI/Events/ContentEvents.cs | 29 + src/SMAPI/Events/ControlEvents.cs | 112 ++ src/SMAPI/Events/EventArgsClickableMenuChanged.cs | 31 + src/SMAPI/Events/EventArgsClickableMenuClosed.cs | 26 + .../Events/EventArgsControllerButtonPressed.cs | 32 + .../Events/EventArgsControllerButtonReleased.cs | 32 + .../Events/EventArgsControllerTriggerPressed.cs | 37 + .../Events/EventArgsControllerTriggerReleased.cs | 37 + .../Events/EventArgsCurrentLocationChanged.cs | 31 + src/SMAPI/Events/EventArgsGameLocationsChanged.cs | 27 + src/SMAPI/Events/EventArgsInput.cs | 124 ++ src/SMAPI/Events/EventArgsIntChanged.cs | 29 + src/SMAPI/Events/EventArgsInventoryChanged.cs | 41 + src/SMAPI/Events/EventArgsKeyPressed.cs | 26 + src/SMAPI/Events/EventArgsKeyboardStateChanged.cs | 31 + src/SMAPI/Events/EventArgsLevelUp.cs | 52 + .../Events/EventArgsLocationObjectsChanged.cs | 28 + src/SMAPI/Events/EventArgsMineLevelChanged.cs | 30 + src/SMAPI/Events/EventArgsMouseStateChanged.cs | 42 + src/SMAPI/Events/EventArgsValueChanged.cs | 31 + src/SMAPI/Events/GameEvents.cs | 96 + src/SMAPI/Events/GraphicsEvents.cs | 116 ++ src/SMAPI/Events/InputEvents.cs | 43 + src/SMAPI/Events/ItemStackChange.cs | 20 + src/SMAPI/Events/LocationEvents.cs | 54 + src/SMAPI/Events/MenuEvents.cs | 40 + src/SMAPI/Events/MineEvents.cs | 28 + src/SMAPI/Events/PlayerEvents.cs | 43 + src/SMAPI/Events/SaveEvents.cs | 56 + src/SMAPI/Events/TimeEvents.cs | 37 + src/SMAPI/Framework/Command.cs | 40 + src/SMAPI/Framework/CommandManager.cs | 116 ++ src/SMAPI/Framework/Content/AssetData.cs | 44 + .../Framework/Content/AssetDataForDictionary.cs | 45 + src/SMAPI/Framework/Content/AssetDataForImage.cs | 70 + src/SMAPI/Framework/Content/AssetDataForObject.cs | 54 + src/SMAPI/Framework/Content/AssetInfo.cs | 82 + src/SMAPI/Framework/ContentManagerShim.cs | 50 + src/SMAPI/Framework/CursorPosition.cs | 35 + src/SMAPI/Framework/DeprecationLevel.cs | 15 + src/SMAPI/Framework/DeprecationManager.cs | 105 + .../Exceptions/SAssemblyLoadFailedException.cs | 16 + .../Framework/Exceptions/SContentLoadException.cs | 18 + src/SMAPI/Framework/Exceptions/SParseException.cs | 17 + src/SMAPI/Framework/GameVersion.cs | 68 + src/SMAPI/Framework/IModMetadata.cs | 47 + src/SMAPI/Framework/InternalExtensions.cs | 131 ++ .../Logging/ConsoleInterceptionManager.cs | 86 + .../Framework/Logging/InterceptingTextWriter.cs | 63 + src/SMAPI/Framework/Logging/LogFileManager.cs | 57 + src/SMAPI/Framework/ModHelpers/BaseHelper.cs | 23 + src/SMAPI/Framework/ModHelpers/CommandHelper.cs | 54 + src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 476 +++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 129 ++ .../Framework/ModHelpers/ModRegistryHelper.cs | 48 + src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 200 ++ .../Framework/ModHelpers/TranslationHelper.cs | 140 ++ .../ModLoading/AssemblyDefinitionResolver.cs | 61 + .../Framework/ModLoading/AssemblyLoadStatus.cs | 15 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 333 ++++ .../Framework/ModLoading/AssemblyParseResult.cs | 36 + .../Framework/ModLoading/Finders/EventFinder.cs | 82 + .../Framework/ModLoading/Finders/FieldFinder.cs | 82 + .../Framework/ModLoading/Finders/MethodFinder.cs | 82 + .../Framework/ModLoading/Finders/PropertyFinder.cs | 82 + .../Framework/ModLoading/Finders/TypeFinder.cs | 133 ++ .../Framework/ModLoading/IInstructionHandler.cs | 34 + .../ModLoading/IncompatibleInstructionException.cs | 35 + .../ModLoading/InstructionHandleResult.cs | 24 + .../ModLoading/InvalidModStateException.cs | 14 + .../Framework/ModLoading/ModDependencyStatus.cs | 18 + src/SMAPI/Framework/ModLoading/ModMetadata.cs | 68 + .../Framework/ModLoading/ModMetadataStatus.cs | 12 + src/SMAPI/Framework/ModLoading/ModResolver.cs | 366 ++++ src/SMAPI/Framework/ModLoading/Platform.cs | 12 + .../Framework/ModLoading/PlatformAssemblyMap.cs | 55 + src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 94 + .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 50 + .../Rewriters/FieldToPropertyRewriter.cs | 51 + .../ModLoading/Rewriters/MethodParentRewriter.cs | 88 + .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 154 ++ .../Rewriters/VirtualEntryCallRemover.cs | 90 + src/SMAPI/Framework/ModRegistry.cs | 113 ++ src/SMAPI/Framework/Models/Manifest.cs | 47 + src/SMAPI/Framework/Models/ManifestDependency.cs | 34 + src/SMAPI/Framework/Models/ModCompatibility.cs | 55 + src/SMAPI/Framework/Models/ModDataID.cs | 85 + src/SMAPI/Framework/Models/ModDataRecord.cs | 63 + src/SMAPI/Framework/Models/ModStatus.cs | 18 + src/SMAPI/Framework/Models/SConfig.cs | 27 + src/SMAPI/Framework/Monitor.cs | 194 ++ src/SMAPI/Framework/Reflection/CacheEntry.cs | 30 + src/SMAPI/Framework/Reflection/PrivateField.cs | 93 + src/SMAPI/Framework/Reflection/PrivateMethod.cs | 99 + src/SMAPI/Framework/Reflection/PrivateProperty.cs | 93 + src/SMAPI/Framework/Reflection/Reflector.cs | 276 +++ src/SMAPI/Framework/RequestExitDelegate.cs | 7 + src/SMAPI/Framework/SContentManager.cs | 531 +++++ src/SMAPI/Framework/SGame.cs | 1403 +++++++++++++ src/SMAPI/Framework/Serialisation/JsonHelper.cs | 96 + .../Framework/Serialisation/SFieldConverter.cs | 121 ++ .../Serialisation/SelectiveStringEnumConverter.cs | 37 + src/SMAPI/Framework/Utilities/ContextHash.cs | 61 + src/SMAPI/Framework/Utilities/Countdown.cs | 44 + src/SMAPI/Framework/WebApiClient.cs | 73 + src/SMAPI/IAssetData.cs | 47 + src/SMAPI/IAssetDataForDictionary.cs | 26 + src/SMAPI/IAssetDataForImage.cs | 23 + src/SMAPI/IAssetEditor.cs | 17 + src/SMAPI/IAssetInfo.cs | 28 + src/SMAPI/IAssetLoader.cs | 17 + src/SMAPI/ICommandHelper.cs | 26 + src/SMAPI/IContentHelper.cs | 56 + src/SMAPI/ICursorPosition.cs | 17 + src/SMAPI/IManifest.cs | 41 + src/SMAPI/IManifestDependency.cs | 18 + src/SMAPI/IMod.cs | 26 + src/SMAPI/IModHelper.cs | 58 + src/SMAPI/IModLinked.cs | 12 + src/SMAPI/IModRegistry.cs | 20 + src/SMAPI/IMonitor.cs | 25 + src/SMAPI/IPrivateField.cs | 26 + src/SMAPI/IPrivateMethod.cs | 27 + src/SMAPI/IPrivateProperty.cs | 26 + src/SMAPI/IReflectionHelper.cs | 67 + src/SMAPI/ISemanticVersion.cs | 59 + src/SMAPI/ITranslationHelper.cs | 34 + src/SMAPI/LogLevel.cs | 24 + src/SMAPI/Metadata/CoreAssets.cs | 166 ++ src/SMAPI/Metadata/InstructionMetadata.cs | 101 + src/SMAPI/Mod.cs | 50 + src/SMAPI/PatchMode.cs | 12 + src/SMAPI/Program.cs | 966 +++++++++ src/SMAPI/Properties/AssemblyInfo.cs | 9 + src/SMAPI/SemanticVersion.cs | 237 +++ src/SMAPI/StardewModdingAPI.config.json | 2063 ++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 277 +++ src/SMAPI/Translation.cs | 154 ++ src/SMAPI/Utilities/SButton.cs | 675 +++++++ src/SMAPI/Utilities/SDate.cs | 232 +++ src/SMAPI/icon.ico | Bin 0 -> 15086 bytes src/SMAPI/packages.config | 5 + src/SMAPI/steam_appid.txt | 1 + src/SMAPI/unix-launcher.sh | 88 + .../Properties/AssemblyInfo.cs | 7 - .../SpriteBatchMethods.cs | 59 - .../StardewModdingAPI.AssemblyRewriters.csproj | 44 - src/StardewModdingAPI.Installer/Enums/Platform.cs | 12 - .../Enums/ScriptAction.cs | 12 - .../InteractiveInstaller.cs | 740 ------- src/StardewModdingAPI.Installer/Program.cs | 17 - .../Properties/AssemblyInfo.cs | 6 - .../StardewModdingAPI.Installer.csproj | 56 - src/StardewModdingAPI.Installer/readme.txt | 44 - src/StardewModdingAPI.ModBuildConfig/README.md | 121 -- .../assets/nuget-icon.pdn | Bin 7401 -> 0 bytes .../assets/nuget-icon.png | Bin 5054 -> 0 bytes .../build/smapi.targets | 273 --- .../package.nuspec | 22 - .../release-notes.md | 28 - src/StardewModdingAPI.Models/ModInfoModel.cs | 48 - src/StardewModdingAPI.Models/ModSeachModel.cs | 30 - .../StardewModdingAPI.Models.projitems | 15 - .../StardewModdingAPI.Models.shproj | 13 - .../Core/ModResolverTests.cs | 556 ------ .../Core/TranslationTests.cs | 356 ---- .../Properties/AssemblyInfo.cs | 6 - src/StardewModdingAPI.Tests/Sample.cs | 30 - .../StardewModdingAPI.Tests.csproj | 69 - .../Utilities/SDateTests.cs | 255 --- .../Utilities/SemanticVersionTests.cs | 302 --- src/StardewModdingAPI.Tests/packages.config | 7 - .../Controllers/ModsController.cs | 162 -- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 74 - .../Framework/InternalControllerFeatureProvider.cs | 27 - .../Framework/ModRepositories/BaseRepository.cs | 51 - .../ModRepositories/ChucklefishRepository.cs | 92 - .../Framework/ModRepositories/GitHubRepository.cs | 97 - .../Framework/ModRepositories/IModRepository.cs | 24 - .../Framework/ModRepositories/NexusRepository.cs | 89 - .../Framework/RewriteSubdomainRule.cs | 30 - .../Framework/VersionConstraint.cs | 15 - src/StardewModdingAPI.Web/Program.cs | 26 - .../Properties/AssemblyInfo.cs | 4 - .../Properties/launchSettings.json | 29 - .../StardewModdingAPI.Web.csproj | 26 - src/StardewModdingAPI.Web/Startup.cs | 70 - .../appsettings.Development.json | 10 - src/StardewModdingAPI.Web/appsettings.json | 30 - src/StardewModdingAPI.sln | 128 -- src/StardewModdingAPI.sln.DotSettings | 19 - src/StardewModdingAPI/App.config | 9 - src/StardewModdingAPI/Constants.cs | 169 -- src/StardewModdingAPI/ContentSource.cs | 12 - src/StardewModdingAPI/Context.cs | 37 - src/StardewModdingAPI/Events/ChangeType.cs | 15 - src/StardewModdingAPI/Events/ContentEvents.cs | 29 - src/StardewModdingAPI/Events/ControlEvents.cs | 112 -- .../Events/EventArgsClickableMenuChanged.cs | 31 - .../Events/EventArgsClickableMenuClosed.cs | 26 - .../Events/EventArgsControllerButtonPressed.cs | 32 - .../Events/EventArgsControllerButtonReleased.cs | 32 - .../Events/EventArgsControllerTriggerPressed.cs | 37 - .../Events/EventArgsControllerTriggerReleased.cs | 37 - .../Events/EventArgsCurrentLocationChanged.cs | 31 - .../Events/EventArgsGameLocationsChanged.cs | 27 - src/StardewModdingAPI/Events/EventArgsInput.cs | 124 -- .../Events/EventArgsIntChanged.cs | 29 - .../Events/EventArgsInventoryChanged.cs | 41 - .../Events/EventArgsKeyPressed.cs | 26 - .../Events/EventArgsKeyboardStateChanged.cs | 31 - src/StardewModdingAPI/Events/EventArgsLevelUp.cs | 52 - .../Events/EventArgsLocationObjectsChanged.cs | 28 - .../Events/EventArgsMineLevelChanged.cs | 30 - .../Events/EventArgsMouseStateChanged.cs | 42 - .../Events/EventArgsValueChanged.cs | 31 - src/StardewModdingAPI/Events/GameEvents.cs | 96 - src/StardewModdingAPI/Events/GraphicsEvents.cs | 116 -- src/StardewModdingAPI/Events/InputEvents.cs | 43 - src/StardewModdingAPI/Events/ItemStackChange.cs | 20 - src/StardewModdingAPI/Events/LocationEvents.cs | 54 - src/StardewModdingAPI/Events/MenuEvents.cs | 40 - src/StardewModdingAPI/Events/MineEvents.cs | 28 - src/StardewModdingAPI/Events/PlayerEvents.cs | 43 - src/StardewModdingAPI/Events/SaveEvents.cs | 56 - src/StardewModdingAPI/Events/TimeEvents.cs | 37 - src/StardewModdingAPI/Framework/Command.cs | 40 - src/StardewModdingAPI/Framework/CommandManager.cs | 116 -- .../Framework/Content/AssetData.cs | 44 - .../Framework/Content/AssetDataForDictionary.cs | 45 - .../Framework/Content/AssetDataForImage.cs | 70 - .../Framework/Content/AssetDataForObject.cs | 54 - .../Framework/Content/AssetInfo.cs | 82 - .../Framework/ContentManagerShim.cs | 50 - src/StardewModdingAPI/Framework/CursorPosition.cs | 35 - .../Framework/DeprecationLevel.cs | 15 - .../Framework/DeprecationManager.cs | 105 - .../Exceptions/SAssemblyLoadFailedException.cs | 16 - .../Framework/Exceptions/SContentLoadException.cs | 18 - .../Framework/Exceptions/SParseException.cs | 17 - src/StardewModdingAPI/Framework/GameVersion.cs | 68 - src/StardewModdingAPI/Framework/IModMetadata.cs | 47 - .../Framework/InternalExtensions.cs | 131 -- .../Logging/ConsoleInterceptionManager.cs | 86 - .../Framework/Logging/InterceptingTextWriter.cs | 63 - .../Framework/Logging/LogFileManager.cs | 57 - .../Framework/ModHelpers/BaseHelper.cs | 23 - .../Framework/ModHelpers/CommandHelper.cs | 54 - .../Framework/ModHelpers/ContentHelper.cs | 476 ----- .../Framework/ModHelpers/ModHelper.cs | 129 -- .../Framework/ModHelpers/ModRegistryHelper.cs | 48 - .../Framework/ModHelpers/ReflectionHelper.cs | 200 -- .../Framework/ModHelpers/TranslationHelper.cs | 140 -- .../ModLoading/AssemblyDefinitionResolver.cs | 61 - .../Framework/ModLoading/AssemblyLoadStatus.cs | 15 - .../Framework/ModLoading/AssemblyLoader.cs | 333 ---- .../Framework/ModLoading/AssemblyParseResult.cs | 36 - .../Framework/ModLoading/Finders/EventFinder.cs | 82 - .../Framework/ModLoading/Finders/FieldFinder.cs | 82 - .../Framework/ModLoading/Finders/MethodFinder.cs | 82 - .../Framework/ModLoading/Finders/PropertyFinder.cs | 82 - .../Framework/ModLoading/Finders/TypeFinder.cs | 133 -- .../Framework/ModLoading/IInstructionHandler.cs | 34 - .../ModLoading/IncompatibleInstructionException.cs | 35 - .../ModLoading/InstructionHandleResult.cs | 24 - .../ModLoading/InvalidModStateException.cs | 14 - .../Framework/ModLoading/ModDependencyStatus.cs | 18 - .../Framework/ModLoading/ModMetadata.cs | 68 - .../Framework/ModLoading/ModMetadataStatus.cs | 12 - .../Framework/ModLoading/ModResolver.cs | 366 ---- .../Framework/ModLoading/Platform.cs | 12 - .../Framework/ModLoading/PlatformAssemblyMap.cs | 55 - .../Framework/ModLoading/RewriteHelper.cs | 94 - .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 50 - .../Rewriters/FieldToPropertyRewriter.cs | 51 - .../ModLoading/Rewriters/MethodParentRewriter.cs | 88 - .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 154 -- .../Rewriters/VirtualEntryCallRemover.cs | 90 - src/StardewModdingAPI/Framework/ModRegistry.cs | 113 -- src/StardewModdingAPI/Framework/Models/Manifest.cs | 47 - .../Framework/Models/ManifestDependency.cs | 34 - .../Framework/Models/ModCompatibility.cs | 55 - .../Framework/Models/ModDataID.cs | 85 - .../Framework/Models/ModDataRecord.cs | 63 - .../Framework/Models/ModStatus.cs | 18 - src/StardewModdingAPI/Framework/Models/SConfig.cs | 27 - src/StardewModdingAPI/Framework/Monitor.cs | 194 -- .../Framework/Reflection/CacheEntry.cs | 30 - .../Framework/Reflection/PrivateField.cs | 93 - .../Framework/Reflection/PrivateMethod.cs | 99 - .../Framework/Reflection/PrivateProperty.cs | 93 - .../Framework/Reflection/Reflector.cs | 276 --- .../Framework/RequestExitDelegate.cs | 7 - src/StardewModdingAPI/Framework/SContentManager.cs | 531 ----- src/StardewModdingAPI/Framework/SGame.cs | 1403 ------------- .../Framework/Serialisation/JsonHelper.cs | 96 - .../Framework/Serialisation/SFieldConverter.cs | 121 -- .../Serialisation/SelectiveStringEnumConverter.cs | 37 - .../Framework/Utilities/ContextHash.cs | 61 - .../Framework/Utilities/Countdown.cs | 44 - src/StardewModdingAPI/Framework/WebApiClient.cs | 73 - src/StardewModdingAPI/IAssetData.cs | 47 - src/StardewModdingAPI/IAssetDataForDictionary.cs | 26 - src/StardewModdingAPI/IAssetDataForImage.cs | 23 - src/StardewModdingAPI/IAssetEditor.cs | 17 - src/StardewModdingAPI/IAssetInfo.cs | 28 - src/StardewModdingAPI/IAssetLoader.cs | 17 - src/StardewModdingAPI/ICommandHelper.cs | 26 - src/StardewModdingAPI/IContentHelper.cs | 56 - src/StardewModdingAPI/ICursorPosition.cs | 17 - src/StardewModdingAPI/IManifest.cs | 41 - src/StardewModdingAPI/IManifestDependency.cs | 18 - src/StardewModdingAPI/IMod.cs | 26 - src/StardewModdingAPI/IModHelper.cs | 58 - src/StardewModdingAPI/IModLinked.cs | 12 - src/StardewModdingAPI/IModRegistry.cs | 20 - src/StardewModdingAPI/IMonitor.cs | 25 - src/StardewModdingAPI/IPrivateField.cs | 26 - src/StardewModdingAPI/IPrivateMethod.cs | 27 - src/StardewModdingAPI/IPrivateProperty.cs | 26 - src/StardewModdingAPI/IReflectionHelper.cs | 67 - src/StardewModdingAPI/ISemanticVersion.cs | 59 - src/StardewModdingAPI/ITranslationHelper.cs | 34 - src/StardewModdingAPI/LogLevel.cs | 24 - src/StardewModdingAPI/Metadata/CoreAssets.cs | 166 -- .../Metadata/InstructionMetadata.cs | 101 - src/StardewModdingAPI/Mod.cs | 50 - src/StardewModdingAPI/PatchMode.cs | 12 - src/StardewModdingAPI/Program.cs | 966 --------- src/StardewModdingAPI/Properties/AssemblyInfo.cs | 9 - src/StardewModdingAPI/SemanticVersion.cs | 237 --- .../StardewModdingAPI.config.json | 2063 -------------------- src/StardewModdingAPI/StardewModdingAPI.csproj | 277 --- src/StardewModdingAPI/Translation.cs | 154 -- src/StardewModdingAPI/Utilities/SButton.cs | 675 ------- src/StardewModdingAPI/Utilities/SDate.cs | 232 --- src/StardewModdingAPI/icon.ico | Bin 15086 -> 0 bytes src/StardewModdingAPI/packages.config | 5 - src/StardewModdingAPI/steam_appid.txt | 1 - src/StardewModdingAPI/unix-launcher.sh | 88 - src/TrainerMod/TrainerMod.csproj | 8 +- src/common.targets | 107 - src/prepare-install-package.targets | 49 - 409 files changed, 20466 insertions(+), 20456 deletions(-) create mode 100644 .editorconfig delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 README.md create mode 100644 build/GlobalAssemblyInfo.cs create mode 100644 build/common.targets create mode 100644 build/prepare-install-package.targets create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/LICENSE.md create mode 100644 docs/README.md create mode 100644 docs/release-notes.md delete mode 100644 release-notes.md delete mode 100644 src/.editorconfig delete mode 100644 src/GlobalAssemblyInfo.cs create mode 100644 src/ModBuildConfig/README.md create mode 100644 src/ModBuildConfig/assets/nuget-icon.pdn create mode 100644 src/ModBuildConfig/assets/nuget-icon.png create mode 100644 src/ModBuildConfig/build/smapi.targets create mode 100644 src/ModBuildConfig/package.nuspec create mode 100644 src/ModBuildConfig/release-notes.md create mode 100644 src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs create mode 100644 src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj create mode 100644 src/SMAPI.Installer/Enums/Platform.cs create mode 100644 src/SMAPI.Installer/Enums/ScriptAction.cs create mode 100644 src/SMAPI.Installer/InteractiveInstaller.cs create mode 100644 src/SMAPI.Installer/Program.cs create mode 100644 src/SMAPI.Installer/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Installer/StardewModdingAPI.Installer.csproj create mode 100644 src/SMAPI.Installer/readme.txt create mode 100644 src/SMAPI.Models/ModInfoModel.cs create mode 100644 src/SMAPI.Models/ModSeachModel.cs create mode 100644 src/SMAPI.Models/StardewModdingAPI.Models.projitems create mode 100644 src/SMAPI.Models/StardewModdingAPI.Models.shproj create mode 100644 src/SMAPI.Tests/Core/ModResolverTests.cs create mode 100644 src/SMAPI.Tests/Core/TranslationTests.cs create mode 100644 src/SMAPI.Tests/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Tests/Sample.cs create mode 100644 src/SMAPI.Tests/StardewModdingAPI.Tests.csproj create mode 100644 src/SMAPI.Tests/Utilities/SDateTests.cs create mode 100644 src/SMAPI.Tests/Utilities/SemanticVersionTests.cs create mode 100644 src/SMAPI.Tests/packages.config create mode 100644 src/SMAPI.Web/Controllers/ModsController.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs create mode 100644 src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs create mode 100644 src/SMAPI.Web/Framework/RewriteSubdomainRule.cs create mode 100644 src/SMAPI.Web/Framework/VersionConstraint.cs create mode 100644 src/SMAPI.Web/Program.cs create mode 100644 src/SMAPI.Web/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Web/Properties/launchSettings.json create mode 100644 src/SMAPI.Web/StardewModdingAPI.Web.csproj create mode 100644 src/SMAPI.Web/Startup.cs create mode 100644 src/SMAPI.Web/appsettings.Development.json create mode 100644 src/SMAPI.Web/appsettings.json create mode 100644 src/SMAPI.sln create mode 100644 src/SMAPI.sln.DotSettings create mode 100644 src/SMAPI/App.config create mode 100644 src/SMAPI/Constants.cs create mode 100644 src/SMAPI/ContentSource.cs create mode 100644 src/SMAPI/Context.cs create mode 100644 src/SMAPI/Events/ChangeType.cs create mode 100644 src/SMAPI/Events/ContentEvents.cs create mode 100644 src/SMAPI/Events/ControlEvents.cs create mode 100644 src/SMAPI/Events/EventArgsClickableMenuChanged.cs create mode 100644 src/SMAPI/Events/EventArgsClickableMenuClosed.cs create mode 100644 src/SMAPI/Events/EventArgsControllerButtonPressed.cs create mode 100644 src/SMAPI/Events/EventArgsControllerButtonReleased.cs create mode 100644 src/SMAPI/Events/EventArgsControllerTriggerPressed.cs create mode 100644 src/SMAPI/Events/EventArgsControllerTriggerReleased.cs create mode 100644 src/SMAPI/Events/EventArgsCurrentLocationChanged.cs create mode 100644 src/SMAPI/Events/EventArgsGameLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsInput.cs create mode 100644 src/SMAPI/Events/EventArgsIntChanged.cs create mode 100644 src/SMAPI/Events/EventArgsInventoryChanged.cs create mode 100644 src/SMAPI/Events/EventArgsKeyPressed.cs create mode 100644 src/SMAPI/Events/EventArgsKeyboardStateChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLevelUp.cs create mode 100644 src/SMAPI/Events/EventArgsLocationObjectsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsMineLevelChanged.cs create mode 100644 src/SMAPI/Events/EventArgsMouseStateChanged.cs create mode 100644 src/SMAPI/Events/EventArgsValueChanged.cs create mode 100644 src/SMAPI/Events/GameEvents.cs create mode 100644 src/SMAPI/Events/GraphicsEvents.cs create mode 100644 src/SMAPI/Events/InputEvents.cs create mode 100644 src/SMAPI/Events/ItemStackChange.cs create mode 100644 src/SMAPI/Events/LocationEvents.cs create mode 100644 src/SMAPI/Events/MenuEvents.cs create mode 100644 src/SMAPI/Events/MineEvents.cs create mode 100644 src/SMAPI/Events/PlayerEvents.cs create mode 100644 src/SMAPI/Events/SaveEvents.cs create mode 100644 src/SMAPI/Events/TimeEvents.cs create mode 100644 src/SMAPI/Framework/Command.cs create mode 100644 src/SMAPI/Framework/CommandManager.cs create mode 100644 src/SMAPI/Framework/Content/AssetData.cs create mode 100644 src/SMAPI/Framework/Content/AssetDataForDictionary.cs create mode 100644 src/SMAPI/Framework/Content/AssetDataForImage.cs create mode 100644 src/SMAPI/Framework/Content/AssetDataForObject.cs create mode 100644 src/SMAPI/Framework/Content/AssetInfo.cs create mode 100644 src/SMAPI/Framework/ContentManagerShim.cs create mode 100644 src/SMAPI/Framework/CursorPosition.cs create mode 100644 src/SMAPI/Framework/DeprecationLevel.cs create mode 100644 src/SMAPI/Framework/DeprecationManager.cs create mode 100644 src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs create mode 100644 src/SMAPI/Framework/Exceptions/SContentLoadException.cs create mode 100644 src/SMAPI/Framework/Exceptions/SParseException.cs create mode 100644 src/SMAPI/Framework/GameVersion.cs create mode 100644 src/SMAPI/Framework/IModMetadata.cs create mode 100644 src/SMAPI/Framework/InternalExtensions.cs create mode 100644 src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs create mode 100644 src/SMAPI/Framework/Logging/InterceptingTextWriter.cs create mode 100644 src/SMAPI/Framework/Logging/LogFileManager.cs create mode 100644 src/SMAPI/Framework/ModHelpers/BaseHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/CommandHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/ContentHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/ModHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs create mode 100644 src/SMAPI/Framework/ModHelpers/TranslationHelper.cs create mode 100644 src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs create mode 100644 src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs create mode 100644 src/SMAPI/Framework/ModLoading/AssemblyLoader.cs create mode 100644 src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs create mode 100644 src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/IInstructionHandler.cs create mode 100644 src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs create mode 100644 src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs create mode 100644 src/SMAPI/Framework/ModLoading/InvalidModStateException.cs create mode 100644 src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs create mode 100644 src/SMAPI/Framework/ModLoading/ModMetadata.cs create mode 100644 src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs create mode 100644 src/SMAPI/Framework/ModLoading/ModResolver.cs create mode 100644 src/SMAPI/Framework/ModLoading/Platform.cs create mode 100644 src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs create mode 100644 src/SMAPI/Framework/ModLoading/RewriteHelper.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs create mode 100644 src/SMAPI/Framework/ModRegistry.cs create mode 100644 src/SMAPI/Framework/Models/Manifest.cs create mode 100644 src/SMAPI/Framework/Models/ManifestDependency.cs create mode 100644 src/SMAPI/Framework/Models/ModCompatibility.cs create mode 100644 src/SMAPI/Framework/Models/ModDataID.cs create mode 100644 src/SMAPI/Framework/Models/ModDataRecord.cs create mode 100644 src/SMAPI/Framework/Models/ModStatus.cs create mode 100644 src/SMAPI/Framework/Models/SConfig.cs create mode 100644 src/SMAPI/Framework/Monitor.cs create mode 100644 src/SMAPI/Framework/Reflection/CacheEntry.cs create mode 100644 src/SMAPI/Framework/Reflection/PrivateField.cs create mode 100644 src/SMAPI/Framework/Reflection/PrivateMethod.cs create mode 100644 src/SMAPI/Framework/Reflection/PrivateProperty.cs create mode 100644 src/SMAPI/Framework/Reflection/Reflector.cs create mode 100644 src/SMAPI/Framework/RequestExitDelegate.cs create mode 100644 src/SMAPI/Framework/SContentManager.cs create mode 100644 src/SMAPI/Framework/SGame.cs create mode 100644 src/SMAPI/Framework/Serialisation/JsonHelper.cs create mode 100644 src/SMAPI/Framework/Serialisation/SFieldConverter.cs create mode 100644 src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs create mode 100644 src/SMAPI/Framework/Utilities/ContextHash.cs create mode 100644 src/SMAPI/Framework/Utilities/Countdown.cs create mode 100644 src/SMAPI/Framework/WebApiClient.cs create mode 100644 src/SMAPI/IAssetData.cs create mode 100644 src/SMAPI/IAssetDataForDictionary.cs create mode 100644 src/SMAPI/IAssetDataForImage.cs create mode 100644 src/SMAPI/IAssetEditor.cs create mode 100644 src/SMAPI/IAssetInfo.cs create mode 100644 src/SMAPI/IAssetLoader.cs create mode 100644 src/SMAPI/ICommandHelper.cs create mode 100644 src/SMAPI/IContentHelper.cs create mode 100644 src/SMAPI/ICursorPosition.cs create mode 100644 src/SMAPI/IManifest.cs create mode 100644 src/SMAPI/IManifestDependency.cs create mode 100644 src/SMAPI/IMod.cs create mode 100644 src/SMAPI/IModHelper.cs create mode 100644 src/SMAPI/IModLinked.cs create mode 100644 src/SMAPI/IModRegistry.cs create mode 100644 src/SMAPI/IMonitor.cs create mode 100644 src/SMAPI/IPrivateField.cs create mode 100644 src/SMAPI/IPrivateMethod.cs create mode 100644 src/SMAPI/IPrivateProperty.cs create mode 100644 src/SMAPI/IReflectionHelper.cs create mode 100644 src/SMAPI/ISemanticVersion.cs create mode 100644 src/SMAPI/ITranslationHelper.cs create mode 100644 src/SMAPI/LogLevel.cs create mode 100644 src/SMAPI/Metadata/CoreAssets.cs create mode 100644 src/SMAPI/Metadata/InstructionMetadata.cs create mode 100644 src/SMAPI/Mod.cs create mode 100644 src/SMAPI/PatchMode.cs create mode 100644 src/SMAPI/Program.cs create mode 100644 src/SMAPI/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI/SemanticVersion.cs create mode 100644 src/SMAPI/StardewModdingAPI.config.json create mode 100644 src/SMAPI/StardewModdingAPI.csproj create mode 100644 src/SMAPI/Translation.cs create mode 100644 src/SMAPI/Utilities/SButton.cs create mode 100644 src/SMAPI/Utilities/SDate.cs create mode 100644 src/SMAPI/icon.ico create mode 100644 src/SMAPI/packages.config create mode 100644 src/SMAPI/steam_appid.txt create mode 100644 src/SMAPI/unix-launcher.sh delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj delete mode 100644 src/StardewModdingAPI.Installer/Enums/Platform.cs delete mode 100644 src/StardewModdingAPI.Installer/Enums/ScriptAction.cs delete mode 100644 src/StardewModdingAPI.Installer/InteractiveInstaller.cs delete mode 100644 src/StardewModdingAPI.Installer/Program.cs delete mode 100644 src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj delete mode 100644 src/StardewModdingAPI.Installer/readme.txt delete mode 100644 src/StardewModdingAPI.ModBuildConfig/README.md delete mode 100644 src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn delete mode 100644 src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png delete mode 100644 src/StardewModdingAPI.ModBuildConfig/build/smapi.targets delete mode 100644 src/StardewModdingAPI.ModBuildConfig/package.nuspec delete mode 100644 src/StardewModdingAPI.ModBuildConfig/release-notes.md delete mode 100644 src/StardewModdingAPI.Models/ModInfoModel.cs delete mode 100644 src/StardewModdingAPI.Models/ModSeachModel.cs delete mode 100644 src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems delete mode 100644 src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj delete mode 100644 src/StardewModdingAPI.Tests/Core/ModResolverTests.cs delete mode 100644 src/StardewModdingAPI.Tests/Core/TranslationTests.cs delete mode 100644 src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.Tests/Sample.cs delete mode 100644 src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj delete mode 100644 src/StardewModdingAPI.Tests/Utilities/SDateTests.cs delete mode 100644 src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs delete mode 100644 src/StardewModdingAPI.Tests/packages.config delete mode 100644 src/StardewModdingAPI.Web/Controllers/ModsController.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/VersionConstraint.cs delete mode 100644 src/StardewModdingAPI.Web/Program.cs delete mode 100644 src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.Web/Properties/launchSettings.json delete mode 100644 src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj delete mode 100644 src/StardewModdingAPI.Web/Startup.cs delete mode 100644 src/StardewModdingAPI.Web/appsettings.Development.json delete mode 100644 src/StardewModdingAPI.Web/appsettings.json delete mode 100644 src/StardewModdingAPI.sln delete mode 100644 src/StardewModdingAPI.sln.DotSettings delete mode 100644 src/StardewModdingAPI/App.config delete mode 100644 src/StardewModdingAPI/Constants.cs delete mode 100644 src/StardewModdingAPI/ContentSource.cs delete mode 100644 src/StardewModdingAPI/Context.cs delete mode 100644 src/StardewModdingAPI/Events/ChangeType.cs delete mode 100644 src/StardewModdingAPI/Events/ContentEvents.cs delete mode 100644 src/StardewModdingAPI/Events/ControlEvents.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsInput.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsIntChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsKeyPressed.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsLevelUp.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsValueChanged.cs delete mode 100644 src/StardewModdingAPI/Events/GameEvents.cs delete mode 100644 src/StardewModdingAPI/Events/GraphicsEvents.cs delete mode 100644 src/StardewModdingAPI/Events/InputEvents.cs delete mode 100644 src/StardewModdingAPI/Events/ItemStackChange.cs delete mode 100644 src/StardewModdingAPI/Events/LocationEvents.cs delete mode 100644 src/StardewModdingAPI/Events/MenuEvents.cs delete mode 100644 src/StardewModdingAPI/Events/MineEvents.cs delete mode 100644 src/StardewModdingAPI/Events/PlayerEvents.cs delete mode 100644 src/StardewModdingAPI/Events/SaveEvents.cs delete mode 100644 src/StardewModdingAPI/Events/TimeEvents.cs delete mode 100644 src/StardewModdingAPI/Framework/Command.cs delete mode 100644 src/StardewModdingAPI/Framework/CommandManager.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/AssetData.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/AssetInfo.cs delete mode 100644 src/StardewModdingAPI/Framework/ContentManagerShim.cs delete mode 100644 src/StardewModdingAPI/Framework/CursorPosition.cs delete mode 100644 src/StardewModdingAPI/Framework/DeprecationLevel.cs delete mode 100644 src/StardewModdingAPI/Framework/DeprecationManager.cs delete mode 100644 src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs delete mode 100644 src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs delete mode 100644 src/StardewModdingAPI/Framework/Exceptions/SParseException.cs delete mode 100644 src/StardewModdingAPI/Framework/GameVersion.cs delete mode 100644 src/StardewModdingAPI/Framework/IModMetadata.cs delete mode 100644 src/StardewModdingAPI/Framework/InternalExtensions.cs delete mode 100644 src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs delete mode 100644 src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs delete mode 100644 src/StardewModdingAPI/Framework/Logging/LogFileManager.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Platform.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs delete mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs delete mode 100644 src/StardewModdingAPI/Framework/ModRegistry.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/Manifest.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/ManifestDependency.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/ModCompatibility.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/ModDataID.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/ModDataRecord.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/ModStatus.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/SConfig.cs delete mode 100644 src/StardewModdingAPI/Framework/Monitor.cs delete mode 100644 src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs delete mode 100644 src/StardewModdingAPI/Framework/Reflection/PrivateField.cs delete mode 100644 src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs delete mode 100644 src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs delete mode 100644 src/StardewModdingAPI/Framework/Reflection/Reflector.cs delete mode 100644 src/StardewModdingAPI/Framework/RequestExitDelegate.cs delete mode 100644 src/StardewModdingAPI/Framework/SContentManager.cs delete mode 100644 src/StardewModdingAPI/Framework/SGame.cs delete mode 100644 src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs delete mode 100644 src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs delete mode 100644 src/StardewModdingAPI/Framework/Utilities/ContextHash.cs delete mode 100644 src/StardewModdingAPI/Framework/Utilities/Countdown.cs delete mode 100644 src/StardewModdingAPI/Framework/WebApiClient.cs delete mode 100644 src/StardewModdingAPI/IAssetData.cs delete mode 100644 src/StardewModdingAPI/IAssetDataForDictionary.cs delete mode 100644 src/StardewModdingAPI/IAssetDataForImage.cs delete mode 100644 src/StardewModdingAPI/IAssetEditor.cs delete mode 100644 src/StardewModdingAPI/IAssetInfo.cs delete mode 100644 src/StardewModdingAPI/IAssetLoader.cs delete mode 100644 src/StardewModdingAPI/ICommandHelper.cs delete mode 100644 src/StardewModdingAPI/IContentHelper.cs delete mode 100644 src/StardewModdingAPI/ICursorPosition.cs delete mode 100644 src/StardewModdingAPI/IManifest.cs delete mode 100644 src/StardewModdingAPI/IManifestDependency.cs delete mode 100644 src/StardewModdingAPI/IMod.cs delete mode 100644 src/StardewModdingAPI/IModHelper.cs delete mode 100644 src/StardewModdingAPI/IModLinked.cs delete mode 100644 src/StardewModdingAPI/IModRegistry.cs delete mode 100644 src/StardewModdingAPI/IMonitor.cs delete mode 100644 src/StardewModdingAPI/IPrivateField.cs delete mode 100644 src/StardewModdingAPI/IPrivateMethod.cs delete mode 100644 src/StardewModdingAPI/IPrivateProperty.cs delete mode 100644 src/StardewModdingAPI/IReflectionHelper.cs delete mode 100644 src/StardewModdingAPI/ISemanticVersion.cs delete mode 100644 src/StardewModdingAPI/ITranslationHelper.cs delete mode 100644 src/StardewModdingAPI/LogLevel.cs delete mode 100644 src/StardewModdingAPI/Metadata/CoreAssets.cs delete mode 100644 src/StardewModdingAPI/Metadata/InstructionMetadata.cs delete mode 100644 src/StardewModdingAPI/Mod.cs delete mode 100644 src/StardewModdingAPI/PatchMode.cs delete mode 100644 src/StardewModdingAPI/Program.cs delete mode 100644 src/StardewModdingAPI/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI/SemanticVersion.cs delete mode 100644 src/StardewModdingAPI/StardewModdingAPI.config.json delete mode 100644 src/StardewModdingAPI/StardewModdingAPI.csproj delete mode 100644 src/StardewModdingAPI/Translation.cs delete mode 100644 src/StardewModdingAPI/Utilities/SButton.cs delete mode 100644 src/StardewModdingAPI/Utilities/SDate.cs delete mode 100644 src/StardewModdingAPI/icon.ico delete mode 100644 src/StardewModdingAPI/packages.config delete mode 100644 src/StardewModdingAPI/steam_appid.txt delete mode 100644 src/StardewModdingAPI/unix-launcher.sh delete mode 100644 src/common.targets delete mode 100644 src/prepare-install-package.targets (limited to 'src/StardewModdingAPI.Tests/packages.config') diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a5cdcf97 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,68 @@ +# topmost editorconfig +root: true + +########## +## General formatting +## documentation: http://editorconfig.org +########## +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.csproj] +indent_size = 2 +insert_final_newline = false + +[*.json] +indent_size = 2 + +########## +## C# formatting +## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +########## +[*.cs] + +#sort 'system' usings first +dotnet_sort_system_directives_first = true + +# use 'this.' qualifier +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:error +dotnet_style_qualification_for_method = true:error +dotnet_style_qualification_for_event = true:error + +# use language keywords (like int) instead of type (like Int32) +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# don't use 'var' for language keywords +csharp_style_var_for_built_in_types = false:error + +# suggest modern C# features where simpler +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion + +# prefer method block bodies +csharp_style_expression_bodied_methods = false:suggestion +csharp_style_expression_bodied_constructors = false:suggestion + +# prefer property expression bodies +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion + +# prefer inline out variables +csharp_style_inlined_variable_declaration = true:warning + +# avoid superfluous braces +csharp_prefer_braces = false:suggestion diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 52d47a4b..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,17 +0,0 @@ -Do you want to... - -* **Ask for help using SMAPI?** - Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375), - don't create a GitHub issue. - -* **Report a bug?** - Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375) - instead, unless you're sure it's a bug in SMAPI itself. - -* **Submit a pull request?** - Pull requests are welcome! If you're submitting a new feature, it's best to discuss first to make - sure it'll be accepted. Feel free to come chat in [#modding on Discord](https://discord.gg/kH55QXP) - or post in the [SMAPI support thread](http://community.playstarbound.com/threads/108375). - - Documenting your code and using the same formatting conventions is appreciated, but don't worry too - much about it. We'll fix up the code after we accept the pull request if needed. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 02bbb60b..00000000 --- a/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 01dd77ce..00000000 --- a/README.md +++ /dev/null @@ -1,179 +0,0 @@ -![](docs/imgs/SMAPI.png) - -## Contents -* [What is SMAPI?](#what-is-smapi) -* **[For players](#for-players)** -* **[For mod developers](#for-mod-developers)** -* [For SMAPI developers](#for-smapi-developers) - * [Compiling from source](#compiling-from-source) - * [Debugging a local build](#debugging-a-local-build) - * [Preparing a release](#preparing-a-release) -* [Advanced usage](#advanced-usage) - * [Configuration file](#configuration-file) - * [Command-line arguments](#command-line-arguments) - -## What is SMAPI? -**SMAPI** is an [open-source](LICENSE) modding API for [Stardew Valley](http://stardewvalley.net/) -that lets you play the game with mods. It's safely installed alongside the game's executable, and -doesn't change any of your game files. It serves five main purposes: - -1. **Load mods into the game.** - _SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't - possible without SMAPI to load them.)_ - -2. **Provide APIs and events for mods.** - _SMAPI provides low-level APIs and events which let mods interact with the game in ways they - otherwise couldn't._ - -3. **Rewrite mods for crossplatform compatibility.** - _SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows - without the mods needing to handle differences between the Linux/Mac and Windows versions of the - game._ - -4. **Rewrite mods to update them.** - _SMAPI detects when a mod accesses part of the game that changed in a recent update which - affects many mods, and rewrites the mod so it's compatible._ - -5. **Intercept errors.** - _SMAPI intercepts errors that happen in the game, displays the error details in the console - window, and in most cases automatically recovers the game. This prevents mods from accidentally - crashing the game, and makes it possible to troubleshoot errors in the game itself that would - otherwise show a generic 'program has stopped working' type of message._ - -## For players -* [Intro & FAQs](http://stardewvalleywiki.com/Modding:Player_FAQs) -* [Installing SMAPI](http://stardewvalleywiki.com/Modding:Installing_SMAPI) -* [Release notes](release-notes.md#release-notes) -* Need help? Come [chat on Discord](https://discord.gg/KCJHWhX) or [post in the support forums](http://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/). - _Please don't submit issues on GitHub for support questions._ - -## For mod developers -* [Modding documentation](http://stardewvalleywiki.com/Modding:Index) -* [Release notes](release-notes.md#release-notes) -* [Chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other modders - -## For SMAPI developers -_This section is about compiling SMAPI itself from source. If you don't know what that means, this -section isn't relevant to you; see the previous sections to use or create mods._ - -### Compiling from source -Using an official SMAPI release is recommended for most users. - -SMAPI uses some C# 7 code, so you'll need at least -[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, -[MonoDevelop 7.0](http://www.monodevelop.com/) on Linux, -[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent -IDE to compile it. It uses build configuration derived from the -[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect -your current OS automatically and load the correct references. Compile output will be placed in a -`bin` folder at the root of the git repository. - -### Debugging a local build -Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting -the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch -SMAPI with the debugger attached, so you can intercept errors and step through the code being -executed. This doesn't work in MonoDevelop on Linux, unfortunately. - -### Preparing a release -To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See -[crossplatforming info](http://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) -on the wiki for the first-time setup. - -1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a - [semantic version](http://semver.org). Recommended format: - - build type | format | example - :--------- | :-------------------------------- | :------ - dev build | `-alpha.` | `2.0-alpha.20171230` - prerelease | `-prerelease.` | `2.0-prerelease.2` - release | `` | `2.0` - -2. In Windows: - 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). - 2. Transfer the `SMAPI ` folder to Linux or Mac. - _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, - since we need to set Unix file permissions that Windows won't save._ - -2. In Linux or Mac: - 1. Rebuild the solution in _Release_ mode. - 2. Copy `bin/internal/Packaged/Mono` into the `SMAPI ` folder. - 3. If you did everything right so far, you should have a folder like this: - - ``` - SMAPI-2.x/ - install.exe - readme.txt - internal/ - Mono/ - Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI - StardewModdingAPI.AssemblyRewriters.dll - StardewModdingAPI.config.json - StardewModdingAPI.exe - StardewModdingAPI.pdb - StardewModdingAPI.xml - steam_appid.txt - System.Numerics.dll - System.Runtime.Caching.dll - System.ValueTuple.dll - Windows/ - Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI.AssemblyRewriters.dll - StardewModdingAPI.config.json - StardewModdingAPI.exe - StardewModdingAPI.pdb - StardewModdingAPI.xml - System.ValueTuple.dll - steam_appid.txt - ``` - 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. - 5. Copy & paste the `SMAPI ` folder as `SMAPI for developers`. - 6. In the `SMAPI ` folder... - * edit `internal/Mono/StardewModdingAPI.config.json` and - `internal/Windows/StardewModdingAPI.config.json` to disable developer mode; - * delete `internal/Windows/StardewModdingAPI.xml`. - 7. Compress the two folders into `SMAPI .zip` and `SMAPI for developers.zip`. - -## Advanced usage -### Configuration file -You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your -game folder. - -Basic fields: - -field | purpose ------------------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). -`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. -`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. - -### Command-line arguments -The SMAPI installer recognises three command-line arguments: - -argument | purpose --------- | ------- -`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. -`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. -`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. - -SMAPI itself recognises two arguments, but these are intended for internal use or testing and may -change without warning. - -argument | purpose --------- | ------- -`--log-path "path"` | The relative or absolute path of the log file SMAPI should write. -`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) - -### Compile flags -SMAPI uses a small number of conditional compilation constants, which you can set by editing the -`` element in `StardewModdingAPI.csproj`. Supported constants: - -flag | purpose ----- | ------- -`SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs new file mode 100644 index 00000000..196d67c5 --- /dev/null +++ b/build/GlobalAssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: ComVisible(false)] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/build/common.targets b/build/common.targets new file mode 100644 index 00000000..ee138524 --- /dev/null +++ b/build/common.targets @@ -0,0 +1,107 @@ + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + $(HOME)/.steam/steam/steamapps/common/Stardew Valley + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + + + + + $(DefineConstants);SMAPI_FOR_WINDOWS + + + + False + + + False + + + False + + + False + + + $(GamePath)\Stardew Valley.exe + False + + + $(GamePath)\xTile.dll + False + False + + + + + + $(DefineConstants);SMAPI_FOR_UNIX + + + + $(GamePath)\MonoGame.Framework.dll + False + False + + + $(GamePath)\StardewValley.exe + False + + + $(GamePath)\xTile.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets new file mode 100644 index 00000000..f2a2b23c --- /dev/null +++ b/build/prepare-install-package.targets @@ -0,0 +1,49 @@ + + + + + $(SolutionDir)\..\bin\$(Configuration)\SMAPI + $(SolutionDir)\..\bin\Packaged + $(PackagePath)\internal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..52d47a4b --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,17 @@ +Do you want to... + +* **Ask for help using SMAPI?** + Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375), + don't create a GitHub issue. + +* **Report a bug?** + Please post a message in the [SMAPI support thread](http://community.playstarbound.com/threads/108375) + instead, unless you're sure it's a bug in SMAPI itself. + +* **Submit a pull request?** + Pull requests are welcome! If you're submitting a new feature, it's best to discuss first to make + sure it'll be accepted. Feel free to come chat in [#modding on Discord](https://discord.gg/kH55QXP) + or post in the [SMAPI support thread](http://community.playstarbound.com/threads/108375). + + Documenting your code and using the same formatting conventions is appreciated, but don't worry too + much about it. We'll fix up the code after we accept the pull request if needed. diff --git a/docs/LICENSE.md b/docs/LICENSE.md new file mode 100644 index 00000000..02bbb60b --- /dev/null +++ b/docs/LICENSE.md @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..01dd77ce --- /dev/null +++ b/docs/README.md @@ -0,0 +1,179 @@ +![](docs/imgs/SMAPI.png) + +## Contents +* [What is SMAPI?](#what-is-smapi) +* **[For players](#for-players)** +* **[For mod developers](#for-mod-developers)** +* [For SMAPI developers](#for-smapi-developers) + * [Compiling from source](#compiling-from-source) + * [Debugging a local build](#debugging-a-local-build) + * [Preparing a release](#preparing-a-release) +* [Advanced usage](#advanced-usage) + * [Configuration file](#configuration-file) + * [Command-line arguments](#command-line-arguments) + +## What is SMAPI? +**SMAPI** is an [open-source](LICENSE) modding API for [Stardew Valley](http://stardewvalley.net/) +that lets you play the game with mods. It's safely installed alongside the game's executable, and +doesn't change any of your game files. It serves five main purposes: + +1. **Load mods into the game.** + _SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't + possible without SMAPI to load them.)_ + +2. **Provide APIs and events for mods.** + _SMAPI provides low-level APIs and events which let mods interact with the game in ways they + otherwise couldn't._ + +3. **Rewrite mods for crossplatform compatibility.** + _SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows + without the mods needing to handle differences between the Linux/Mac and Windows versions of the + game._ + +4. **Rewrite mods to update them.** + _SMAPI detects when a mod accesses part of the game that changed in a recent update which + affects many mods, and rewrites the mod so it's compatible._ + +5. **Intercept errors.** + _SMAPI intercepts errors that happen in the game, displays the error details in the console + window, and in most cases automatically recovers the game. This prevents mods from accidentally + crashing the game, and makes it possible to troubleshoot errors in the game itself that would + otherwise show a generic 'program has stopped working' type of message._ + +## For players +* [Intro & FAQs](http://stardewvalleywiki.com/Modding:Player_FAQs) +* [Installing SMAPI](http://stardewvalleywiki.com/Modding:Installing_SMAPI) +* [Release notes](release-notes.md#release-notes) +* Need help? Come [chat on Discord](https://discord.gg/KCJHWhX) or [post in the support forums](http://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/). + _Please don't submit issues on GitHub for support questions._ + +## For mod developers +* [Modding documentation](http://stardewvalleywiki.com/Modding:Index) +* [Release notes](release-notes.md#release-notes) +* [Chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other modders + +## For SMAPI developers +_This section is about compiling SMAPI itself from source. If you don't know what that means, this +section isn't relevant to you; see the previous sections to use or create mods._ + +### Compiling from source +Using an official SMAPI release is recommended for most users. + +SMAPI uses some C# 7 code, so you'll need at least +[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, +[MonoDevelop 7.0](http://www.monodevelop.com/) on Linux, +[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent +IDE to compile it. It uses build configuration derived from the +[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect +your current OS automatically and load the correct references. Compile output will be placed in a +`bin` folder at the root of the git repository. + +### Debugging a local build +Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting +the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch +SMAPI with the debugger attached, so you can intercept errors and step through the code being +executed. This doesn't work in MonoDevelop on Linux, unfortunately. + +### Preparing a release +To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See +[crossplatforming info](http://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) +on the wiki for the first-time setup. + +1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a + [semantic version](http://semver.org). Recommended format: + + build type | format | example + :--------- | :-------------------------------- | :------ + dev build | `-alpha.` | `2.0-alpha.20171230` + prerelease | `-prerelease.` | `2.0-prerelease.2` + release | `` | `2.0` + +2. In Windows: + 1. Rebuild the solution in _Release_ mode. + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). + 2. Transfer the `SMAPI ` folder to Linux or Mac. + _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, + since we need to set Unix file permissions that Windows won't save._ + +2. In Linux or Mac: + 1. Rebuild the solution in _Release_ mode. + 2. Copy `bin/internal/Packaged/Mono` into the `SMAPI ` folder. + 3. If you did everything right so far, you should have a folder like this: + + ``` + SMAPI-2.x/ + install.exe + readme.txt + internal/ + Mono/ + Mods/* + Mono.Cecil.dll + Newtonsoft.Json.dll + StardewModdingAPI + StardewModdingAPI.AssemblyRewriters.dll + StardewModdingAPI.config.json + StardewModdingAPI.exe + StardewModdingAPI.pdb + StardewModdingAPI.xml + steam_appid.txt + System.Numerics.dll + System.Runtime.Caching.dll + System.ValueTuple.dll + Windows/ + Mods/* + Mono.Cecil.dll + Newtonsoft.Json.dll + StardewModdingAPI.AssemblyRewriters.dll + StardewModdingAPI.config.json + StardewModdingAPI.exe + StardewModdingAPI.pdb + StardewModdingAPI.xml + System.ValueTuple.dll + steam_appid.txt + ``` + 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. + 5. Copy & paste the `SMAPI ` folder as `SMAPI for developers`. + 6. In the `SMAPI ` folder... + * edit `internal/Mono/StardewModdingAPI.config.json` and + `internal/Windows/StardewModdingAPI.config.json` to disable developer mode; + * delete `internal/Windows/StardewModdingAPI.xml`. + 7. Compress the two folders into `SMAPI .zip` and `SMAPI for developers.zip`. + +## Advanced usage +### Configuration file +You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your +game folder. + +Basic fields: + +field | purpose +----------------- | ------- +`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). +`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. +`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. + +### Command-line arguments +The SMAPI installer recognises three command-line arguments: + +argument | purpose +-------- | ------- +`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. +`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. +`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. + +SMAPI itself recognises two arguments, but these are intended for internal use or testing and may +change without warning. + +argument | purpose +-------- | ------- +`--log-path "path"` | The relative or absolute path of the log file SMAPI should write. +`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) + +### Compile flags +SMAPI uses a small number of conditional compilation constants, which you can set by editing the +`` element in `StardewModdingAPI.csproj`. Supported constants: + +flag | purpose +---- | ------- +`SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. diff --git a/docs/release-notes.md b/docs/release-notes.md new file mode 100644 index 00000000..5b102df3 --- /dev/null +++ b/docs/release-notes.md @@ -0,0 +1,471 @@ +# Release notes +## 2.0 (upcoming) + + +For players: +* SMAPI now alerts you when mods have new versions available. +* SMAPI now warns you about mods which may impact game stability or compatibility. +* The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color. +* Renamed installer folder to avoid confusion. +* Updated compatibility list. +* Fixed update check errors on Linux/Mac. +* Fixed collection-changed errors during startup for some players. + +For mod developers: +* Added support for editing, injecting, and reloading XNB data loaded by the game at any time. + _This let SMAPI mods do anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ +* Added support for automatic mod update checks. + _Add your Chucklefish, GitHub, or Nexus mod ID to the manifest and SMAPI will check for updates automatically. SMAPI will retroactively enable updates for most existing mods._ +* Added unified input events. + _The new `InputEvents` combine keyboard + mouse + controller input into one event and constant for easy handling, add metadata like the cursor position and grab tile to support click handling._ +* Added support for suppressing input. + _You can prevent the game from receiving input via `InputEvents`, to enable new scenarios like action highjacking and UI overlays._ +* Added support for optional dependencies. +* Added support for specifying the mod version as a string (like `"1.0-alpha"`) in `manifest.json`. +* Added day of week to `SDate` instances. +* Added `IEquatable` to `ISemanticVersion`. +* Removed the TrainerMod's `save` and `load` commands. +* Removed all deprecated code. +* Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. +* Removed support for mods with a non-unique `UniqueID` value in their manifest. +* Removed access to SMAPI internals through the reflection helper, to discourage fragile mods. +* Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro. + +For power users: +* Added command-line arguments to the SMAPI installer so it can be scripted. + +For SMAPI developers: +* Significantly refactored SMAPI to support changes in 2.0 and upcoming releases. +* Overhauled `StardewModdingAPI.config.json` format to support mod data like update keys. +* Removed SMAPI 1._x_ compatibility mode. + +## 1.15.4 +For players: +* Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader. +* Fixed errors in rare cases when a mod calculates an in-game date. + +For modders: +* Added UTC timestamp to log file. + +For SMAPI developers: +* Internal changes to support the upcoming SMAPI 2.0 release. + +## 1.15.3 +For players: +* Fixed mods being wrongly marked as duplicate in some cases. + +## 1.15.2 +For players: +* Improved errors when a mod DLL can't be loaded. +* Improved errors when using very old versions of Stardew Valley. +* Updated compatibility list. + +For mod developers: +* Added `Context.CanPlayerMove` property for mod convenience. +* Added content helper properties for the game's current language. +* Fixed `Context.IsPlayerFree` being false if the player is performing an action. +* Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. +* Fixed `SemanticVersion` not being deserialisable through Json.NET. +* Fixed terminal not launching on Xfce Linux. + +For SMAPI developers: +* Internal changes to support the upcoming SMAPI 2.0 release. + +## 1.15.1 +For players: +* Fixed controller mod input broken in 1.15. +* Fixed TrainerMod packaging unneeded files. + +For modders: +* Fixed mod registry lookups by unique ID not being case-insensitive. + +## 1.15 +For players: +* Cleaned up SMAPI console a bit. +* Revamped TrainerMod's item commands: + * `player_add` is a new command to add any item to your inventory (including tools, weapons, equipment, craftables, wallpaper, etc). This replaces the former `player_additem`, `player_addring`, and `player_addweapon`. + * `list_items` now shows all items in the game. You can search by item type like `list_items weapon`, or search by item name like `list_items galaxy sword`. + * `list_items` now also matches translated item names when playing in another language. + * `list_item_types` is a new command to see a list of item types. +* Fixed unhelpful error when a `config.json` is invalid. +* Fixed rare crash when window loses focus for a few players (further to fix in 1.14). +* Fixed invalid `ObjectInformation.xnb` causing a flood of warnings; SMAPI now shows one error instead. +* Updated mod compatibility list. + +For modders: +* Added `SDate` utility for in-game date calculations (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)). +* Added support for minimum dependency versions in `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). +* Added more useful logging when loading mods. +* Added a `ModID` property to all mod helpers for extension methods. +* Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code. +* Fixed `SemanticVersion` parsing some invalid versions into close approximations (like `1.apple` → `1.0-apple`). +* Fixed `SemanticVersion` not treating hyphens as separators when comparing prerelease tags. + _(While that was technically correct, it leads to unintuitive behaviour like sorting `-alpha-2` _after_ `-alpha-10`, even though `-alpha.2` sorts before `-alpha.10`.)_ +* Fixed corrupted state exceptions not being logged by SMAPI. +* Increased all deprecations to _pending removal_. + +For SMAPI developers: +* Added SMAPI 2.0 compile mode, for testing how mods will work with SMAPI 2.0. +* Added prototype SMAPI 2.0 feature to override XNB files (not enabled for mods yet). +* Added prototype SMAPI 2.0 support for version strings in `manifest.json` (not recommended for mods yet). +* Compiling SMAPI now uses your `~/stardewvalley.targets` file if present. + +## 1.14 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.14). + +For players: +* SMAPI now shows friendly errors when... + * it can't detect the game; + * a mod dependency is missing (if it's listed in the mod manifest); + * you have Stardew Valley 1.11 or earlier (which aren't compatible); + * you run `install.exe` from within the downloaded zip file. +* Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. +* Fixed `libgdiplus.dylib` errors for some players on Mac. +* Fixed rare crash when window loses focus for a few players. +* Bumped minimum game version to 1.2.30. +* Updated mod compatibility list. + +For modders: +* You can now add dependencies to `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). +* You can now translate your mod (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)). +* You can now load unpacked `.tbin` files from your mod folder through the content API. +* SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. + _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ +* Added `Context.IsPlayerFree` for mods that need to check if the player can act (i.e. save is loaded, no menu is displayed, no cutscene is in progress, etc). +* Added `Context.IsInDrawLoop` for specialised mods. +* Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. +* Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. +* Fixed `debug` command output not printed to console. +* Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most modders think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead. + +## 1.13.1 +For players: +* Fixed errors when loading a mod with no name or version. +* Fixed mods with no manifest `Name` field having no name (SMAPI will now shows their filename). + +## 1.13 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.12...1.13). + +For players: +* SMAPI now recovers better from mod draw errors and detects when the error is irrecoverable. +* SMAPI now recovers automatically from errors in the game loop when possible. +* SMAPI now remembers if your game crashed and offers help next time you launch it. +* Fixed installer sometimes finding redundant game paths. +* Fixed save events not being raised after the first day on Linux/Mac. +* Fixed error on Linux/Mac when a mod loads a PNG immediately after the save is loaded. +* Updated mod compatibility list for Stardew Valley 1.2. + +For mod developers: +* Added a `Context.IsWorldReady` flag for mods to use. + _This indicates whether a save is loaded and the world is finished initialising, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful for events which can be raised before the world is loaded (like update tick)._ +* Added a `debug` console command which lets you run the game's debug commands (e.g. `debug warp FarmHouse 1 1` warps you to the farmhouse). +* Added basic context info to logs to simplify troubleshooting. +* Added a `Mod.Dispose` method which can be overriden to clean up before exit. This method isn't guaranteed to be called on every exit. +* Deprecated mods that don't have a `Name`, `Version`, or `UniqueID` in their manifest. These will be required in SMAPI 2.0. +* Deprecated `GameEvents.GameLoaded` and `GameEvents.FirstUpdateTick`. You can move any affected code into your mod's `Entry` method. +* Fixed maps not recognising custom tilesheets added through the SMAPI content API. +* Internal refactoring for upcoming features. + +## 1.12 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.11...1.12). + +For players: +* The installer now lets you choose the install path if you have multiple copies of the game, instead of using the first path found. +* Fixed mod draw errors breaking the game. +* Fixed mods on Linux/Mac no longer working after the game saves. +* Fixed `libgdiplus.dylib` errors on Mac when mods read PNG files. +* Adopted pufferchick. + +For mod developers: +* Unknown mod manifest fields are now stored in `IManifest::ExtraFields`. +* The content API now defaults to `ContentSource.ModFolder`. +* Fixed content API error when loading a PNG during early game init (e.g. in mod's `Entry`). +* Fixed content API error when loading an XNB from the mod folder on Mac. + +## 1.11 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...1.11). + +For players: +* SMAPI now detects issues in `ObjectInformation.xnb` files caused by outdated XNB mods. +* Errors when loading a save are now shown in the SMAPI console. +* Improved console logging performance. +* Fixed errors during game update causing the game to hang. +* Fixed errors due to mod events triggering during game save in Stardew Valley 1.2. + +For mod developers: +* Added a content API which loads custom textures/maps/data from the mod's folder (`.xnb` or `.png` format) or game content. +* `Console.Out` messages are now written to the log file. +* `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly. +* Fixed value-changed events being raised when the player loads a save due to values being initialised. + +## 1.10 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.9...1.10). + +For players: +* Updated to Stardew Valley 1.2. +* Added logic to rewrite many mods for compatibility with game updates, though some mods may still need an update. +* Fixed `SEHException` errors affecting some players. +* Fixed issue where SMAPI didn't unlock some files on exit. +* Fixed rare issue where the installer would crash trying to delete a bundled mod from `%appdata%`. +* Improved TrainerMod commands: + * Added `world_setyear` to change the current year. + * Replaced `player_addmelee` with `player_addweapon` with support for non-melee weapons. + +For mod developers: +* Mods are now initialised after the `Initialize`/`LoadContent` phase, which means the `GameEvents.Initialize` and `GameEvents.LoadContent` events are deprecated. You can move any logic in those methods to your mod's `Entry` method. +* Added `IsBetween` and string overloads to the `ISemanticVersion` methods. +* Fixed mouse-changed event never updating prior mouse position. +* Fixed `monitor.ExitGameImmediately` not working correctly. +* Fixed `Constants.SaveFolderName` not set for a new game until the save is created. + +## 1.9 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9). + +For players: +* SMAPI now detects incompatible mods and disables them before they cause problems. +* SMAPI now allows mods nested into an otherwise empty parent folder (like `Mods\ModName-1.0\ModName\manifest.json`), since that's a common default behaviour when unpacking mods. +* The installer now detects if you need to update .NET Framework before installing SMAPI. +* The installer now detects if you need to run the game at least once (to let it perform first-time setup) before installing SMAPI. +* The installer on Linux now finds games installed to `~/.steam/steam/steamapps/common/Stardew Valley` too. +* The installer now removes old SMAPI logs to prevent confusion. +* The console now has simpler error messages. +* The console now has improved command handling & feedback. +* The console no longer shows the game's debug output (unless you use a _SMAPI for developers_ build). +* Fixed the game-needs-an-update error not pausing before exit. +* Fixed installer errors for some players when deleting files. +* Fixed installer not ignoring potential game folders that don't contain a Stardew Valley exe. +* Fixed installer not recognising Linux/Mac paths starting with `~/` or containing an escaped space. +* Fixed TrainerMod letting you add invalid items which may crash the game. +* Fixed TrainerMod's `world_downminelevel` command not working. +* Fixed rare issue where mod dependencies would override SMAPI dependencies and cause unpredictable bugs. +* Fixed errors in mods' console command handlers crashing the game. + +For mod developers: +* Added a simpler API for console commands (see `helper.ConsoleCommands`). +* Added `TimeEvents.AfterDayStarted` event triggered when a day starts. This happens no matter how the day started (including new game, loaded save, or player went to bed). +* Added `ContentEvents.AfterLocaleChanged` event triggered when the player changes the content language (for the upcoming Stardew Valley 1.2). +* Added `SaveEvents.AfterReturnToTitle` event triggered when the player returns to the title screen (for the upcoming Stardew Valley 1.2). +* Added `helper.Reflection.GetPrivateProperty` method. +* Added a `--log-path` argument to specify the SMAPI log path during testing. +* SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings automatically, so mods no longer need to add a `StringEnumConverter` themselves for those. +* The SMAPI log now has a simpler filename. +* The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. +* The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing. +* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised. +* Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly. +* Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), + and all _notice_-level deprecations have been increased to _info_. +* Removed the experimental `IConfigFile`. + +For SMAPI developers: +* Added support for debugging SMAPI on Linux/Mac if supported by the editor. + +## 1.8 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.7...1.8). + +For players: +* Mods no longer generate `.cache` subfolders. +* Fixed multiple issues where mods failed during assembly loading. +* Tweaked install package to reduce confusion. + +For mod developers: +* The `SemanticVersion` constructor now accepts a string version. +* Increased deprecation level for `Extensions` to _pending removal_. +* **Warning:** `Assembly.GetExecutingAssembly().Location` will no longer reliably + return a valid path, because mod assemblies are loaded from memory when rewritten for + compatibility. This approach has been discouraged since SMAPI 1.3; use `helper.DirectoryPath` + instead. + +For SMAPI developers: +* Rewrote assembly loading from the ground up. The new implementation... + * is much simpler; + * eliminates the `.cache` folders by loading rewritten assemblies from memory; + * ensures DLLs are loaded in leaf-to-root order (i.e. dependencies first); + * improves dependent assembly resolution; + * no longer loads DLLs if they're not referenced; + * reduces log verbosity. + +## 1.7 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.6...1.7). + +For players: +* The console now shows the folder path where mods should be added. +* The console now shows deprecation warnings after the list of loaded mods (instead of intermingled). + +For mod developers: +* Added a mod registry which provides metadata about loaded mods. +* The `Entry(…)` method is now deferred until all mods are loaded. +* Fixed `SaveEvents.BeforeSave` and `.AfterSave` not triggering on days when the player shipped something. +* Fixed `PlayerEvents.LoadedGame` and `SaveEvents.AfterLoad` being fired before the world finishes initialising. +* Fixed some `LocationEvents`, `PlayerEvents`, and `TimeEvents` being fired during game startup. +* Increased deprecation levels for `SObject`, `LogWriter` (not `Log`), and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`) to _pending removal_. Increased deprecation levels for `Mod.PerSaveConfigFolder`, `Mod.PerSaveConfigPath`, and `Version.VersionString` to _info_. + +## 1.6 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.5...1.6). + +For players: +* Added console commands to open the game/data folders. +* Updated list of incompatible mods. +* Fixed `config.json` values being duplicated in some cases. +* Fixed some Linux users not being able to launch SMAPI from Steam. +* Fixed the installer not finding custom install paths on 32-bit Windows. +* Fixed error when loading a mod which was released with a `.cache` folder for a different platform. +* Fixed error when the console doesn't support colour. +* Fixed error when a mod reads a custom JSON file from a directory that doesn't exist. + +For mod developers: +* Added three events: `SaveEvents.BeforeSave`, `SaveEvents.AfterSave`, and `SaveEvents.AfterLoad`. +* Deprecated three events: + * `TimeEvents.OnNewDay` is unreliable; use `TimeEvents.DayOfMonthChanged` or `SaveEvents` instead. + * `PlayerEvents.LoadedGame` is replaced by `SaveEvents.AfterLoad`. + * `PlayerEvents.FarmerChanged` serves no purpose. + +For SMAPI developers: + * Added support for specifying a lower bound in mod incompatibility data. + * Added support for custom incompatible-mod error text. + * Fixed issue where `TrainerMod` used older logic to detect the game path. + +## 1.5 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.4...1.5). + +For players: + * Added an option to disable update checks. + * SMAPI will now show a friendly error with update links when you try to use a known incompatible mod version. + * Fixed an error when a mod uses the new reflection API on a missing field or method. + * Fixed an issue where mods weren't notified of a menu change if it changed while SMAPI was still notifying mods of the previous change. + +For developers: + * Deprecated `Version` in favour of `SemanticVersion`. + _This new implementation is [semver 2.0](http://semver.org/)-compliant, introduces `NewerThan(version)` and `OlderThan(version)` convenience methods, adds support for parsing a version string into a `SemanticVersion`, and fixes various bugs with the former implementation. This also replaces `Manifest` with `IManifest`._ + * Increased deprecation levels for `SObject`, `Extensions`, `LogWriter` (not `Log`), `SPlayer`, and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`). + +## 1.4 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.3...1.4). + +For players: + * SMAPI will now prevent mods from crashing your game with menu errors. + * The installer will now automatically detect most custom install paths. + * The installer will now automatically clean up old SMAPI files. + * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. + * Improved installer wording to reduce confusion. + * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. + * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. + * Fixed the SMAPI console not being shown on Linux and Mac. + +For developers: + * Added a reflection API (via `helper.Reflection`) that simplifies robust access to the game's private fields and methods. + * Added a searchable `list_items` console command to replace the `out_items`, `out_melee`, and `out_rings` commands. + * Added `TypeLoadException` details when intercepted by SMAPI. + * Fixed an issue where you couldn't debug into an assembly because it was copied into the `.cache` directory. That will now only happen if necessary. + +## 1.3 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.2...1.3). + +For players: + * You can now run most mods on any platform (e.g. run Windows mods on Linux/Mac). + * Fixed the normal uninstaller not removing files added by the 'SMAPI for developers' installer. + +## 1.2 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.1.1...1.2). + +For players: + * Fixed compatibility with some older mods. + * Fixed mod errors in most event handlers crashing the game. + * Fixed mod errors in some event handlers preventing other mods from receiving the same event. + * Fixed game crashing on startup with an audio error for some players. + +For developers: + * Improved logging to show `ReflectionTypeLoadException` details when it's caught by SMAPI. + +## 1.1 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.0...1.1.1). + +For players: + * Fixed console exiting immediately when some exceptions occur. + * Fixed an error in 1.0 when mod uses `config.json` but the file doesn't exist. + * Fixed critical errors being saved to a separate log file. + * Fixed compatibility with some older mods.1.1.1 + * Fixed race condition where some mods would sometimes crash because the game wasn't ready yet.1.1.1 + +For developers: + * Added new logging interface: + * easier to use; + * supports trace logs (written to the log file, but hidden in the console by default); + * messages are now listed in order; + * messages now show which mod logged them; + * more consistent and intuitive console color scheme. + * Added optional `MinimumApiVersion` to `manifest.json`. + * Added emergency interrupt feature for dangerous mods. + * Fixed deprecation warnings being repeated if the mod can't be identified.1.1.1 + +## 1.0 +See [log](https://github.com/Pathoschild/SMAPI/compare/0.40.1.1-3...1.0). + +For players: + * Added support for Linux and Mac. + * Added installer to automate adding & removing SMAPI. + * Added background update check on launch. + * Fixed missing `steam_appid.txt` file. + * Fixed some mod UIs disappearing at a non-default zoom level for some users. + * Removed undocumented support for mods in AppData folder **(breaking change)**. + * Removed `F2` debug mode. + +For mod developers: + * Added deprecation warnings. + * Added OS version to log. + * Added zoom-adjusted mouse position to mouse-changed event arguments. + * Added SMAPI code documentation. + * Switched to [semantic versioning](http://semver.org). + * Fixed mod versions not shown correctly in the log. + * Fixed misspelled field in `manifest.json` schema. + * Fixed some events getting wrong data. + * Simplified log output. + +For SMAPI developers: + * Simplified compiling from source. + * Formalised release process and added automated build packaging. + * Removed obsolete and unfinished code. + * Internal cleanup & refactoring. + +## 0.x +* 0.40.1.1 (2016-09-30, [log](https://github.com/Pathoschild/SMAPI/compare/0.40.0...0.40.1.1-3)) + * Added support for Stardew Valley 1.1. + +* 0.40.0 (2016-04-05, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.7...0.40.0)) + * Fixed an error that ocurred during minigames. + +* 0.39.7 (2016-04-04, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.6...0.39.7)) + * Added 'no check' graphics events that are triggered regardless of game's if checks. + +* 0.39.6 (2016-04-01, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.5...0.39.6)) + * Added game & SMAPI versions to log. + * Fixed conflict in graphics tick events. + * Bug fixes. + +* 0.39.5 (2016-03-30, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.4...0.39.5)) +* 0.39.4 (2016-03-29, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.3...0.39.4)) +* 0.39.3 (2016-03-28, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.2...0.39.3)) +* 0.39.2 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.1...0.39.2)) +* 0.39.1 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.8...0.39.1)) +* 0.38.8 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.7...0.38.8)) +* 0.38.7 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.6...0.38.7)) +* 0.38.6 (2016-03-22, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.5...0.38.6)) +* 0.38.5 (2016-03-22, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.4...0.38.5)) +* 0.38.4 (2016-03-21, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.3...0.38.4)) +* 0.38.3 (2016-03-21, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.2...0.38.3)) +* 0.38.2 (2016-03-21, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.0...0.38.2)) +* 0.38.0 (2016-03-20, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.1...0.38.0)) +* 0.38.1 (2016-03-20, [log](https://github.com/Pathoschild/SMAPI/compare/0.37.3...0.38.1)) +* 0.37.3 (2016-03-08, [log](https://github.com/Pathoschild/SMAPI/compare/0.37.2...0.37.3)) +* 0.37.2 (2016-03-07, [log](https://github.com/Pathoschild/SMAPI/compare/0.37.1...0.37.2)) +* 0.37.1 (2016-03-06, [log](https://github.com/Pathoschild/SMAPI/compare/0.36...0.37.1)) +* 0.36 (2016-03-04, [log](https://github.com/Pathoschild/SMAPI/compare/0.37...0.36)) +* 0.37 (2016-03-04, [log](https://github.com/Pathoschild/SMAPI/compare/0.35...0.37)) +* 0.35 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.34...0.35)) +* 0.34 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.33...0.34)) +* 0.33 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.32...0.33)) +* 0.32 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.31...0.32)) +* 0.31 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.3...0.31)) +* 0.3 (2016-03-01, [log](https://github.com/Pathoschild/SMAPI/compare/Alpha0.2...0.3)) +* 0.2 (2016-02-29, [log](https://github.com/Pathoschild/SMAPI/compare/Alpha0.1...Alpha0.2) +* 0.1 (2016-02-28) diff --git a/release-notes.md b/release-notes.md deleted file mode 100644 index 5b102df3..00000000 --- a/release-notes.md +++ /dev/null @@ -1,471 +0,0 @@ -# Release notes -## 2.0 (upcoming) - - -For players: -* SMAPI now alerts you when mods have new versions available. -* SMAPI now warns you about mods which may impact game stability or compatibility. -* The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color. -* Renamed installer folder to avoid confusion. -* Updated compatibility list. -* Fixed update check errors on Linux/Mac. -* Fixed collection-changed errors during startup for some players. - -For mod developers: -* Added support for editing, injecting, and reloading XNB data loaded by the game at any time. - _This let SMAPI mods do anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ -* Added support for automatic mod update checks. - _Add your Chucklefish, GitHub, or Nexus mod ID to the manifest and SMAPI will check for updates automatically. SMAPI will retroactively enable updates for most existing mods._ -* Added unified input events. - _The new `InputEvents` combine keyboard + mouse + controller input into one event and constant for easy handling, add metadata like the cursor position and grab tile to support click handling._ -* Added support for suppressing input. - _You can prevent the game from receiving input via `InputEvents`, to enable new scenarios like action highjacking and UI overlays._ -* Added support for optional dependencies. -* Added support for specifying the mod version as a string (like `"1.0-alpha"`) in `manifest.json`. -* Added day of week to `SDate` instances. -* Added `IEquatable` to `ISemanticVersion`. -* Removed the TrainerMod's `save` and `load` commands. -* Removed all deprecated code. -* Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. -* Removed support for mods with a non-unique `UniqueID` value in their manifest. -* Removed access to SMAPI internals through the reflection helper, to discourage fragile mods. -* Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro. - -For power users: -* Added command-line arguments to the SMAPI installer so it can be scripted. - -For SMAPI developers: -* Significantly refactored SMAPI to support changes in 2.0 and upcoming releases. -* Overhauled `StardewModdingAPI.config.json` format to support mod data like update keys. -* Removed SMAPI 1._x_ compatibility mode. - -## 1.15.4 -For players: -* Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader. -* Fixed errors in rare cases when a mod calculates an in-game date. - -For modders: -* Added UTC timestamp to log file. - -For SMAPI developers: -* Internal changes to support the upcoming SMAPI 2.0 release. - -## 1.15.3 -For players: -* Fixed mods being wrongly marked as duplicate in some cases. - -## 1.15.2 -For players: -* Improved errors when a mod DLL can't be loaded. -* Improved errors when using very old versions of Stardew Valley. -* Updated compatibility list. - -For mod developers: -* Added `Context.CanPlayerMove` property for mod convenience. -* Added content helper properties for the game's current language. -* Fixed `Context.IsPlayerFree` being false if the player is performing an action. -* Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. -* Fixed `SemanticVersion` not being deserialisable through Json.NET. -* Fixed terminal not launching on Xfce Linux. - -For SMAPI developers: -* Internal changes to support the upcoming SMAPI 2.0 release. - -## 1.15.1 -For players: -* Fixed controller mod input broken in 1.15. -* Fixed TrainerMod packaging unneeded files. - -For modders: -* Fixed mod registry lookups by unique ID not being case-insensitive. - -## 1.15 -For players: -* Cleaned up SMAPI console a bit. -* Revamped TrainerMod's item commands: - * `player_add` is a new command to add any item to your inventory (including tools, weapons, equipment, craftables, wallpaper, etc). This replaces the former `player_additem`, `player_addring`, and `player_addweapon`. - * `list_items` now shows all items in the game. You can search by item type like `list_items weapon`, or search by item name like `list_items galaxy sword`. - * `list_items` now also matches translated item names when playing in another language. - * `list_item_types` is a new command to see a list of item types. -* Fixed unhelpful error when a `config.json` is invalid. -* Fixed rare crash when window loses focus for a few players (further to fix in 1.14). -* Fixed invalid `ObjectInformation.xnb` causing a flood of warnings; SMAPI now shows one error instead. -* Updated mod compatibility list. - -For modders: -* Added `SDate` utility for in-game date calculations (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)). -* Added support for minimum dependency versions in `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). -* Added more useful logging when loading mods. -* Added a `ModID` property to all mod helpers for extension methods. -* Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code. -* Fixed `SemanticVersion` parsing some invalid versions into close approximations (like `1.apple` → `1.0-apple`). -* Fixed `SemanticVersion` not treating hyphens as separators when comparing prerelease tags. - _(While that was technically correct, it leads to unintuitive behaviour like sorting `-alpha-2` _after_ `-alpha-10`, even though `-alpha.2` sorts before `-alpha.10`.)_ -* Fixed corrupted state exceptions not being logged by SMAPI. -* Increased all deprecations to _pending removal_. - -For SMAPI developers: -* Added SMAPI 2.0 compile mode, for testing how mods will work with SMAPI 2.0. -* Added prototype SMAPI 2.0 feature to override XNB files (not enabled for mods yet). -* Added prototype SMAPI 2.0 support for version strings in `manifest.json` (not recommended for mods yet). -* Compiling SMAPI now uses your `~/stardewvalley.targets` file if present. - -## 1.14 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.14). - -For players: -* SMAPI now shows friendly errors when... - * it can't detect the game; - * a mod dependency is missing (if it's listed in the mod manifest); - * you have Stardew Valley 1.11 or earlier (which aren't compatible); - * you run `install.exe` from within the downloaded zip file. -* Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. -* Fixed `libgdiplus.dylib` errors for some players on Mac. -* Fixed rare crash when window loses focus for a few players. -* Bumped minimum game version to 1.2.30. -* Updated mod compatibility list. - -For modders: -* You can now add dependencies to `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). -* You can now translate your mod (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)). -* You can now load unpacked `.tbin` files from your mod folder through the content API. -* SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. - _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ -* Added `Context.IsPlayerFree` for mods that need to check if the player can act (i.e. save is loaded, no menu is displayed, no cutscene is in progress, etc). -* Added `Context.IsInDrawLoop` for specialised mods. -* Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. -* Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. -* Fixed `debug` command output not printed to console. -* Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most modders think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead. - -## 1.13.1 -For players: -* Fixed errors when loading a mod with no name or version. -* Fixed mods with no manifest `Name` field having no name (SMAPI will now shows their filename). - -## 1.13 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.12...1.13). - -For players: -* SMAPI now recovers better from mod draw errors and detects when the error is irrecoverable. -* SMAPI now recovers automatically from errors in the game loop when possible. -* SMAPI now remembers if your game crashed and offers help next time you launch it. -* Fixed installer sometimes finding redundant game paths. -* Fixed save events not being raised after the first day on Linux/Mac. -* Fixed error on Linux/Mac when a mod loads a PNG immediately after the save is loaded. -* Updated mod compatibility list for Stardew Valley 1.2. - -For mod developers: -* Added a `Context.IsWorldReady` flag for mods to use. - _This indicates whether a save is loaded and the world is finished initialising, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful for events which can be raised before the world is loaded (like update tick)._ -* Added a `debug` console command which lets you run the game's debug commands (e.g. `debug warp FarmHouse 1 1` warps you to the farmhouse). -* Added basic context info to logs to simplify troubleshooting. -* Added a `Mod.Dispose` method which can be overriden to clean up before exit. This method isn't guaranteed to be called on every exit. -* Deprecated mods that don't have a `Name`, `Version`, or `UniqueID` in their manifest. These will be required in SMAPI 2.0. -* Deprecated `GameEvents.GameLoaded` and `GameEvents.FirstUpdateTick`. You can move any affected code into your mod's `Entry` method. -* Fixed maps not recognising custom tilesheets added through the SMAPI content API. -* Internal refactoring for upcoming features. - -## 1.12 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.11...1.12). - -For players: -* The installer now lets you choose the install path if you have multiple copies of the game, instead of using the first path found. -* Fixed mod draw errors breaking the game. -* Fixed mods on Linux/Mac no longer working after the game saves. -* Fixed `libgdiplus.dylib` errors on Mac when mods read PNG files. -* Adopted pufferchick. - -For mod developers: -* Unknown mod manifest fields are now stored in `IManifest::ExtraFields`. -* The content API now defaults to `ContentSource.ModFolder`. -* Fixed content API error when loading a PNG during early game init (e.g. in mod's `Entry`). -* Fixed content API error when loading an XNB from the mod folder on Mac. - -## 1.11 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...1.11). - -For players: -* SMAPI now detects issues in `ObjectInformation.xnb` files caused by outdated XNB mods. -* Errors when loading a save are now shown in the SMAPI console. -* Improved console logging performance. -* Fixed errors during game update causing the game to hang. -* Fixed errors due to mod events triggering during game save in Stardew Valley 1.2. - -For mod developers: -* Added a content API which loads custom textures/maps/data from the mod's folder (`.xnb` or `.png` format) or game content. -* `Console.Out` messages are now written to the log file. -* `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly. -* Fixed value-changed events being raised when the player loads a save due to values being initialised. - -## 1.10 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.9...1.10). - -For players: -* Updated to Stardew Valley 1.2. -* Added logic to rewrite many mods for compatibility with game updates, though some mods may still need an update. -* Fixed `SEHException` errors affecting some players. -* Fixed issue where SMAPI didn't unlock some files on exit. -* Fixed rare issue where the installer would crash trying to delete a bundled mod from `%appdata%`. -* Improved TrainerMod commands: - * Added `world_setyear` to change the current year. - * Replaced `player_addmelee` with `player_addweapon` with support for non-melee weapons. - -For mod developers: -* Mods are now initialised after the `Initialize`/`LoadContent` phase, which means the `GameEvents.Initialize` and `GameEvents.LoadContent` events are deprecated. You can move any logic in those methods to your mod's `Entry` method. -* Added `IsBetween` and string overloads to the `ISemanticVersion` methods. -* Fixed mouse-changed event never updating prior mouse position. -* Fixed `monitor.ExitGameImmediately` not working correctly. -* Fixed `Constants.SaveFolderName` not set for a new game until the save is created. - -## 1.9 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9). - -For players: -* SMAPI now detects incompatible mods and disables them before they cause problems. -* SMAPI now allows mods nested into an otherwise empty parent folder (like `Mods\ModName-1.0\ModName\manifest.json`), since that's a common default behaviour when unpacking mods. -* The installer now detects if you need to update .NET Framework before installing SMAPI. -* The installer now detects if you need to run the game at least once (to let it perform first-time setup) before installing SMAPI. -* The installer on Linux now finds games installed to `~/.steam/steam/steamapps/common/Stardew Valley` too. -* The installer now removes old SMAPI logs to prevent confusion. -* The console now has simpler error messages. -* The console now has improved command handling & feedback. -* The console no longer shows the game's debug output (unless you use a _SMAPI for developers_ build). -* Fixed the game-needs-an-update error not pausing before exit. -* Fixed installer errors for some players when deleting files. -* Fixed installer not ignoring potential game folders that don't contain a Stardew Valley exe. -* Fixed installer not recognising Linux/Mac paths starting with `~/` or containing an escaped space. -* Fixed TrainerMod letting you add invalid items which may crash the game. -* Fixed TrainerMod's `world_downminelevel` command not working. -* Fixed rare issue where mod dependencies would override SMAPI dependencies and cause unpredictable bugs. -* Fixed errors in mods' console command handlers crashing the game. - -For mod developers: -* Added a simpler API for console commands (see `helper.ConsoleCommands`). -* Added `TimeEvents.AfterDayStarted` event triggered when a day starts. This happens no matter how the day started (including new game, loaded save, or player went to bed). -* Added `ContentEvents.AfterLocaleChanged` event triggered when the player changes the content language (for the upcoming Stardew Valley 1.2). -* Added `SaveEvents.AfterReturnToTitle` event triggered when the player returns to the title screen (for the upcoming Stardew Valley 1.2). -* Added `helper.Reflection.GetPrivateProperty` method. -* Added a `--log-path` argument to specify the SMAPI log path during testing. -* SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings automatically, so mods no longer need to add a `StringEnumConverter` themselves for those. -* The SMAPI log now has a simpler filename. -* The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. -* The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing. -* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised. -* Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly. -* Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), - and all _notice_-level deprecations have been increased to _info_. -* Removed the experimental `IConfigFile`. - -For SMAPI developers: -* Added support for debugging SMAPI on Linux/Mac if supported by the editor. - -## 1.8 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.7...1.8). - -For players: -* Mods no longer generate `.cache` subfolders. -* Fixed multiple issues where mods failed during assembly loading. -* Tweaked install package to reduce confusion. - -For mod developers: -* The `SemanticVersion` constructor now accepts a string version. -* Increased deprecation level for `Extensions` to _pending removal_. -* **Warning:** `Assembly.GetExecutingAssembly().Location` will no longer reliably - return a valid path, because mod assemblies are loaded from memory when rewritten for - compatibility. This approach has been discouraged since SMAPI 1.3; use `helper.DirectoryPath` - instead. - -For SMAPI developers: -* Rewrote assembly loading from the ground up. The new implementation... - * is much simpler; - * eliminates the `.cache` folders by loading rewritten assemblies from memory; - * ensures DLLs are loaded in leaf-to-root order (i.e. dependencies first); - * improves dependent assembly resolution; - * no longer loads DLLs if they're not referenced; - * reduces log verbosity. - -## 1.7 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.6...1.7). - -For players: -* The console now shows the folder path where mods should be added. -* The console now shows deprecation warnings after the list of loaded mods (instead of intermingled). - -For mod developers: -* Added a mod registry which provides metadata about loaded mods. -* The `Entry(…)` method is now deferred until all mods are loaded. -* Fixed `SaveEvents.BeforeSave` and `.AfterSave` not triggering on days when the player shipped something. -* Fixed `PlayerEvents.LoadedGame` and `SaveEvents.AfterLoad` being fired before the world finishes initialising. -* Fixed some `LocationEvents`, `PlayerEvents`, and `TimeEvents` being fired during game startup. -* Increased deprecation levels for `SObject`, `LogWriter` (not `Log`), and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`) to _pending removal_. Increased deprecation levels for `Mod.PerSaveConfigFolder`, `Mod.PerSaveConfigPath`, and `Version.VersionString` to _info_. - -## 1.6 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.5...1.6). - -For players: -* Added console commands to open the game/data folders. -* Updated list of incompatible mods. -* Fixed `config.json` values being duplicated in some cases. -* Fixed some Linux users not being able to launch SMAPI from Steam. -* Fixed the installer not finding custom install paths on 32-bit Windows. -* Fixed error when loading a mod which was released with a `.cache` folder for a different platform. -* Fixed error when the console doesn't support colour. -* Fixed error when a mod reads a custom JSON file from a directory that doesn't exist. - -For mod developers: -* Added three events: `SaveEvents.BeforeSave`, `SaveEvents.AfterSave`, and `SaveEvents.AfterLoad`. -* Deprecated three events: - * `TimeEvents.OnNewDay` is unreliable; use `TimeEvents.DayOfMonthChanged` or `SaveEvents` instead. - * `PlayerEvents.LoadedGame` is replaced by `SaveEvents.AfterLoad`. - * `PlayerEvents.FarmerChanged` serves no purpose. - -For SMAPI developers: - * Added support for specifying a lower bound in mod incompatibility data. - * Added support for custom incompatible-mod error text. - * Fixed issue where `TrainerMod` used older logic to detect the game path. - -## 1.5 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.4...1.5). - -For players: - * Added an option to disable update checks. - * SMAPI will now show a friendly error with update links when you try to use a known incompatible mod version. - * Fixed an error when a mod uses the new reflection API on a missing field or method. - * Fixed an issue where mods weren't notified of a menu change if it changed while SMAPI was still notifying mods of the previous change. - -For developers: - * Deprecated `Version` in favour of `SemanticVersion`. - _This new implementation is [semver 2.0](http://semver.org/)-compliant, introduces `NewerThan(version)` and `OlderThan(version)` convenience methods, adds support for parsing a version string into a `SemanticVersion`, and fixes various bugs with the former implementation. This also replaces `Manifest` with `IManifest`._ - * Increased deprecation levels for `SObject`, `Extensions`, `LogWriter` (not `Log`), `SPlayer`, and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`). - -## 1.4 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.3...1.4). - -For players: - * SMAPI will now prevent mods from crashing your game with menu errors. - * The installer will now automatically detect most custom install paths. - * The installer will now automatically clean up old SMAPI files. - * Each mod now has its own `.cache` folder, so removing the mod won't leave orphaned cache files behind. - * Improved installer wording to reduce confusion. - * Fixed the installer not removing TrainerMod from appdata if it's already in the game mods directory. - * Fixed the installer not moving mods out of appdata if the game isn't installed on the same Windows partition. - * Fixed the SMAPI console not being shown on Linux and Mac. - -For developers: - * Added a reflection API (via `helper.Reflection`) that simplifies robust access to the game's private fields and methods. - * Added a searchable `list_items` console command to replace the `out_items`, `out_melee`, and `out_rings` commands. - * Added `TypeLoadException` details when intercepted by SMAPI. - * Fixed an issue where you couldn't debug into an assembly because it was copied into the `.cache` directory. That will now only happen if necessary. - -## 1.3 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.2...1.3). - -For players: - * You can now run most mods on any platform (e.g. run Windows mods on Linux/Mac). - * Fixed the normal uninstaller not removing files added by the 'SMAPI for developers' installer. - -## 1.2 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.1.1...1.2). - -For players: - * Fixed compatibility with some older mods. - * Fixed mod errors in most event handlers crashing the game. - * Fixed mod errors in some event handlers preventing other mods from receiving the same event. - * Fixed game crashing on startup with an audio error for some players. - -For developers: - * Improved logging to show `ReflectionTypeLoadException` details when it's caught by SMAPI. - -## 1.1 -See [log](https://github.com/Pathoschild/SMAPI/compare/1.0...1.1.1). - -For players: - * Fixed console exiting immediately when some exceptions occur. - * Fixed an error in 1.0 when mod uses `config.json` but the file doesn't exist. - * Fixed critical errors being saved to a separate log file. - * Fixed compatibility with some older mods.1.1.1 - * Fixed race condition where some mods would sometimes crash because the game wasn't ready yet.1.1.1 - -For developers: - * Added new logging interface: - * easier to use; - * supports trace logs (written to the log file, but hidden in the console by default); - * messages are now listed in order; - * messages now show which mod logged them; - * more consistent and intuitive console color scheme. - * Added optional `MinimumApiVersion` to `manifest.json`. - * Added emergency interrupt feature for dangerous mods. - * Fixed deprecation warnings being repeated if the mod can't be identified.1.1.1 - -## 1.0 -See [log](https://github.com/Pathoschild/SMAPI/compare/0.40.1.1-3...1.0). - -For players: - * Added support for Linux and Mac. - * Added installer to automate adding & removing SMAPI. - * Added background update check on launch. - * Fixed missing `steam_appid.txt` file. - * Fixed some mod UIs disappearing at a non-default zoom level for some users. - * Removed undocumented support for mods in AppData folder **(breaking change)**. - * Removed `F2` debug mode. - -For mod developers: - * Added deprecation warnings. - * Added OS version to log. - * Added zoom-adjusted mouse position to mouse-changed event arguments. - * Added SMAPI code documentation. - * Switched to [semantic versioning](http://semver.org). - * Fixed mod versions not shown correctly in the log. - * Fixed misspelled field in `manifest.json` schema. - * Fixed some events getting wrong data. - * Simplified log output. - -For SMAPI developers: - * Simplified compiling from source. - * Formalised release process and added automated build packaging. - * Removed obsolete and unfinished code. - * Internal cleanup & refactoring. - -## 0.x -* 0.40.1.1 (2016-09-30, [log](https://github.com/Pathoschild/SMAPI/compare/0.40.0...0.40.1.1-3)) - * Added support for Stardew Valley 1.1. - -* 0.40.0 (2016-04-05, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.7...0.40.0)) - * Fixed an error that ocurred during minigames. - -* 0.39.7 (2016-04-04, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.6...0.39.7)) - * Added 'no check' graphics events that are triggered regardless of game's if checks. - -* 0.39.6 (2016-04-01, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.5...0.39.6)) - * Added game & SMAPI versions to log. - * Fixed conflict in graphics tick events. - * Bug fixes. - -* 0.39.5 (2016-03-30, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.4...0.39.5)) -* 0.39.4 (2016-03-29, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.3...0.39.4)) -* 0.39.3 (2016-03-28, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.2...0.39.3)) -* 0.39.2 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.39.1...0.39.2)) -* 0.39.1 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.8...0.39.1)) -* 0.38.8 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.7...0.38.8)) -* 0.38.7 (2016-03-23, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.6...0.38.7)) -* 0.38.6 (2016-03-22, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.5...0.38.6)) -* 0.38.5 (2016-03-22, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.4...0.38.5)) -* 0.38.4 (2016-03-21, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.3...0.38.4)) -* 0.38.3 (2016-03-21, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.2...0.38.3)) -* 0.38.2 (2016-03-21, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.0...0.38.2)) -* 0.38.0 (2016-03-20, [log](https://github.com/Pathoschild/SMAPI/compare/0.38.1...0.38.0)) -* 0.38.1 (2016-03-20, [log](https://github.com/Pathoschild/SMAPI/compare/0.37.3...0.38.1)) -* 0.37.3 (2016-03-08, [log](https://github.com/Pathoschild/SMAPI/compare/0.37.2...0.37.3)) -* 0.37.2 (2016-03-07, [log](https://github.com/Pathoschild/SMAPI/compare/0.37.1...0.37.2)) -* 0.37.1 (2016-03-06, [log](https://github.com/Pathoschild/SMAPI/compare/0.36...0.37.1)) -* 0.36 (2016-03-04, [log](https://github.com/Pathoschild/SMAPI/compare/0.37...0.36)) -* 0.37 (2016-03-04, [log](https://github.com/Pathoschild/SMAPI/compare/0.35...0.37)) -* 0.35 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.34...0.35)) -* 0.34 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.33...0.34)) -* 0.33 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.32...0.33)) -* 0.32 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.31...0.32)) -* 0.31 (2016-03-02, [log](https://github.com/Pathoschild/SMAPI/compare/0.3...0.31)) -* 0.3 (2016-03-01, [log](https://github.com/Pathoschild/SMAPI/compare/Alpha0.2...0.3)) -* 0.2 (2016-02-29, [log](https://github.com/Pathoschild/SMAPI/compare/Alpha0.1...Alpha0.2) -* 0.1 (2016-02-28) diff --git a/src/.editorconfig b/src/.editorconfig deleted file mode 100644 index a5cdcf97..00000000 --- a/src/.editorconfig +++ /dev/null @@ -1,68 +0,0 @@ -# topmost editorconfig -root: true - -########## -## General formatting -## documentation: http://editorconfig.org -########## -[*] -indent_style = space -indent_size = 4 -insert_final_newline = true -trim_trailing_whitespace = true -charset = utf-8 - -[*.csproj] -indent_size = 2 -insert_final_newline = false - -[*.json] -indent_size = 2 - -########## -## C# formatting -## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference -########## -[*.cs] - -#sort 'system' usings first -dotnet_sort_system_directives_first = true - -# use 'this.' qualifier -dotnet_style_qualification_for_field = true:error -dotnet_style_qualification_for_property = true:error -dotnet_style_qualification_for_method = true:error -dotnet_style_qualification_for_event = true:error - -# use language keywords (like int) instead of type (like Int32) -dotnet_style_predefined_type_for_locals_parameters_members = true:error -dotnet_style_predefined_type_for_member_access = true:error - -# don't use 'var' for language keywords -csharp_style_var_for_built_in_types = false:error - -# suggest modern C# features where simpler -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion - -# prefer method block bodies -csharp_style_expression_bodied_methods = false:suggestion -csharp_style_expression_bodied_constructors = false:suggestion - -# prefer property expression bodies -csharp_style_expression_bodied_properties = true:suggestion -csharp_style_expression_bodied_indexers = true:suggestion -csharp_style_expression_bodied_accessors = true:suggestion - -# prefer inline out variables -csharp_style_inlined_variable_declaration = true:warning - -# avoid superfluous braces -csharp_prefer_braces = false:suggestion diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs deleted file mode 100644 index 196d67c5..00000000 --- a/src/GlobalAssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: ComVisible(false)] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/ModBuildConfig/README.md b/src/ModBuildConfig/README.md new file mode 100644 index 00000000..c261e705 --- /dev/null +++ b/src/ModBuildConfig/README.md @@ -0,0 +1,121 @@ +**Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration +for [Stardew Valley](http://stardewvalley.net/) [SMAPI](https://github.com/Pathoschild/SMAPI) mods. + +The package... + +* lets you write your mod once, and compile it on any computer. It detects the current platform + (Linux, Mac, or Windows) and game install path, and injects the right references automatically. +* configures Visual Studio so you can debug into the mod code when the game is running (_Windows + only_). +* packages the mod automatically into the game's mod folder when you build the code (_optional_). + +## Contents +* [Install](#install) +* [Simplify mod development](#simplify-mod-development) +* [Troubleshoot](#troubleshoot) +* [Versions](#versions) + +## Install +**When creating a new mod:** + +1. Create an empty library project. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). +4. Compile on any platform. + +**When migrating an existing mod:** + +1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, + `StardewModdingAPI`, and `xTile`. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. Compile on any platform. + +## Simplify mod development +### Package your mod into the game folder automatically +You can copy your mod files into the `Mods` folder automatically each time you build, so you don't +need to do it manually: + +1. Edit your mod's `.csproj` file. +2. Add this block above the first `` line: + + ```xml + $(MSBuildProjectName) + ``` + +That's it! Each time you build, the files in `\Mods\` will be updated with +your `manifest.json`, build output, and any `i18n` files. + +Notes: +* To add custom files, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). +* To customise the folder name, just replace `$(MSBuildProjectName)` with the folder name you want. +* If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). + +### Debug into the mod code (Windows-only) +Stepping into your mod code when the game is running is straightforward, since this package injects +the configuration automatically. To do it: + +1. [Package your mod into the game folder automatically](#package-your-mod-into-the-game-folder-automatically). +2. Launch the project with debugging in Visual Studio or MonoDevelop. + +This will deploy your mod files into the game folder, launch SMAPI, and attach a debugger +automatically. Now you can step through your code, set breakpoints, etc. + +### Create release zips automatically (Windows-only) +You can create the mod package automatically when you build: + +1. Edit your mod's `.csproj` file. +2. Add this block above the first `` line: + + ```xml + $(SolutionDir)\_releases + ``` + +That's it! Each time you build, the mod files will be zipped into `_releases\.zip`. (You +can change the value to save the zips somewhere else.) + +## Troubleshoot +### "Failed to find the game install path" +That error means the package couldn't figure out where the game is installed. You need to specify +the game location yourself. There's two ways to do that: + +* **Option 1: set the path globally.** + _This will apply to every project that uses version 1.5+ of package._ + + 1. Get the full folder path containing the Stardew Valley executable. + 2. Create this file path: + + platform | path + --------- | ---- + Linux/Mac | `~/stardewvalley.targets` + Windows | `%USERPROFILE%\stardewvalley.targets` + + 3. Save the file with this content: + + ```xml + + + PATH_HERE + + + ``` + + 4. Replace `PATH_HERE` with your custom game install path. + +* **Option 2: set the path in the project file.** + _(You'll need to do it for every project that uses the package.)_ + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the ` + PATH_HERE + + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + +## Versions +See [release notes](release-notes.md). diff --git a/src/ModBuildConfig/assets/nuget-icon.pdn b/src/ModBuildConfig/assets/nuget-icon.pdn new file mode 100644 index 00000000..7bd5c0c5 Binary files /dev/null and b/src/ModBuildConfig/assets/nuget-icon.pdn differ diff --git a/src/ModBuildConfig/assets/nuget-icon.png b/src/ModBuildConfig/assets/nuget-icon.png new file mode 100644 index 00000000..611cdf88 Binary files /dev/null and b/src/ModBuildConfig/assets/nuget-icon.png differ diff --git a/src/ModBuildConfig/build/smapi.targets b/src/ModBuildConfig/build/smapi.targets new file mode 100644 index 00000000..a1b6aab3 --- /dev/null +++ b/src/ModBuildConfig/build/smapi.targets @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + A build task which packs mod files into a conventional release zip. + public class CreateModReleaseZip : Task, ITask + { + /********* + ** Accessors + *********/ + /// The mod files to pack. + public ITaskItem[] Files { get; set; } + + /// The name of the mod. + public string ModName { get; set; } + + /// The absolute or relative path to the folder which should contain the generated zip file. + public string OutputFolderPath { get; set; } + + + /********* + ** Public methods + *********/ + public override bool Execute() + { + try + { + // create output path if needed + Directory.CreateDirectory(OutputFolderPath); + + // get zip filename + string fileName = string.Format("{0}-{1}.zip", this.ModName, this.GetManifestVersion()); + + // clear old zip file if present + string zipPath = Path.Combine(OutputFolderPath, fileName); + if (File.Exists(zipPath)) + File.Delete(zipPath); + + // create zip file + using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) + using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + { + foreach (ITaskItem file in Files) + { + // get file info + string filePath = file.ItemSpec; + string entryName = ModName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); + if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) + entryName = Path.Combine("i18n", entryName); + + // add to zip + using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) + { + fileStream.CopyTo(fileStreamInZip); + } + } + } + + return true; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex); + return false; + } + } + + /// Get a semantic version from the mod manifest (if available). + public string GetManifestVersion() + { + // Get the file JSON string + string json = ""; + foreach(ITaskItem file in Files) + { + if(Path.GetFileName(file.ItemSpec).ToLower() != "manifest.json") + continue; + json = File.ReadAllText(file.ItemSpec); + break; + } + + // Serialize the manifest json into a data object, then get a version object from that. + IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); + IDictionary version = (IDictionary)data["Version"]; + + // Store our version numbers for ease of use + int major = (int)version["MajorVersion"]; + int minor = (int)version["MinorVersion"]; + int patch = (int)version["PatchVersion"]; + + return String.Format("{0}.{1}.{2}", major, minor, patch); + } + } + ]]> + + + + + + + + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + + + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + + + + + + + + + + false + + + false + + + false + + + false + + + $(GamePath)\Stardew Valley.exe + false + + + $(GamePath)\StardewModdingAPI.exe + false + + + $(GamePath)\xTile.dll + false + False + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + + + $(GamePath)\MonoGame.Framework.dll + false + False + + + $(GamePath)\StardewValley.exe + false + + + $(GamePath)\StardewModdingAPI.exe + false + + + $(GamePath)\xTile.dll + false + + + + + + + + + + + + + + + + + + + + + + + + $(GamePath)\Mods\$(DeployModFolderName) + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ModBuildConfig/package.nuspec b/src/ModBuildConfig/package.nuspec new file mode 100644 index 00000000..b8e96481 --- /dev/null +++ b/src/ModBuildConfig/package.nuspec @@ -0,0 +1,22 @@ + + + + Pathoschild.Stardew.ModBuildConfig + 1.7.1 + MSBuild config for Stardew Valley mods + Pathoschild + Pathoschild + false + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.7.1/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig#readme + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.7.1/assets/nuget-icon.png + Automates the build configuration for crossplatform Stardew Valley SMAPI mods. + + 1.7 added an option to create release zips on build and added a reference to XNA's XACT library for audio-related mods. + 1.7.1 fixed an issue where i18n folders were flattened, and ensures that the manifest/i18n files in the project take precedence over those in the build output if both are present. + + + + + + diff --git a/src/ModBuildConfig/release-notes.md b/src/ModBuildConfig/release-notes.md new file mode 100644 index 00000000..ff2734f8 --- /dev/null +++ b/src/ModBuildConfig/release-notes.md @@ -0,0 +1,28 @@ +## Release notes +### 1.6 +* Added support for deploying mod files into `Mods` automatically. +* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. + +### 1.5 +* Added support for setting a custom game path globally. +* Added default GOG path on Mac. + +### 1.4 +* Fixed detection of non-default game paths on 32-bit Windows. +* Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). + +### 1.3 +* Added support for non-default game paths on Windows. + +### 1.2 +* Exclude game binaries from mod build output. + +### 1.1 +* Added support for overriding the target platform. + +### 1.0 +* Initial release. +* Added support for detecting the game path automatically. +* Added support for injecting XNA/MonoGame references automatically based on the OS. +* Added support for mod builders like SilVerPLuM. diff --git a/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7cc6804a --- /dev/null +++ b/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")] +[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")] +[assembly: AssemblyProduct("StardewModdingAPI.AssemblyRewriters")] +[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")] diff --git a/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs new file mode 100644 index 00000000..a7f100f2 --- /dev/null +++ b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.AssemblyRewriters +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin() + { + base.Begin(); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} diff --git a/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj new file mode 100644 index 00000000..651b822d --- /dev/null +++ b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -0,0 +1,44 @@ + + + + + Debug + x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49} + Library + Properties + StardewModdingAPI.AssemblyRewriters + StardewModdingAPI.AssemblyRewriters + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Installer/Enums/Platform.cs b/src/SMAPI.Installer/Enums/Platform.cs new file mode 100644 index 00000000..9bcaa3c3 --- /dev/null +++ b/src/SMAPI.Installer/Enums/Platform.cs @@ -0,0 +1,12 @@ +namespace StardewModdingApi.Installer.Enums +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux/Mac version of the game. + Mono, + + /// The Windows version of the game. + Windows + } +} \ No newline at end of file diff --git a/src/SMAPI.Installer/Enums/ScriptAction.cs b/src/SMAPI.Installer/Enums/ScriptAction.cs new file mode 100644 index 00000000..e62b2a7c --- /dev/null +++ b/src/SMAPI.Installer/Enums/ScriptAction.cs @@ -0,0 +1,12 @@ +namespace StardewModdingApi.Installer.Enums +{ + /// The action to perform. + internal enum ScriptAction + { + /// Install SMAPI to the game directory. + Install, + + /// Remove SMAPI from the game directory. + Uninstall + } +} \ No newline at end of file diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs new file mode 100644 index 00000000..1a132e54 --- /dev/null +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -0,0 +1,740 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using Microsoft.Win32; +using StardewModdingApi.Installer.Enums; + +namespace StardewModdingApi.Installer +{ + /// Interactively performs the install and uninstall logic. + internal class InteractiveInstaller + { + /********* + ** Properties + *********/ + /// The value that represents Windows 7. + private readonly Version Windows7Version = new Version(6, 1); + + /// The default file paths where Stardew Valley can be installed. + /// The target platform. + /// Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. + private IEnumerable GetDefaultInstallPaths(Platform platform) + { + switch (platform) + { + case Platform.Mono: + { + string home = Environment.GetEnvironmentVariable("HOME"); + + // Linux + yield return $"{home}/GOG Games/Stardew Valley/game"; + yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley") + ? $"{home}/.steam/steam/steamapps/common/Stardew Valley" + : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley"; + + // Mac + yield return "/Applications/Stardew Valley.app/Contents/MacOS"; + yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; + } + break; + + case Platform.Windows: + { + // Windows + yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley"; + yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"; + + // Windows registry + IDictionary registryKeys = new Dictionary + { + [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam + [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows + }; + foreach (var pair in registryKeys) + { + string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); + if (!string.IsNullOrWhiteSpace(path)) + yield return path; + } + } + break; + + default: + throw new InvalidOperationException($"Unknown platform '{platform}'."); + } + } + + /// Get the absolute file or folder paths to remove when uninstalling SMAPI. + /// The folder for Stardew Valley and SMAPI. + /// The folder for SMAPI mods. + private IEnumerable GetUninstallPaths(DirectoryInfo installDir, DirectoryInfo modsDir) + { + string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); + + // common + yield return GetInstallPath("Mono.Cecil.dll"); + yield return GetInstallPath("Newtonsoft.Json.dll"); + yield return GetInstallPath("StardewModdingAPI.exe"); + yield return GetInstallPath("StardewModdingAPI.config.json"); + yield return GetInstallPath("StardewModdingAPI.data.json"); + yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); + yield return GetInstallPath("System.ValueTuple.dll"); + yield return GetInstallPath("steam_appid.txt"); + + // Linux/Mac only + yield return GetInstallPath("libgdiplus.dylib"); + yield return GetInstallPath("StardewModdingAPI"); + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); + yield return GetInstallPath("System.Numerics.dll"); + yield return GetInstallPath("System.Runtime.Caching.dll"); + + // Windows only + yield return GetInstallPath("StardewModdingAPI.pdb"); + + // obsolete + yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 + yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + if (modsDir.Exists) + { + foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) + yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7 + } + yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files + } + + /// Whether the current console supports color formatting. + private static readonly bool ConsoleSupportsColor = InteractiveInstaller.GetConsoleSupportsColor(); + + + /********* + ** Public methods + *********/ + /// Run the install or uninstall script. + /// The command line arguments. + /// + /// Initialisation flow: + /// 1. Collect information (mainly OS and install path) and validate it. + /// 2. Ask the user whether to install or uninstall. + /// + /// Uninstall logic: + /// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup. + /// 2. Delete all files and folders in the game directory matching one of the values returned by . + /// + /// Install flow: + /// 1. Run the uninstall flow. + /// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory. + /// 3. On Linux/Mac: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) + /// 4. Create the 'Mods' directory. + /// 5. Copy the bundled mods into the 'Mods' directory (deleting any existing versions). + /// 6. Move any mods from app data into game's mods directory. + /// + public void Run(string[] args) + { + /**** + ** read command-line arguments + ****/ + // get action from CLI + bool installArg = args.Contains("--install"); + bool uninstallArg = args.Contains("--uninstall"); + if (installArg && uninstallArg) + { + this.PrintError("You can't specify both --install and --uninstall command-line flags."); + Console.ReadLine(); + return; + } + + // get game path from CLI + string gamePathArg = null; + { + int pathIndex = Array.LastIndexOf(args, "--game-path") + 1; + if (pathIndex >= 1 && args.Length >= pathIndex) + gamePathArg = args[pathIndex]; + } + + /**** + ** collect details + ****/ + // get platform + Platform platform = this.DetectPlatform(); + this.PrintDebug($"Platform: {(platform == Platform.Windows ? "Windows" : "Linux or Mac")}."); + + // get game path + DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg); + if (installDir == null) + { + this.PrintError("Failed finding your game path."); + Console.ReadLine(); + return; + } + + // get folders + DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.ToString())); + DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); + var paths = new + { + executable = Path.Combine(installDir.FullName, platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe"), + unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"), + unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), + unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") + }; + this.PrintDebug($"Install path: {installDir}."); + + /**** + ** validate assumptions + ****/ + if (!packageDir.Exists) + { + this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip") + ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder." + : $"The 'internal/{platform}' package folder is missing (should be at {packageDir})." + ); + Console.ReadLine(); + return; + } + if (!File.Exists(paths.executable)) + { + this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); + Console.ReadLine(); + return; + } + + /**** + ** validate Windows dependencies + ****/ + if (platform == Platform.Windows) + { + // .NET Framework 4.5+ + if (!this.HasNetFramework45(platform)) + { + this.PrintError(Environment.OSVersion.Version >= this.Windows7Version + ? "Please install the latest version of .NET Framework before installing SMAPI." // Windows 7+ + : "Please install .NET Framework 4.5 before installing SMAPI." // Windows Vista or earlier + ); + this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details."); + Console.ReadLine(); + return; + } + if (!this.HasXNA(platform)) + { + this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup."); + Console.ReadLine(); + return; + } + } + + Console.WriteLine(); + + /**** + ** ask user what to do + ****/ + ScriptAction action; + + if (installArg) + action = ScriptAction.Install; + else if (uninstallArg) + action = ScriptAction.Uninstall; + else + { + Console.WriteLine("You can...."); + Console.WriteLine("[1] Install SMAPI."); + Console.WriteLine("[2] Uninstall SMAPI."); + Console.WriteLine(); + + string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2"); + switch (choice) + { + case "1": + action = ScriptAction.Install; + break; + case "2": + action = ScriptAction.Uninstall; + break; + default: + throw new InvalidOperationException($"Unexpected action key '{choice}'."); + } + Console.WriteLine(); + } + + /**** + ** Always uninstall old files + ****/ + // restore game launcher + if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup)) + { + this.PrintDebug("Removing SMAPI launcher..."); + this.InteractivelyDelete(paths.unixLauncher); + File.Move(paths.unixLauncherBackup, paths.unixLauncher); + } + + // remove old files + string[] removePaths = this.GetUninstallPaths(installDir, modsDir) + .Where(path => Directory.Exists(path) || File.Exists(path)) + .ToArray(); + if (removePaths.Any()) + { + this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); + foreach (string path in removePaths) + this.InteractivelyDelete(path); + } + + /**** + ** Install new files + ****/ + if (action == ScriptAction.Install) + { + // copy SMAPI files to game dir + this.PrintDebug("Adding SMAPI files..."); + foreach (FileInfo sourceFile in packageDir.EnumerateFiles()) + { + string targetPath = Path.Combine(installDir.FullName, sourceFile.Name); + this.InteractivelyDelete(targetPath); + sourceFile.CopyTo(targetPath); + } + + // replace mod launcher (if possible) + if (platform == Platform.Mono) + { + this.PrintDebug("Safely replacing game launcher..."); + if (!File.Exists(paths.unixLauncherBackup)) + File.Move(paths.unixLauncher, paths.unixLauncherBackup); + else if (File.Exists(paths.unixLauncher)) + this.InteractivelyDelete(paths.unixLauncher); + + File.Move(paths.unixSmapiLauncher, paths.unixLauncher); + } + + // create mods directory (if needed) + if (!modsDir.Exists) + { + this.PrintDebug("Creating mods directory..."); + modsDir.Create(); + } + + // add or replace bundled mods + Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods")); + DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods")); + if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) + { + this.PrintDebug("Adding bundled mods..."); + foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) + { + this.PrintDebug($" adding {sourceDir.Name}..."); + + // initialise target dir + DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name)); + this.InteractivelyDelete(targetDir.FullName); + targetDir.Create(); + + // copy files + foreach (FileInfo sourceFile in sourceDir.EnumerateFiles()) + sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); + } + } + + // remove obsolete appdata mods + this.InteractivelyRemoveAppDataMods(platform, modsDir, packagedModsDir); + } + Console.WriteLine(); + + /**** + ** exit + ****/ + this.PrintColor("Done!", ConsoleColor.DarkGreen); + if (platform == Platform.Windows) + { + this.PrintColor( + action == ScriptAction.Install + ? "Don't forget to launch StardewModdingAPI.exe instead of the normal game executable. See the readme.txt for details." + : "If you manually changed shortcuts or Steam to launch SMAPI, don't forget to change those back.", + ConsoleColor.DarkGreen + ); + } + else if (action == ScriptAction.Install) + this.PrintColor("You can launch the game the same way as before to play with mods.", ConsoleColor.DarkGreen); + Console.ReadKey(); + } + + + /********* + ** Private methods + *********/ + /// Detect the game's platform. + /// The platform is not supported. + private Platform DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + case PlatformID.Unix: + return Platform.Mono; + + default: + return Platform.Windows; + } + } + + /// Test whether the current console supports color formatting. + private static bool GetConsoleSupportsColor() + { + try + { + Console.ForegroundColor = Console.ForegroundColor; + return true; + } + catch (Exception) + { + return false; // Mono bug + } + } + + /// Get the value of a key in the Windows registry. + /// The full path of the registry key relative to HKLM. + /// The name of the value. + private string GetLocalMachineRegistryValue(string key, string name) + { + RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; + RegistryKey openKey = localMachine.OpenSubKey(key); + if (openKey == null) + return null; + using (openKey) + return (string)openKey.GetValue(name); + } + + /// Print a debug message. + /// The text to print. + private void PrintDebug(string text) + { + this.PrintColor(text, ConsoleColor.DarkGray); + } + + /// Print a warning message. + /// The text to print. + private void PrintWarning(string text) + { + this.PrintColor(text, ConsoleColor.DarkYellow); + } + + /// Print a warning message. + /// The text to print. + private void PrintError(string text) + { + this.PrintColor(text, ConsoleColor.Red); + } + + /// Print a message to the console. + /// The message text. + /// The text foreground color. + private void PrintColor(string text, ConsoleColor color) + { + if (InteractiveInstaller.ConsoleSupportsColor) + { + Console.ForegroundColor = color; + Console.WriteLine(text); + Console.ResetColor(); + } + else + Console.WriteLine(text); + } + + /// Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows. + /// The current platform. + /// The current platform is not Windows. + private bool HasNetFramework45(Platform platform) + { + switch (platform) + { + case Platform.Windows: + using (RegistryKey versionKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full")) + return versionKey?.GetValue("Release") != null; // .NET Framework 4.5+ + + default: + throw new NotSupportedException("The installed .NET Framework version can only be checked on Windows."); + } + } + + /// Get whether the current system has XNA Framework installed. This only applies on Windows. + /// The current platform. + /// The current platform is not Windows. + private bool HasXNA(Platform platform) + { + switch (platform) + { + case Platform.Windows: + using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\XNA\Framework")) + return key != null; // XNA Framework 4.0+ + + default: + throw new NotSupportedException("The installed XNA Framework version can only be checked on Windows."); + } + } + + /// Interactively delete a file or folder path, and block until deletion completes. + /// The file or folder path. + private void InteractivelyDelete(string path) + { + while (true) + { + try + { + this.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); + break; + } + catch (Exception ex) + { + this.PrintError($"Oops! The installer couldn't delete {path}: [{ex.GetType().Name}] {ex.Message}."); + this.PrintError("Try rebooting your computer and then run the installer again. If that doesn't work, try deleting it yourself then press any key to retry."); + Console.ReadKey(); + } + } + } + + /// Delete a file or folder regardless of file permissions, and block until deletion completes. + /// The file or folder to reset. + private void ForceDelete(FileSystemInfo entry) + { + // ignore if already deleted + entry.Refresh(); + if (!entry.Exists) + return; + + // delete children + var folder = entry as DirectoryInfo; + if (folder != null) + { + foreach (FileSystemInfo child in folder.GetFileSystemInfos()) + this.ForceDelete(child); + } + + // reset permissions & delete + entry.Attributes = FileAttributes.Normal; + entry.Delete(); + + // wait for deletion to finish + for (int i = 0; i < 10; i++) + { + entry.Refresh(); + if (entry.Exists) + Thread.Sleep(500); + } + + // throw exception if deletion didn't happen before timeout + entry.Refresh(); + if (entry.Exists) + throw new IOException($"Timed out trying to delete {entry.FullName}"); + } + + /// Interactively ask the user to choose a value. + /// The message to print. + /// The allowed options (not case sensitive). + private string InteractivelyChoose(string message, params string[] options) + { + while (true) + { + Console.WriteLine(message); + string input = Console.ReadLine()?.Trim().ToLowerInvariant(); + if (!options.Contains(input)) + { + Console.WriteLine("That's not a valid option."); + continue; + } + return input; + } + } + + /// Interactively locate the game install path to update. + /// The current platform. + /// The path specified as a command-line argument (if any), which should override automatic path detection. + private DirectoryInfo InteractivelyGetInstallPath(Platform platform, string specifiedPath) + { + // get executable name + string executableFilename = platform == Platform.Windows + ? "Stardew Valley.exe" + : "StardewValley.exe"; + + // validate specified path + if (specifiedPath != null) + { + var dir = new DirectoryInfo(specifiedPath); + if (!dir.Exists) + { + this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't exist."); + return null; + } + if (!dir.EnumerateFiles(executableFilename).Any()) + { + this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't contain the Stardew Valley executable."); + return null; + } + return dir; + } + + // get installed paths + DirectoryInfo[] defaultPaths = + ( + from path in this.GetDefaultInstallPaths(platform).Distinct(StringComparer.InvariantCultureIgnoreCase) + let dir = new DirectoryInfo(path) + where dir.Exists && dir.EnumerateFiles(executableFilename).Any() + select dir + ) + .ToArray(); + + // choose where to install + if (defaultPaths.Any()) + { + // only one path + if (defaultPaths.Length == 1) + return defaultPaths.First(); + + // let user choose path + Console.WriteLine(); + Console.WriteLine("Found multiple copies of the game:"); + for (int i = 0; i < defaultPaths.Length; i++) + Console.WriteLine($"[{i + 1}] {defaultPaths[i].FullName}"); + Console.WriteLine(); + + string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); + string choice = this.InteractivelyChoose("Where do you want to add/remove SMAPI? Type the number next to your choice, then press enter.", validOptions); + int index = int.Parse(choice, CultureInfo.InvariantCulture) - 1; + return defaultPaths[index]; + } + + // ask user + Console.WriteLine("Oops, couldn't find the game automatically."); + while (true) + { + // get path from user + Console.WriteLine($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter."); + string path = Console.ReadLine()?.Trim(); + if (string.IsNullOrWhiteSpace(path)) + { + Console.WriteLine(" You must specify a directory path to continue."); + continue; + } + + // normalise path + if (platform == Platform.Windows) + path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path + if (platform == Platform.Mono) + path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line + if (path.StartsWith("~/")) + { + string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE"); + path = Path.Combine(home, path.Substring(2)); + } + + // get directory + if (File.Exists(path)) + path = Path.GetDirectoryName(path); + DirectoryInfo directory = new DirectoryInfo(path); + + // validate path + if (!directory.Exists) + { + Console.WriteLine(" That directory doesn't seem to exist."); + continue; + } + if (!directory.EnumerateFiles(executableFilename).Any()) + { + Console.WriteLine(" That directory doesn't contain a Stardew Valley executable."); + continue; + } + + // looks OK + Console.WriteLine(" OK!"); + return directory; + } + } + + /// Interactively move mods out of the appdata directory. + /// The current platform. + /// The directory which should contain all mods. + /// The installer directory containing packaged mods. + private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) + { + // get packaged mods to delete + string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); + + // get path + string homePath = platform == Platform.Windows + ? Environment.GetEnvironmentVariable("APPDATA") + : Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".config"); + string appDataPath = Path.Combine(homePath, "StardewValley"); + DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods")); + + // check if migration needed + if (!modDir.Exists) + return; + this.PrintDebug($"Found an obsolete mod path: {modDir.FullName}"); + this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); + + // move mods if no conflicts (else warn) + foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos()) + { + // get type + bool isDir = entry is DirectoryInfo; + if (!isDir && !(entry is FileInfo)) + continue; // should never happen + + // delete packaged mods (newer version bundled into SMAPI) + if (isDir && packagedModNames.Contains(entry.Name, StringComparer.InvariantCultureIgnoreCase)) + { + this.PrintDebug($" Deleting {entry.Name} because it's bundled into SMAPI..."); + this.InteractivelyDelete(entry.FullName); + continue; + } + + // check paths + string newPath = Path.Combine(properModsDir.FullName, entry.Name); + if (isDir ? Directory.Exists(newPath) : File.Exists(newPath)) + { + this.PrintWarning($" Can't move {entry.Name} because it already exists in your game's mod directory."); + continue; + } + + // move into mods + this.PrintDebug($" Moving {entry.Name} into the game's mod directory..."); + this.Move(entry, newPath); + } + + // delete if empty + if (modDir.EnumerateFileSystemInfos().Any()) + this.PrintWarning(" You have files in this folder which couldn't be moved automatically. These will be ignored by SMAPI."); + else + { + this.PrintDebug(" Deleted empty directory."); + modDir.Delete(); + } + } + + /// Move a filesystem entry to a new parent directory. + /// The filesystem entry to move. + /// The destination path. + /// We can't use or , because those don't work across partitions. + private void Move(FileSystemInfo entry, string newPath) + { + // file + if (entry is FileInfo file) + { + file.CopyTo(newPath); + file.Delete(); + } + + // directory + else + { + Directory.CreateDirectory(newPath); + + DirectoryInfo directory = (DirectoryInfo)entry; + foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos()) + this.Move(child, Path.Combine(newPath, child.Name)); + + directory.Delete(); + } + } + } +} diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs new file mode 100644 index 00000000..8f328ecf --- /dev/null +++ b/src/SMAPI.Installer/Program.cs @@ -0,0 +1,17 @@ +namespace StardewModdingApi.Installer +{ + /// The entry point for SMAPI's install and uninstall console app. + internal class Program + { + /********* + ** Public methods + *********/ + /// Run the install or uninstall script. + /// The command line arguments. + public static void Main(string[] args) + { + var installer = new InteractiveInstaller(); + installer.Run(args); + } + } +} diff --git a/src/SMAPI.Installer/Properties/AssemblyInfo.cs b/src/SMAPI.Installer/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..3a6a4648 --- /dev/null +++ b/src/SMAPI.Installer/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.Installer")] +[assembly: AssemblyProduct("StardewModdingAPI.Installer")] +[assembly: Guid("443ddf81-6aaf-420a-a610-3459f37e5575")] diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj new file mode 100644 index 00000000..f8e368a4 --- /dev/null +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -0,0 +1,56 @@ + + + + + Debug + x86 + {443DDF81-6AAF-420A-A610-3459F37E5575} + Exe + Properties + StardewModdingAPI.Installer + StardewModdingAPI.Installer + v4.0 + 512 + true + + + x86 + true + full + false + $(SolutionDir)\..\bin\Debug\Installer + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + $(SolutionDir)\..\bin\Release\Installer + TRACE + prompt + 4 + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + Always + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt new file mode 100644 index 00000000..eb27ac52 --- /dev/null +++ b/src/SMAPI.Installer/readme.txt @@ -0,0 +1,44 @@ + ___ ___ ___ ___ + / /\ /__/\ / /\ / /\ ___ + / /:/_ | |::\ / /::\ / /::\ / /\ + / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ + / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ /__/::\ + /__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ \__\/\:\__ + \ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \ \:\/\ + \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ + \__\/ /:/ \ \:\ \ \:\ \ \:\ /__/:/ + /__/:/ \ \:\ \ \:\ \ \:\ \__\/ + \__\/ \__\/ \__\/ \__\/ + + +SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. + + +Install guide +-------------------------------- +See http://stardewvalleywiki.com/Modding:Installing_SMAPI. + + +Need help? +-------------------------------- +- FAQs: http://stardewvalleywiki.com/Modding:Player_FAQs +- Ask for help: https://discord.gg/kH55QXP + + +Manual install +-------------------------------- +THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. +If you really want to install SMAPI manually, here's how. + +1. Download the latest version of SMAPI: https://github.com/Pathoschild/SMAPI/releases. +2. Unzip the .zip file somewhere (not in your game folder). +3. Copy the files from the "internal/Windows" folder (on Windows) or "internal/Mono" folder (on + Linux/Mac) into your game folder. The `StardewModdingAPI.exe` file should be right next to the + game's executable. +4. + - Windows only: if you use Steam, see the install guide above to enable achievements and + overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. + + - Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and + "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to + play with mods. diff --git a/src/SMAPI.Models/ModInfoModel.cs b/src/SMAPI.Models/ModInfoModel.cs new file mode 100644 index 00000000..44071230 --- /dev/null +++ b/src/SMAPI.Models/ModInfoModel.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Models +{ + /// Generic metadata about a mod. + internal class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; } + + /// The mod's semantic version number. + public string Version { get; } + + /// The mod's web URL. + public string Url { get; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; } + + + /********* + ** Public methods + *********/ + /// Construct a valid instance. + /// The mod name. + /// The mod's semantic version number. + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + [JsonConstructor] + public ModInfoModel(string name, string version, string url, string error = null) + { + this.Name = name; + this.Version = version; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an valid instance. + /// The error message indicating why the mod is invalid. + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI.Models/ModSeachModel.cs b/src/SMAPI.Models/ModSeachModel.cs new file mode 100644 index 00000000..526fbaf3 --- /dev/null +++ b/src/SMAPI.Models/ModSeachModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Models +{ + /// Specifies mods whose update-check info to fetch. + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + /// This constructed is needed for JSON deserialisation. + public ModSearchModel() { } + + /// Construct an valid instance. + /// The namespaced mod keys to search. + public ModSearchModel(IEnumerable modKeys) + { + this.ModKeys = modKeys.ToArray(); + } + } +} diff --git a/src/SMAPI.Models/StardewModdingAPI.Models.projitems b/src/SMAPI.Models/StardewModdingAPI.Models.projitems new file mode 100644 index 00000000..e2cb29e1 --- /dev/null +++ b/src/SMAPI.Models/StardewModdingAPI.Models.projitems @@ -0,0 +1,15 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc + + + StardewModdingAPI.Models + + + + + + \ No newline at end of file diff --git a/src/SMAPI.Models/StardewModdingAPI.Models.shproj b/src/SMAPI.Models/StardewModdingAPI.Models.shproj new file mode 100644 index 00000000..c80517af --- /dev/null +++ b/src/SMAPI.Models/StardewModdingAPI.Models.shproj @@ -0,0 +1,13 @@ + + + + 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc + 14.0 + + + + + + + + diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs new file mode 100644 index 00000000..051ffe99 --- /dev/null +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -0,0 +1,556 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Tests.Core +{ + /// Unit tests for . + [TestFixture] + public class ModResolverTests + { + /********* + ** Unit tests + *********/ + /**** + ** ReadManifests + ****/ + [Test(Description = "Assert that the resolver correctly returns an empty list if there are no mods installed.")] + public void ReadBasicManifest_NoMods_ReturnsEmptyList() + { + // arrange + string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(rootFolder); + + // act + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); + + // assert + Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); + } + + [Test(Description = "Assert that the resolver correctly returns a failed metadata if there's an empty mod folder.")] + public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest() + { + // arrange + string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(modFolder); + + // act + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); + IModMetadata mod = mods.FirstOrDefault(); + + // assert + Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); + Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed."); + Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); + } + + [Test(Description = "Assert that the resolver correctly reads manifest data from a randomised file.")] + public void ReadBasicManifest_CanReadFile() + { + // create manifest data + IDictionary originalDependency = new Dictionary + { + [nameof(IManifestDependency.UniqueID)] = Sample.String() + }; + IDictionary original = new Dictionary + { + [nameof(IManifest.Name)] = Sample.String(), + [nameof(IManifest.Author)] = Sample.String(), + [nameof(IManifest.Version)] = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + [nameof(IManifest.Description)] = Sample.String(), + [nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}", + [nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll", + [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}", + [nameof(IManifest.Dependencies)] = new[] { originalDependency }, + ["ExtraString"] = Sample.String(), + ["ExtraInt"] = Sample.Int() + }; + + // write to filesystem + string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); + string filename = Path.Combine(modFolder, "manifest.json"); + Directory.CreateDirectory(modFolder); + File.WriteAllText(filename, JsonConvert.SerializeObject(original)); + + // act + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); + IModMetadata mod = mods.FirstOrDefault(); + + // assert + Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest."); + Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); + Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); + Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); + Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); + Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); + + Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name."); + Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match."); + Assert.AreEqual(original[nameof(IManifest.Author)], mod.Manifest.Author, "The manifest's author doesn't match."); + Assert.AreEqual(original[nameof(IManifest.Description)], mod.Manifest.Description, "The manifest's description doesn't match."); + Assert.AreEqual(original[nameof(IManifest.EntryDll)], mod.Manifest.EntryDll, "The manifest's entry DLL doesn't match."); + Assert.AreEqual(original[nameof(IManifest.MinimumApiVersion)], mod.Manifest.MinimumApiVersion?.ToString(), "The manifest's minimum API version doesn't match."); + Assert.AreEqual(original[nameof(IManifest.Version)]?.ToString(), mod.Manifest.Version?.ToString(), "The manifest's version doesn't match."); + + Assert.IsNotNull(mod.Manifest.ExtraFields, "The extra fields should not be null."); + Assert.AreEqual(2, mod.Manifest.ExtraFields.Count, "The extra fields should contain two values."); + Assert.AreEqual(original["ExtraString"], mod.Manifest.ExtraFields["ExtraString"], "The manifest's extra fields should contain an 'ExtraString' value."); + Assert.AreEqual(original["ExtraInt"], mod.Manifest.ExtraFields["ExtraInt"], "The manifest's extra fields should contain an 'ExtraInt' value."); + + Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null."); + Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value."); + Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match."); + } + + /**** + ** ValidateManifests + ****/ + [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] + public void ValidateManifests_NoMods_DoesNothing() + { + new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + } + + [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] + public void ValidateManifests_Skips_Failed() + { + // arrange + Mock mock = this.GetMetadata("Mod A"); + mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); + + // act + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + + // assert + mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); + } + + [Test(Description = "Assert that validation fails if the mod has 'assume broken' status.")] + public void ValidateManifests_ModStatus_AssumeBroken_Fails() + { + // arrange + Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + this.SetupMetadataForValidation(mock, new ModDataRecord + { + Compatibility = new[] { new ModCompatibility("~1.0", ModStatus.AssumeBroken, null) }, + AlternativeUrl = "http://example.org" + }); + + // act + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + + // assert + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); + } + + [Test(Description = "Assert that validation fails when the minimum API version is higher than the current SMAPI version.")] + public void ValidateManifests_MinimumApiVersion_Fails() + { + // arrange + Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1"))); + this.SetupMetadataForValidation(mock); + + // act + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + + // assert + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); + } + + [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] + public void ValidateManifests_MissingEntryDLL_Fails() + { + // arrange + Mock mock = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.EntryDll = "Missing.dll"), allowStatusChange: true); + this.SetupMetadataForValidation(mock); + + // act + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + + // assert + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); + } + + [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] + public void ValidateManifests_DuplicateUniqueID_Fails() + { + // arrange + Mock modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock modB = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.Name = "Mod B"), allowStatusChange: true); + Mock modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false); + foreach (Mock mod in new[] { modA, modB, modC }) + this.SetupMetadataForValidation(mod); + + // act + new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + + // assert + modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); + } + + [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] + public void ValidateManifests_Valid_Passes() + { + // set up manifest + IManifest manifest = this.GetManifest(); + + // create DLL + string modFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(modFolder); + File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), ""); + + // arrange + Mock mock = new Mock(MockBehavior.Strict); + mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); + mock.Setup(p => p.DataRecord).Returns(() => null); + mock.Setup(p => p.Manifest).Returns(manifest); + mock.Setup(p => p.DirectoryPath).Returns(modFolder); + + // act + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); + + // assert + // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. + } + + /**** + ** ProcessDependencies + ****/ + [Test(Description = "Assert that processing dependencies doesn't fail if there are no mods installed.")] + public void ProcessDependencies_NoMods_DoesNothing() + { + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0]).ToArray(); + + // assert + Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods."); + } + + [Test(Description = "Assert that processing dependencies doesn't change the order if there are no mod dependencies.")] + public void ProcessDependencies_NoDependencies_DoesNothing() + { + // arrange + // A B C + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B"); + Mock modC = this.GetMetadata("Mod C"); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object, modC.Object }).ToArray(); + + // assert + Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order unexpectedly changed with no dependencies."); + Assert.AreSame(modB.Object, mods[1], "The load order unexpectedly changed with no dependencies."); + Assert.AreSame(modC.Object, mods[2], "The load order unexpectedly changed with no dependencies."); + } + + [Test(Description = "Assert that processing dependencies skips mods that have already failed without calling any other properties.")] + public void ProcessDependencies_Skips_Failed() + { + // arrange + Mock mock = new Mock(MockBehavior.Strict); + mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); + + // act + new ModResolver().ProcessDependencies(new[] { mock.Object }); + + // assert + mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); + } + + [Test(Description = "Assert that simple dependencies are reordered correctly.")] + public void ProcessDependencies_Reorders_SimpleDependencies() + { + // arrange + // A ◀── B + // ▲ ▲ + // │ │ + // └─ C ─┘ + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod A", "Mod B" }); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }).ToArray(); + + // assert + Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since the other mods depend on it."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); + Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs both mod A and mod B."); + } + + [Test(Description = "Assert that simple dependency chains are reordered correctly.")] + public void ProcessDependencies_Reorders_DependencyChain() + { + // arrange + // A ◀── B ◀── C ◀── D + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); + Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); + + // assert + Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); + Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs mod B, and is needed by mod D."); + Assert.AreSame(modD.Object, mods[3], "The load order is incorrect: mod D should be fourth since it needs mod C."); + } + + [Test(Description = "Assert that overlapping dependency chains are reordered correctly.")] + public void ProcessDependencies_Reorders_OverlappingDependencyChain() + { + // arrange + // A ◀── B ◀── C ◀── D + // ▲ ▲ + // │ │ + // E ◀── F + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); + Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); + Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod B" }); + Mock modF = this.GetMetadata("Mod F", dependencies: new[] { "Mod C", "Mod E" }); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }).ToArray(); + + // assert + Assert.AreEqual(6, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); + Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs mod B, and is needed by mod D."); + Assert.AreSame(modD.Object, mods[3], "The load order is incorrect: mod D should be fourth since it needs mod C."); + Assert.AreSame(modE.Object, mods[4], "The load order is incorrect: mod E should be fifth since it needs mod B, but is specified after C which also needs mod B."); + Assert.AreSame(modF.Object, mods[5], "The load order is incorrect: mod F should be last since it needs mods E and C."); + } + + [Test(Description = "Assert that mods with circular dependency chains are skipped, but any other mods are loaded in the correct order.")] + public void ProcessDependencies_Skips_CircularDependentMods() + { + // arrange + // A ◀── B ◀── C ──▶ D + // ▲ │ + // │ ▼ + // └──── E + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B", "Mod D" }, allowStatusChange: true); + Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod E" }, allowStatusChange: true); + Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }).ToArray(); + + // assert + Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); + modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); + modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); + } + + [Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")] + public void ProcessDependencies_WithSomeFailedMods_Succeeds() + { + // arrange + // A ◀── B ◀── C D (failed) + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); + Mock modD = new Mock(MockBehavior.Strict); + modD.Setup(p => p.Manifest).Returns(null); + modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); + + // assert + Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modD.Object, mods[0], "The load order is incorrect: mod D should be first since it was already failed."); + Assert.AreSame(modA.Object, mods[1], "The load order is incorrect: mod A should be second since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[2], "The load order is incorrect: mod B should be third since it needs mod A, and is needed by mod C."); + Assert.AreSame(modC.Object, mods[3], "The load order is incorrect: mod C should be fourth since it needs mod B, and is needed by mod D."); + } + + [Test(Description = "Assert that dependencies are failed if they don't meet the minimum version.")] + public void ProcessDependencies_WithMinVersions_FailsIfNotMet() + { + // arrange + // A 1.0 ◀── B (need A 1.1) + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); + } + + [Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")] + public void ProcessDependencies_WithMinVersions_SucceedsIfMet() + { + // arrange + // A 1.0 ◀── B (need A 1.0-beta) + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + } + + [Test(Description = "Assert that optional dependencies are sorted correctly if present.")] + public void ProcessDependencies_IfOptional() + { + // arrange + // A ◀── B + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }).ToArray(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + } + + [Test(Description = "Assert that optional dependencies are accepted if they're missing.")] + public void ProcessDependencies_IfOptional_SucceedsIfMissing() + { + // arrange + // A ◀── B where A doesn't exist + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }).ToArray(); + + // assert + Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod."); + } + + + /********* + ** Private methods + *********/ + /// Get a randomised basic manifest. + /// Adjust the generated manifest. + private Manifest GetManifest(Action adjust = null) + { + Manifest manifest = new Manifest + { + Name = Sample.String(), + Author = Sample.String(), + Version = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + Description = Sample.String(), + UniqueID = $"{Sample.String()}.{Sample.String()}", + EntryDll = $"{Sample.String()}.dll" + }; + adjust?.Invoke(manifest); + return manifest; + } + + /// Get a randomised basic manifest. + /// The mod's name and unique ID. + /// The mod version. + /// Adjust the generated manifest. + /// The dependencies this mod requires. + private IManifest GetManifest(string uniqueID, string version, Action adjust, params IManifestDependency[] dependencies) + { + return this.GetManifest(manifest => + { + manifest.Name = uniqueID; + manifest.UniqueID = uniqueID; + manifest.Version = new SemanticVersion(version); + manifest.Dependencies = dependencies; + adjust?.Invoke(manifest); + }); + } + + /// Get a randomised basic manifest. + /// The mod's name and unique ID. + /// The mod version. + /// The dependencies this mod requires. + private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies) + { + return this.GetManifest(uniqueID, version, null, dependencies); + } + + /// Get a randomised basic manifest. + /// The mod's name and unique ID. + private Mock GetMetadata(string uniqueID) + { + return this.GetMetadata(this.GetManifest(uniqueID, "1.0")); + } + + /// Get a randomised basic manifest. + /// The mod's name and unique ID. + /// The dependencies this mod requires. + /// Whether the code being tested is allowed to change the mod status. + private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) + { + IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); + return this.GetMetadata(manifest, allowStatusChange); + } + + /// Get a randomised basic manifest. + /// The mod manifest. + /// Whether the code being tested is allowed to change the mod status. + private Mock GetMetadata(IManifest manifest, bool allowStatusChange = false) + { + Mock mod = new Mock(MockBehavior.Strict); + mod.Setup(p => p.DataRecord).Returns(() => null); + mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); + mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); + mod.Setup(p => p.Manifest).Returns(manifest); + if (allowStatusChange) + { + mod + .Setup(p => p.SetStatus(It.IsAny(), It.IsAny())) + .Callback((status, message) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}")) + .Returns(mod.Object); + } + return mod; + } + + /// Set up a mock mod metadata for . + /// The mock mod metadata. + /// The extra metadata about the mod from SMAPI's internal data (if any). + private void SetupMetadataForValidation(Mock mod, ModDataRecord modRecord = null) + { + mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); + mod.Setup(p => p.DataRecord).Returns(() => null); + mod.Setup(p => p.Manifest).Returns(this.GetManifest()); + mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); + mod.Setup(p => p.DataRecord).Returns(modRecord); + } + } +} diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs new file mode 100644 index 00000000..63404a41 --- /dev/null +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using StardewModdingAPI.Framework.ModHelpers; +using StardewValley; + +namespace StardewModdingAPI.Tests.Core +{ + /// Unit tests for and . + [TestFixture] + public class TranslationTests + { + /********* + ** Data + *********/ + /// Sample translation text for unit tests. + public static string[] Samples = { null, "", " ", "boop", " boop " }; + + + /********* + ** Unit tests + *********/ + /**** + ** Translation helper + ****/ + [Test(Description = "Assert that the translation helper correctly handles no translations.")] + public void Helper_HandlesNoTranslations() + { + // arrange + var data = new Dictionary>(); + + // act + ITranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); + Translation translation = helper.Get("key"); + Translation[] translationList = helper.GetTranslations()?.ToArray(); + + // assert + Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); + Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null."); + Assert.AreEqual(0, translationList.Length, "The full list of translations is unexpectedly not empty."); + + Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); + } + + [Test(Description = "Assert that the translation helper returns the expected translations correctly.")] + public void Helper_GetTranslations_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary(); + TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = helper.GetTranslations()?.ToArray(); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]).Using(this.CompareEquality), $"The translations for {locale} don't match the expected values."); + } + } + + [Test(Description = "Assert that the translations returned by the helper has the expected text.")] + public void Helper_Get_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary(); + TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + + List translations = new List(); + foreach (Translation translation in expected[locale]) + translations.Add(helper.Get(translation.Key)); + actual[locale] = translations.ToArray(); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]).Using(this.CompareEquality), $"The translations for {locale} don't match the expected values."); + } + } + + /**** + ** Translation + ****/ + [Test(Description = "Assert that HasValue returns the expected result for various inputs.")] + [TestCase(null, ExpectedResult = false)] + [TestCase("", ExpectedResult = false)] + [TestCase(" ", ExpectedResult = true)] + [TestCase("boop", ExpectedResult = true)] + [TestCase(" boop ", ExpectedResult = true)] + public bool Translation_HasValue(string text) + { + return new Translation("ModName", "pt-BR", "key", text).HasValue(); + } + + [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] + public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")] + public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")] + public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).UsePlaceholder(value); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else if (!value) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder disabled."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled."); + } + + [Test(Description = "Assert that the translation's Assert method throws the expected exception.")] + public void Translation_Assert([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.That(() => translation.Assert(), Throws.Nothing, "The assert unexpected threw an exception for a valid input."); + else + Assert.That(() => translation.Assert(), Throws.Exception.TypeOf(), "The assert didn't throw an exception for invalid input."); + } + + [Test(Description = "Assert that the translation returns the expected text after setting the default.")] + public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Default(@default); + + // assert + if (!string.IsNullOrEmpty(text)) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + else if (!string.IsNullOrEmpty(@default)) + Assert.AreEqual(@default, translation.ToString(), "The translation returned an unexpected value given a null or empty base text, but valid default."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty base and default text."); + } + + /**** + ** Translation tokens + ****/ + [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] + public void Translation_Tokens([Values("anonymous object", "class", "IDictionary", "IDictionary")] string structure) + { + // arrange + string start = Guid.NewGuid().ToString("N"); + string middle = Guid.NewGuid().ToString("N"); + string end = Guid.NewGuid().ToString("N"); + const string input = "{{start}} tokens are properly replaced (including {{middle}} {{ MIDdlE}}) {{end}}"; + string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", input); + switch (structure) + { + case "anonymous object": + translation = translation.Tokens(new { start, middle, end }); + break; + + case "class": + translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end }); + break; + + case "IDictionary": + translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); + break; + + case "IDictionary": + translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); + break; + + default: + throw new NotSupportedException($"Unknown structure '{structure}'."); + } + + // assert + Assert.AreEqual(expected, translation.ToString(), "The translation returned an unexpected text."); + } + + [Test(Description = "Assert that the translation can replace tokens in all valid formats.")] + [TestCase("{{value}}", "value")] + [TestCase("{{ value }}", "value")] + [TestCase("{{value }}", "value")] + [TestCase("{{ the_value }}", "the_value")] + [TestCase("{{ the.value_here }}", "the.value_here")] + [TestCase("{{ the_value-here.... }}", "the_value-here....")] + [TestCase("{{ tHe_vALuE-HEre.... }}", "tHe_vALuE-HEre....")] + public void Translation_Tokens_ValidFormats(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + [Test(Description = "Assert that translation tokens are case-insensitive and surrounding-whitespace-insensitive.")] + [TestCase("{{value}}", "value")] + [TestCase("{{VaLuE}}", "vAlUe")] + [TestCase("{{VaLuE }}", " vAlUe")] + public void Translation_Tokens_KeysAreNormalised(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + + /********* + ** Private methods + *********/ + /// Set a translation helper's locale and assert that it was set correctly. + /// The translation helper to change. + /// The expected locale. + /// The expected game language code. + private void AssertSetLocale(TranslationHelper helper, string locale, LocalizedContentManager.LanguageCode localeEnum) + { + helper.SetLocale(locale, localeEnum); + Assert.AreEqual(locale, helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(localeEnum, helper.LocaleEnum, "The locale enum doesn't match the input value."); + } + + /// Get sample raw translations to input. + private IDictionary> GetSampleData() + { + return new Dictionary> + { + ["default"] = new Dictionary + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B" + }, + ["en-US"] = new Dictionary(), + ["zzz"] = new Dictionary + { + ["key A"] = "zzz A" + } + }; + } + + /// Get the expected translation output given , based on the expected locale fallback. + private IDictionary GetExpectedTranslations() + { + var expected = new Dictionary + { + ["default"] = new[] + { + new Translation(string.Empty, "default", "key A", "default A"), + new Translation(string.Empty, "default", "key C", "default C") + }, + ["en"] = new[] + { + new Translation(string.Empty, "en", "key A", "en A"), + new Translation(string.Empty, "en", "key B", "en B"), + new Translation(string.Empty, "en", "key C", "default C") + }, + ["zzz"] = new[] + { + new Translation(string.Empty, "zzz", "key A", "zzz A"), + new Translation(string.Empty, "zzz", "key C", "default C") + } + }; + expected["en-us"] = expected["en"].ToArray(); + return expected; + } + + /// Get whether two translations have the same public values. + /// The first translation to compare. + /// The second translation to compare. + private bool CompareEquality(Translation a, Translation b) + { + return a.Key == b.Key && a.ToString() == b.ToString(); + } + + /// Get the default placeholder text when a translation is missing. + /// The translation key. + private string GetPlaceholderText(string key) + { + return string.Format(Translation.PlaceholderText, key); + } + + + /********* + ** Test models + *********/ + /// A model used to test token support. + private class TokenModel + { + /// A sample token property. + public string Start { get; set; } + + /// A sample token property. + public string Middle { get; set; } + + /// A sample token field. + public string End; + } + } +} diff --git a/src/SMAPI.Tests/Properties/AssemblyInfo.cs b/src/SMAPI.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ee09145b --- /dev/null +++ b/src/SMAPI.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.Tests")] +[assembly: AssemblyDescription("")] +[assembly: Guid("36ccb19e-92eb-48c7-9615-98eefd45109b")] diff --git a/src/SMAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs new file mode 100644 index 00000000..99835d92 --- /dev/null +++ b/src/SMAPI.Tests/Sample.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Tests +{ + /// Provides sample values for unit testing. + internal static class Sample + { + /********* + ** Properties + *********/ + /// A random number generator. + private static readonly Random Random = new Random(); + + + /********* + ** Properties + *********/ + /// Get a sample string. + public static string String() + { + return Guid.NewGuid().ToString("N"); + } + + /// Get a sample integer. + public static int Int() + { + return Sample.Random.Next(); + } + } +} diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj new file mode 100644 index 00000000..42c3318f --- /dev/null +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -0,0 +1,69 @@ + + + + + Debug + x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B} + Library + Properties + StardewModdingAPI.Tests + StardewModdingAPI.Tests + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll + + + ..\packages\Moq.4.7.99\lib\net45\Moq.dll + + + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + + + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} + StardewModdingAPI + + + + + \ No newline at end of file diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs new file mode 100644 index 00000000..25acbaf3 --- /dev/null +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; +using NUnit.Framework; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Tests.Utilities +{ + /// Unit tests for . + [TestFixture] + internal class SDateTests + { + /********* + ** Properties + *********/ + /// All valid seasons. + private static readonly string[] ValidSeasons = { "spring", "summer", "fall", "winter" }; + + /// All valid days of a month. + private static readonly int[] ValidDays = Enumerable.Range(1, 28).ToArray(); + + /// Sample relative dates for test cases. + private static class Dates + { + /// The base date to which other dates are relative. + public const string Now = "02 summer Y2"; + + /// The day before . + public const string PrevDay = "01 summer Y2"; + + /// The month before . + public const string PrevMonth = "02 spring Y2"; + + /// The year before . + public const string PrevYear = "02 summer Y1"; + + /// The day after . + public const string NextDay = "03 summer Y2"; + + /// The month after . + public const string NextMonth = "02 fall Y2"; + + /// The year after . + public const string NextYear = "02 summer Y3"; + } + + + /********* + ** Unit tests + *********/ + /**** + ** Constructor + ****/ + [Test(Description = "Assert that the constructor sets the expected values for all valid dates.")] + public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.ValidSeasons))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year) + { + // act + SDate date = new SDate(day, season, year); + + // assert + Assert.AreEqual(day, date.Day); + Assert.AreEqual(season, date.Season); + Assert.AreEqual(year, date.Year); + } + + [Test(Description = "Assert that the constructor throws an exception if the values are invalid.")] + [TestCase(01, "Spring", 1)] // seasons are case-sensitive + [TestCase(01, "springs", 1)] // invalid season name + [TestCase(-1, "spring", 1)] // day < 0 + [TestCase(29, "spring", 1)] // day > 28 + [TestCase(01, "spring", -1)] // year < 1 + [TestCase(01, "spring", 0)] // year < 1 + [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")] + public void Constructor_RejectsInvalidValues(int day, string season, int year) + { + // act & assert + Assert.Throws(() => _ = new SDate(day, season, year), "Constructing the invalid date didn't throw the expected exception."); + } + + /**** + ** ToString + ****/ + [Test(Description = "Assert that ToString returns the expected string.")] + [TestCase("14 spring Y1", ExpectedResult = "14 spring Y1")] + [TestCase("01 summer Y16", ExpectedResult = "01 summer Y16")] + [TestCase("28 fall Y10", ExpectedResult = "28 fall Y10")] + [TestCase("01 winter Y1", ExpectedResult = "01 winter Y1")] + public string ToString(string dateStr) + { + return this.GetDate(dateStr).ToString(); + } + + /**** + ** AddDays + ****/ + [Test(Description = "Assert that AddDays returns the expected date.")] + [TestCase("01 spring Y1", 15, ExpectedResult = "16 spring Y1")] // day transition + [TestCase("01 spring Y1", 28, ExpectedResult = "01 summer Y1")] // season transition + [TestCase("01 spring Y1", 28 * 4, ExpectedResult = "01 spring Y2")] // year transition + [TestCase("01 spring Y1", 28 * 7 + 17, ExpectedResult = "18 winter Y2")] // year transition + [TestCase("15 spring Y1", -14, ExpectedResult = "01 spring Y1")] // negative day transition + [TestCase("15 summer Y1", -28, ExpectedResult = "15 spring Y1")] // negative season transition + [TestCase("15 summer Y2", -28 * 4, ExpectedResult = "15 summer Y1")] // negative year transition + [TestCase("01 spring Y3", -(28 * 7 + 17), ExpectedResult = "12 spring Y1")] // negative year transition + [TestCase("06 fall Y2", 50, ExpectedResult = "28 winter Y3")] // test for zero-index errors + [TestCase("06 fall Y2", 51, ExpectedResult = "01 spring Y3")] // test for zero-index errors + public string AddDays(string dateStr, int addDays) + { + return this.GetDate(dateStr).AddDays(addDays).ToString(); + } + + /**** + ** GetHashCode + ****/ + [Test(Description = "Assert that GetHashCode returns a unique ordered value for every date.")] + public void GetHashCode_ReturnsUniqueOrderedValue() + { + IDictionary hashes = new Dictionary(); + int lastHash = int.MinValue; + for (int year = 1; year <= 4; year++) + { + foreach (string season in SDateTests.ValidSeasons) + { + foreach (int day in SDateTests.ValidDays) + { + SDate date = new SDate(day, season, year); + int hash = date.GetHashCode(); + if (hashes.TryGetValue(hash, out SDate otherDate)) + Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}."); + if (hash < lastHash) + Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash})."); + + lastHash = hash; + hashes[hash] = date; + } + } + } + } + + [Test(Description = "Assert that the == operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] + [TestCase(Dates.Now, null, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.Now, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] + public bool Operators_Equals(string now, string other) + { + return this.GetDate(now) == this.GetDate(other); + } + + [Test(Description = "Assert that the != operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] + [TestCase(Dates.Now, null, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] + public bool Operators_NotEquals(string now, string other) + { + return this.GetDate(now) != this.GetDate(other); + } + + [Test(Description = "Assert that the < operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] + [TestCase(Dates.Now, null, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] + public bool Operators_LessThan(string now, string other) + { + return this.GetDate(now) < this.GetDate(other); + } + + [Test(Description = "Assert that the <= operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] + [TestCase(Dates.Now, null, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.Now, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] + public bool Operators_LessThanOrEqual(string now, string other) + { + return this.GetDate(now) <= this.GetDate(other); + } + + [Test(Description = "Assert that the > operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] + [TestCase(Dates.Now, null, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] + public bool Operators_MoreThan(string now, string other) + { + return this.GetDate(now) > this.GetDate(other); + } + + [Test(Description = "Assert that the > operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] + [TestCase(Dates.Now, null, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = true)] + [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] + [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] + public bool Operators_MoreThanOrEqual(string now, string other) + { + return this.GetDate(now) > this.GetDate(other); + } + + + /********* + ** Private methods + *********/ + /// Convert a string date into a game date, to make unit tests easier to read. + /// The date string like "dd MMMM yy". + private SDate GetDate(string dateStr) + { + if (dateStr == null) + return null; + + void Fail(string reason) => throw new AssertionException($"Couldn't parse date '{dateStr}' because {reason}."); + + // parse + Match match = Regex.Match(dateStr, @"^(?\d+) (?\w+) Y(?\d+)$"); + if (!match.Success) + Fail("it doesn't match expected pattern (should be like 28 spring Y1)"); + + // extract parts + string season = match.Groups["season"].Value; + if (!int.TryParse(match.Groups["day"].Value, out int day)) + Fail($"'{match.Groups["day"].Value}' couldn't be parsed as a day."); + if (!int.TryParse(match.Groups["year"].Value, out int year)) + Fail($"'{match.Groups["year"].Value}' couldn't be parsed as a year."); + + // build date + return new SDate(day, season, year); + } + } +} diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs new file mode 100644 index 00000000..03cd26c9 --- /dev/null +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -0,0 +1,302 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; +using NUnit.Framework; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Tests.Utilities +{ + /// Unit tests for . + [TestFixture] + internal class SemanticVersionTests + { + /********* + ** Unit tests + *********/ + /**** + ** Constructor + ****/ + [Test(Description = "Assert that the constructor sets the expected values for all valid versions.")] + [TestCase("1.0", ExpectedResult = "1.0")] + [TestCase("1.0.0", ExpectedResult = "1.0")] + [TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")] + [TestCase("1.2-some-tag.4", ExpectedResult = "1.2-some-tag.4")] + [TestCase("1.2.3-some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] + [TestCase("1.2.3-some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] + public string Constructor_FromString(string input) + { + return new SemanticVersion(input).ToString(); + } + + [Test(Description = "Assert that the constructor sets the expected values for all valid versions.")] + [TestCase(1, 0, 0, null, ExpectedResult = "1.0")] + [TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] + [TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] + [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] + [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] + [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] + public string Constructor_FromParts(int major, int minor, int patch, string tag) + { + // act + ISemanticVersion version = new SemanticVersion(major, minor, patch, tag); + + // assert + Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); + Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); + Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); + Assert.AreEqual(string.IsNullOrWhiteSpace(tag) ? null : tag.Trim(), version.Build, "The tag doesn't match the given value."); + return version.ToString(); + } + + [Test(Description = "Assert that the constructor throws the expected exception for invalid versions.")] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("1")] + [TestCase("01.0")] + [TestCase("1.05")] + [TestCase("1.5.06")] // leading zeros specifically prohibited by spec + [TestCase("1.2.3.4")] + [TestCase("1.apple")] + [TestCase("1.2.apple")] + [TestCase("1.2.3.apple")] + [TestCase("1..2..3")] + [TestCase("1.2.3-")] + [TestCase("1.2.3-some-tag...")] + [TestCase("1.2.3-some-tag...4")] + [TestCase("apple")] + [TestCase("-apple")] + [TestCase("-5")] + public void Constructor_FromString_WithInvalidValues(string input) + { + if (input == null) + this.AssertAndLogException(() => new SemanticVersion(input)); + else + this.AssertAndLogException(() => new SemanticVersion(input)); + } + + /**** + ** CompareTo + ****/ + [Test(Description = "Assert that version.CompareTo returns the expected value.")] + // equal + [TestCase("0.5.7", "0.5.7", ExpectedResult = 0)] + [TestCase("1.0", "1.0", ExpectedResult = 0)] + [TestCase("1.0-beta", "1.0-beta", ExpectedResult = 0)] + [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = 0)] + [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = 0)] + + // less than + [TestCase("0.5.7", "0.5.8", ExpectedResult = -1)] + [TestCase("1.0", "1.1", ExpectedResult = -1)] + [TestCase("1.0-beta", "1.0", ExpectedResult = -1)] + [TestCase("1.0-beta", "1.0-beta.2", ExpectedResult = -1)] + [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = -1)] + [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = -1)] + [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = -1)] + + // more than + [TestCase("0.5.8", "0.5.7", ExpectedResult = 1)] + [TestCase("1.1", "1.0", ExpectedResult = 1)] + [TestCase("1.0", "1.0-beta", ExpectedResult = 1)] + [TestCase("1.0-beta.2", "1.0-beta", ExpectedResult = 1)] + [TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = 1)] + [TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = 1)] + [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = 1)] + public int CompareTo(string versionStrA, string versionStrB) + { + ISemanticVersion versionA = new SemanticVersion(versionStrA); + ISemanticVersion versionB = new SemanticVersion(versionStrB); + return versionA.CompareTo(versionB); + } + + /**** + ** IsOlderThan + ****/ + [Test(Description = "Assert that version.IsOlderThan returns the expected value.")] + // keep test cases in sync with CompareTo for simplicity. + // equal + [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] + [TestCase("1.0", "1.0", ExpectedResult = false)] + [TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)] + [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)] + [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)] + + // less than + [TestCase("0.5.7", "0.5.8", ExpectedResult = true)] + [TestCase("1.0", "1.1", ExpectedResult = true)] + [TestCase("1.0-beta", "1.0", ExpectedResult = true)] + [TestCase("1.0-beta", "1.0-beta.2", ExpectedResult = true)] + [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = true)] + [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = true)] + [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = true)] + + // more than + [TestCase("0.5.8", "0.5.7", ExpectedResult = false)] + [TestCase("1.1", "1.0", ExpectedResult = false)] + [TestCase("1.0", "1.0-beta", ExpectedResult = false)] + [TestCase("1.0-beta.2", "1.0-beta", ExpectedResult = false)] + [TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = false)] + [TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = false)] + [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = false)] + public bool IsOlderThan(string versionStrA, string versionStrB) + { + ISemanticVersion versionA = new SemanticVersion(versionStrA); + ISemanticVersion versionB = new SemanticVersion(versionStrB); + return versionA.IsOlderThan(versionB); + } + + /**** + ** IsNewerThan + ****/ + [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] + // keep test cases in sync with CompareTo for simplicity. + // equal + [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] + [TestCase("1.0", "1.0", ExpectedResult = false)] + [TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)] + [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)] + [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)] + + // less than + [TestCase("0.5.7", "0.5.8", ExpectedResult = false)] + [TestCase("1.0", "1.1", ExpectedResult = false)] + [TestCase("1.0-beta", "1.0", ExpectedResult = false)] + [TestCase("1.0-beta", "1.0-beta.2", ExpectedResult = false)] + [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = false)] + [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = false)] + [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = false)] + + // more than + [TestCase("0.5.8", "0.5.7", ExpectedResult = true)] + [TestCase("1.1", "1.0", ExpectedResult = true)] + [TestCase("1.0", "1.0-beta", ExpectedResult = true)] + [TestCase("1.0-beta.2", "1.0-beta", ExpectedResult = true)] + [TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = true)] + [TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = true)] + [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = true)] + public bool IsNewerThan(string versionStrA, string versionStrB) + { + ISemanticVersion versionA = new SemanticVersion(versionStrA); + ISemanticVersion versionB = new SemanticVersion(versionStrB); + return versionA.IsNewerThan(versionB); + } + + /**** + ** IsBetween + ****/ + [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] + // is between + [TestCase("0.5.7-beta.3", "0.5.7-beta.3", "0.5.7-beta.3", ExpectedResult = true)] + [TestCase("1.0", "1.0", "1.1", ExpectedResult = true)] + [TestCase("1.0", "1.0-beta", "1.1", ExpectedResult = true)] + [TestCase("1.0", "0.5", "1.1", ExpectedResult = true)] + [TestCase("1.0-beta.2", "1.0-beta.1", "1.0-beta.3", ExpectedResult = true)] + [TestCase("1.0-beta-2", "1.0-beta-1", "1.0-beta-3", ExpectedResult = true)] + + // is not between + [TestCase("1.0-beta", "1.0", "1.1", ExpectedResult = false)] + [TestCase("1.0", "1.1", "1.0", ExpectedResult = false)] + [TestCase("1.0-beta.2", "1.1", "1.0", ExpectedResult = false)] + [TestCase("1.0-beta.2", "1.0-beta.10", "1.0-beta.3", ExpectedResult = false)] + [TestCase("1.0-beta-2", "1.0-beta-10", "1.0-beta-3", ExpectedResult = false)] + public bool IsBetween(string versionStr, string lowerStr, string upperStr) + { + ISemanticVersion lower = new SemanticVersion(lowerStr); + ISemanticVersion upper = new SemanticVersion(upperStr); + ISemanticVersion version = new SemanticVersion(versionStr); + return version.IsBetween(lower, upper); + } + + /**** + ** Serialisable + ****/ + [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] + [TestCase("1.0")] + public void Serialisable(string versionStr) + { + // act + string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr)); + SemanticVersion after = JsonConvert.DeserializeObject(json); + + // assert + Assert.IsNotNull(after, "The semantic version after deserialisation is unexpectedly null."); + Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialisation doesn't match the input version."); + } + + /**** + ** GameVersion + ****/ + [Test(Description = "Assert that the GameVersion subclass correctly parses legacy game versions.")] + [TestCase("1.0")] + [TestCase("1.01")] + [TestCase("1.02")] + [TestCase("1.03")] + [TestCase("1.04")] + [TestCase("1.05")] + [TestCase("1.051")] + [TestCase("1.051b")] + [TestCase("1.06")] + [TestCase("1.07")] + [TestCase("1.07a")] + [TestCase("1.1")] + [TestCase("1.11")] + [TestCase("1.2")] + [TestCase("1.2.15")] + public void GameVersion(string versionStr) + { + // act + GameVersion version = new GameVersion(versionStr); + + // assert + Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value."); + Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); + } + + + /********* + ** Private methods + *********/ + /// Assert that the expected exception type is thrown, and log the action output and thrown exception. + /// The expected exception type. + /// The action which may throw the exception. + /// The message to log if the expected exception isn't thrown. + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")] + private void AssertAndLogException(Func action, string message = null) + where T : Exception + { + this.AssertAndLogException(() => + { + object result = action(); + TestContext.WriteLine($"Func result: {result}"); + }); + } + + /// Assert that the expected exception type is thrown, and log the thrown exception. + /// The expected exception type. + /// The action which may throw the exception. + /// The message to log if the expected exception isn't thrown. + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")] + private void AssertAndLogException(Action action, string message = null) + where T : Exception + { + try + { + action(); + } + catch (T ex) + { + TestContext.WriteLine($"Exception thrown:\n{ex}"); + return; + } + catch (Exception ex) when (!(ex is AssertionException)) + { + TestContext.WriteLine($"Exception thrown:\n{ex}"); + Assert.Fail(message ?? $"Didn't throw the expected exception; expected {typeof(T).FullName}, got {ex.GetType().FullName}."); + } + + // no exception thrown + Assert.Fail(message ?? "Didn't throw an exception."); + } + } +} diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config new file mode 100644 index 00000000..5fdfebdb --- /dev/null +++ b/src/SMAPI.Tests/packages.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs new file mode 100644 index 00000000..7dcfcf13 --- /dev/null +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Models; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides an API to perform mod update checks. + [Produces("application/json")] + [Route("api/{version:semanticVersion}/[controller]")] + internal class ModsController : Controller + { + /********* + ** Properties + *********/ + /// The mod repositories which provide mod metadata. + private readonly IDictionary Repositories; + + /// The cache in which to store mod metadata. + private readonly IMemoryCache Cache; + + /// The number of minutes update checks should be cached before refetching them. + private readonly int CacheMinutes; + + /// A regex which matches SMAPI-style semantic version. + private readonly string VersionRegex; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cache in which to store mod metadata. + /// The config settings for mod update checks. + public ModsController(IMemoryCache cache, IOptions configProvider) + { + ModUpdateCheckConfig config = configProvider.Value; + + this.Cache = cache; + this.CacheMinutes = config.CacheMinutes; + this.VersionRegex = config.SemanticVersionRegex; + + string version = this.GetType().Assembly.GetName().Version.ToString(3); + this.Repositories = + new IModRepository[] + { + new ChucklefishRepository( + vendorKey: config.ChucklefishKey, + userAgent: string.Format(config.ChucklefishUserAgent, version), + baseUrl: config.ChucklefishBaseUrl, + modPageUrlFormat: config.ChucklefishModPageUrlFormat + ), + new GitHubRepository( + vendorKey: config.GitHubKey, + baseUrl: config.GitHubBaseUrl, + releaseUrlFormat: config.GitHubReleaseUrlFormat, + userAgent: string.Format(config.GitHubUserAgent, version), + acceptHeader: config.GitHubAcceptHeader, + username: config.GitHubUsername, + password: config.GitHubPassword + ), + new NexusRepository( + vendorKey: config.NexusKey, + userAgent: config.NexusUserAgent, + baseUrl: config.NexusBaseUrl, + modUrlFormat: config.NexusModUrlFormat + ) + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + } + + /// Fetch version metadata for the given mods. + /// The namespaced mod keys to search as a comma-delimited array. + [HttpGet] + public async Task> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// Fetch version metadata for the given mods. + /// The mod search criteria. + [HttpPost] + public async Task> PostAsync([FromBody] ModSearchModel search) + { + // sort & filter keys + string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) + .ToArray(); + + // fetch mod info + IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (string modKey in modKeys) + { + // parse mod key + if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) + { + result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + continue; + } + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + { + result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + continue; + } + + // fetch mod info + result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + + ModInfoModel info = await repository.GetModInfoAsync(modID); + if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) + info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + + return info; + }); + } + + return result; + } + + + /********* + ** Private methods + *********/ + /// Parse a namespaced mod ID. + /// The raw mod ID to parse. + /// The parsed vendor key. + /// The parsed mod ID. + /// Returns whether the value could be parsed. + private bool TryParseModKey(string raw, out string vendorKey, out string modID) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + { + vendorKey = null; + modID = null; + return false; + } + + // parse + vendorKey = parts[0].Trim(); + modID = parts[1].Trim(); + return true; + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs new file mode 100644 index 00000000..03de639e --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -0,0 +1,74 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for mod update checks. + public class ModUpdateCheckConfig + { + /********* + ** Accessors + *********/ + /**** + ** General + ****/ + /// The number of minutes update checks should be cached before refetching them. + public int CacheMinutes { get; set; } + + /// A regex which matches SMAPI-style semantic version. + /// Derived from SMAPI's SemanticVersion implementation. + public string SemanticVersionRegex { get; set; } + + /**** + ** Chucklefish mod site + ****/ + /// The repository key for the Chucklefish mod site. + public string ChucklefishKey { get; set; } + + /// The user agent for the Chucklefish API client, where {0} is the SMAPI version. + public string ChucklefishUserAgent { get; set; } + + /// The base URL for the Chucklefish mod site. + public string ChucklefishBaseUrl { get; set; } + + /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. + public string ChucklefishModPageUrlFormat { get; set; } + + + /**** + ** GitHub + ****/ + /// The repository key for Nexus Mods. + public string GitHubKey { get; set; } + + /// The user agent for the GitHub API client, where {0} is the SMAPI version. + public string GitHubUserAgent { get; set; } + + /// The base URL for the GitHub API. + public string GitHubBaseUrl { get; set; } + + /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. + public string GitHubReleaseUrlFormat { get; set; } + + /// The Accept header value expected by the GitHub API. + public string GitHubAcceptHeader { get; set; } + + /// The username with which to authenticate to the GitHub API (if any). + public string GitHubUsername { get; set; } + + /// The password with which to authenticate to the GitHub API (if any). + public string GitHubPassword { get; set; } + + /**** + ** Nexus Mods + ****/ + /// The repository key for Nexus Mods. + public string NexusKey { get; set; } + + /// The user agent for the Nexus Mods API client. + public string NexusUserAgent { get; set; } + + /// The base URL for the Nexus Mods API. + public string NexusBaseUrl { get; set; } + + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public string NexusModUrlFormat { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs new file mode 100644 index 00000000..2c24c610 --- /dev/null +++ b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace StardewModdingAPI.Web.Framework +{ + /// Discovers controllers with support for non-public controllers. + internal class InternalControllerFeatureProvider : ControllerFeatureProvider + { + /********* + ** Public methods + *********/ + /// Determines if a given type is a controller. + /// The candidate. + /// true if the type is a controller; otherwise false. + protected override bool IsController(TypeInfo type) + { + return + type.IsClass + && !type.IsAbstract + && (/*type.IsPublic &&*/ !type.ContainsGenericParameters) + && (!type.IsDefined(typeof(NonControllerAttribute)) + && (type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || type.IsDefined(typeof(ControllerAttribute)))); + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs new file mode 100644 index 00000000..d98acd89 --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -0,0 +1,51 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + internal abstract class RepositoryBase : IModRepository + { + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public abstract void Dispose(); + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public abstract Task GetModInfoAsync(string id); + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + protected RepositoryBase(string vendorKey) + { + this.VendorKey = vendorKey; + } + + /// Normalise a version string. + /// The version to normalise. + protected string NormaliseVersion(string version) + { + if (string.IsNullOrWhiteSpace(version)) + return null; + + version = version.Trim(); + if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix + version = version.Substring(1); + + return version; + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs new file mode 100644 index 00000000..ed7bd60b --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -0,0 +1,92 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from the Chucklefish mod site. + internal class ChucklefishRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// The base URL for the Chucklefish mod site. + private readonly string BaseUrl; + + /// The URL for a mod page excluding the base URL, where {0} is the mod ID. + private readonly string ModPageUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The user agent for the API client. + /// The base URL for the Chucklefish mod site. + /// The URL for a mod page excluding the , where {0} is the mod ID. + public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) + : base(vendorKey) + { + this.BaseUrl = baseUrl; + this.ModPageUrlFormat = modPageUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!uint.TryParse(id, out uint _)) + return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + + // fetch info + try + { + // fetch HTML + string html; + try + { + html = await this.Client + .GetAsync(string.Format(this.ModPageUrlFormat, id)) + .AsString(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new ModInfoModel("Found no mod with this ID."); + } + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // extract mod info + string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); + string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; + if (name.StartsWith("[SMAPI] ")) + name = name.Substring("[SMAPI] ".Length); + string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; + + // create model + return new ModInfoModel(name, this.NormaliseVersion(version), url); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs new file mode 100644 index 00000000..174fb79a --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -0,0 +1,97 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Pathoschild.Http.Client; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from GitHub project releases. + internal class GitHubRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ReleaseUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + /// The user agent for the API client. + /// The Accept header value expected by the GitHub API. + /// The username with which to authenticate to the GitHub API. + /// The password with which to authenticate to the GitHub API. + public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + : base(vendorKey) + { + this.ReleaseUrlFormat = releaseUrlFormat; + + this.Client = new FluentClient(baseUrl) + .SetUserAgent(userAgent) + .AddDefault(req => req.WithHeader("Accept", acceptHeader)); + if (!string.IsNullOrWhiteSpace(username)) + this.Client = this.Client.SetBasicAuthentication(username, password); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) + return new ModInfoModel($"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); + + // fetch info + try + { + GitRelease release = await this.Client + .GetAsync(string.Format(this.ReleaseUrlFormat, id)) + .As(); + return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new ModInfoModel("Found no mod with this ID."); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// Metadata about a GitHub release tag. + private class GitRelease + { + /********* + ** Accessors + *********/ + /// The display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The semantic version string. + [JsonProperty("tag_name")] + public string Tag { get; set; } + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs new file mode 100644 index 00000000..98e4c957 --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// A repository which provides mod metadata. + internal interface IModRepository : IDisposable + { + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + Task GetModInfoAsync(string id); + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs new file mode 100644 index 00000000..71970bec --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Pathoschild.Http.Client; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from Nexus Mods. + internal class NexusRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ModUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The user agent for the Nexus Mods API client. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) + : base(vendorKey) + { + this.ModUrlFormat = modUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!uint.TryParse(id, out uint _)) + return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + + // fetch info + try + { + NexusResponseModel response = await this.Client + .GetAsync(string.Format(this.ModUrlFormat, id)) + .As(); + + return response != null + ? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url) + : new ModInfoModel("Found no mod with this ID."); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// A mod metadata response from Nexus Mods. + private class NexusResponseModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + } + } +} diff --git a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs new file mode 100644 index 00000000..5a56844f --- /dev/null +++ b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework +{ + /// Rewrite requests to prepend the subdomain portion (if any) to the path. + /// Derived from . + internal class RewriteSubdomainRule : IRule + { + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + context.Result = RuleResult.ContinueRules; + + // get host parts + string host = context.HttpContext.Request.Host.Host; + string[] parts = host.Split('.'); + + // validate + if (parts.Length < 2) + return; + if (parts.Length < 3 && !"localhost".Equals(parts[1], StringComparison.InvariantCultureIgnoreCase)) + return; + + // prepend to path + context.HttpContext.Request.Path = $"/{parts[0]}{context.HttpContext.Request.Path}"; + } + } +} diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs new file mode 100644 index 00000000..be9c0918 --- /dev/null +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Routing.Constraints; + +namespace StardewModdingAPI.Web.Framework +{ + /// Constrains a route value to a valid semantic version. + internal class VersionConstraint : RegexRouteConstraint + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public VersionConstraint() + : base(@"^v(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$") { } + } +} diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs new file mode 100644 index 00000000..eeecb791 --- /dev/null +++ b/src/SMAPI.Web/Program.cs @@ -0,0 +1,26 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; + +namespace StardewModdingAPI.Web +{ + /// The main app entry point. + public class Program + { + /********* + ** Public methods + *********/ + /// The main app entry point. + /// The command-line arguments. + public static void Main(string[] args) + { + // configure web server + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build() + .Run(); + } + } +} diff --git a/src/SMAPI.Web/Properties/AssemblyInfo.cs b/src/SMAPI.Web/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..63f787a4 --- /dev/null +++ b/src/SMAPI.Web/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("StardewModdingAPI.Web")] +[assembly: AssemblyProduct("StardewModdingAPI.Web")] diff --git a/src/SMAPI.Web/Properties/launchSettings.json b/src/SMAPI.Web/Properties/launchSettings.json new file mode 100644 index 00000000..3acee14d --- /dev/null +++ b/src/SMAPI.Web/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59482/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/v1.0/mods", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Dewdrop": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/v1.0/mods", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:59483" + } + } +} diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj new file mode 100644 index 00000000..6b1d0687 --- /dev/null +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp2.0 + false + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs new file mode 100644 index 00000000..eaf14983 --- /dev/null +++ b/src/SMAPI.Web/Startup.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.ConfigModels; + +namespace StardewModdingAPI.Web +{ + /// The web app startup configuration. + internal class Startup + { + /********* + ** Accessors + *********/ + /// The web app configuration. + public IConfigurationRoot Configuration { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The hosting environment. + public Startup(IHostingEnvironment env) + { + this.Configuration = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddEnvironmentVariables() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables() + .Build(); + } + + /// The method called by the runtime to add services to the container. + /// The service injection container. + public void ConfigureServices(IServiceCollection services) + { + services + .Configure(this.Configuration.GetSection("ModUpdateCheck")) + .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) + .AddMemoryCache() + .AddMvc() + .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) + .AddJsonOptions(options => + { + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + }); + } + + /// The method called by the runtime to configure the HTTP request pipeline. + /// The application builder. + /// The hosting environment. + /// The logger factory. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + app + .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseMvc(); + } + } +} diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json new file mode 100644 index 00000000..fa8ce71a --- /dev/null +++ b/src/SMAPI.Web/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json new file mode 100644 index 00000000..852f6f71 --- /dev/null +++ b/src/SMAPI.Web/appsettings.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "ModUpdateCheck": { + "CacheMinutes": 60, + "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", + + "ChucklefishKey": "Chucklefish", + "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "ChucklefishBaseUrl": "https://community.playstarbound.com", + "ChucklefishModPageUrlFormat": "resources/{0}", + + "GitHubKey": "GitHub", + "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "GitHubBaseUrl": "https://api.github.com", + "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubAcceptHeader": "application/vnd.github.v3+json", + "GitHubUsername": null, /* set via environment properties */ + "GitHubPassword": null, /* set via environment properties */ + + "NexusKey": "Nexus", + "NexusUserAgent": "Nexus Client v0.63.15", + "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", + "NexusModUrlFormat": "mods/{0}" + } +} diff --git a/src/SMAPI.sln b/src/SMAPI.sln new file mode 100644 index 00000000..5936ff43 --- /dev/null +++ b/src/SMAPI.sln @@ -0,0 +1,138 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.16 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + ..\.gitattributes = ..\.gitattributes + ..\.gitignore = ..\.gitignore + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" + ProjectSection(ProjectDependencies) = postProject + {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Models", "SMAPI.Models\StardewModdingAPI.Models.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" + ProjectSection(SolutionItems) = preProject + ..\docs\CONTRIBUTING.md = ..\docs\CONTRIBUTING.md + ..\docs\LICENSE.md = ..\docs\LICENSE.md + ..\docs\README.md = ..\docs\README.md + ..\docs\release-notes.md = ..\docs\release-notes.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" + ProjectSection(SolutionItems) = preProject + ..\build\common.targets = ..\build\common.targets + ..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs + ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets + EndProjectSection +EndProject +Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + SMAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 + SMAPI.Models\StardewModdingAPI.Models.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.ActiveCfg = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.Build.0 = Debug|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|Mixed Platforms.Build.0 = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.ActiveCfg = Release|x86 + {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.Build.0 = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Mixed Platforms.Build.0 = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.ActiveCfg = Release|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.ActiveCfg = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.Build.0 = Debug|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 + {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.ActiveCfg = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.Build.0 = Debug|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.Build.0 = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86 + {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} + {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} + EndGlobalSection +EndGlobal diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings new file mode 100644 index 00000000..d16ef684 --- /dev/null +++ b/src/SMAPI.sln.DotSettings @@ -0,0 +1,19 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + HINT + HINT + Field, Property, Event, Method + Field, Property, Event, Method + True + False + UseVarWhenEvident + UseExplicitType + ID + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + True + True + True + \ No newline at end of file diff --git a/src/SMAPI/App.config b/src/SMAPI/App.config new file mode 100644 index 00000000..27cdf0f7 --- /dev/null +++ b/src/SMAPI/App.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs new file mode 100644 index 00000000..4d0a9ca9 --- /dev/null +++ b/src/SMAPI/Constants.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.ModLoading; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Contains SMAPI's constants and assumptions. + public static class Constants + { + /********* + ** Properties + *********/ + /// The directory path containing the current save's data (if a save is loaded). + private static string RawSavePath => Context.IsSaveLoaded ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : null; + + /// Whether the directory containing the current save's data exists on disk. + private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); + + + /********* + ** Accessors + *********/ + /**** + ** Public + ****/ + /// SMAPI's current semantic version. + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, "beta.1"); + + /// The minimum supported version of Stardew Valley. + public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); + + /// The maximum supported version of Stardew Valley. + public static ISemanticVersion MaximumGameVersion { get; } = null; + + /// The path to the game folder. + public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + /// The directory path containing Stardew Valley's app data. + public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); + + /// The directory path in which error logs should be stored. + public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs"); + + /// The directory path where all saves are stored. + public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves"); + + /// The directory name containing the current save's data (if a save is loaded and the directory exists). + public static string SaveFolderName => Context.IsSaveLoaded ? Constants.GetSaveFolderName() : ""; + + /// The directory path containing the current save's data (if a save is loaded and the directory exists). + public static string CurrentSavePath => Constants.SavePathReady ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : ""; + + /**** + ** Internal + ****/ + /// The GitHub repository to check for updates. + internal const string GitHubRepository = "Pathoschild/SMAPI"; + + /// The file path for the SMAPI configuration file. + internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); + + /// The file path to the log where the latest output should be saved. + internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); + + /// A copy of the log leading up to the previous fatal crash, if any. + internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); + + /// The file path which stores a fatal crash message for the next run. + internal static string FatalCrashMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.crash.marker"); + + /// The full path to the folder containing mods. + internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); + + /// The game's current semantic version. + internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); + + /// The target game platform. + internal static Platform TargetPlatform { get; } = +#if SMAPI_FOR_WINDOWS + Platform.Windows; +#else + Platform.Mono; +#endif + + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API. + internal static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", + ["GitHub"] = "https://github.com/{0}/releases" + }; + + + /********* + ** Internal methods + *********/ + /// Get metadata for mapping assemblies to the current platform. + /// The target game platform. + internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) + { + // get assembly changes needed for platform + string[] removeAssemblyReferences; + Assembly[] targetAssemblies; + switch (targetPlatform) + { + case Platform.Mono: + removeAssemblyReferences = new[] + { + "Stardew Valley", + "Microsoft.Xna.Framework", + "Microsoft.Xna.Framework.Game", + "Microsoft.Xna.Framework.Graphics" + }; + targetAssemblies = new[] + { + typeof(StardewValley.Game1).Assembly, + typeof(Microsoft.Xna.Framework.Vector2).Assembly + }; + break; + + case Platform.Windows: + removeAssemblyReferences = new[] + { + "StardewValley", + "MonoGame.Framework" + }; + targetAssemblies = new[] + { + typeof(StardewValley.Game1).Assembly, + typeof(Microsoft.Xna.Framework.Vector2).Assembly, + typeof(Microsoft.Xna.Framework.Game).Assembly, + typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly + }; + break; + + default: + throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); + } + + return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); + } + + + /********* + ** Private methods + *********/ + /// Get the name of a save directory for the current player. + private static string GetSaveFolderName() + { + string prefix = new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray()); + return $"{prefix}_{Game1.uniqueIDForThisGame}"; + } + + /// Get the game's current version string. + private static string GetGameVersion() + { + // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time + FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); + if (field == null) + throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); + return (string)field.GetValue(null); + } + } +} diff --git a/src/SMAPI/ContentSource.cs b/src/SMAPI/ContentSource.cs new file mode 100644 index 00000000..35c8bc21 --- /dev/null +++ b/src/SMAPI/ContentSource.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Specifies a source containing content that can be loaded. + public enum ContentSource + { + /// Assets in the game's content manager (i.e. XNBs in the game's content folder). + GameContent, + + /// XNB files in the current mod's folder. + ModFolder + } +} diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs new file mode 100644 index 00000000..119e14c8 --- /dev/null +++ b/src/SMAPI/Context.cs @@ -0,0 +1,37 @@ +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI +{ + /// Provides information about the current game state. + public static class Context + { + /********* + ** Accessors + *********/ + /**** + ** Public + ****/ + /// Whether the player has loaded a save and the world has finished initialising. + public static bool IsWorldReady { get; internal set; } + + /// Whether is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc). + public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && !Game1.eventUp; + + /// Whether is true and the player is free to move (e.g. not using a tool). + public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; + + /// Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use to draw to the screen. + public static bool IsInDrawLoop { get; internal set; } + + /**** + ** Internal + ****/ + /// Whether a player save has been loaded. + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); + + /// Whether the game is currently writing to the save file. + internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something + } +} diff --git a/src/SMAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs new file mode 100644 index 00000000..4b207f08 --- /dev/null +++ b/src/SMAPI/Events/ChangeType.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Events +{ + /// Indicates how an inventory item changed. + public enum ChangeType + { + /// The entire stack was removed. + Removed, + + /// The entire stack was added. + Added, + + /// The stack size changed. + StackChange + } +} \ No newline at end of file diff --git a/src/SMAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs new file mode 100644 index 00000000..4b4e2ad0 --- /dev/null +++ b/src/SMAPI/Events/ContentEvents.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the game loads content. + public static class ContentEvents + { + + /********* + ** Events + *********/ + /// Raised after the content language changes. + public static event EventHandler> AfterLocaleChanged; + + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates monitoring and logging. + /// The previous locale. + /// The current locale. + internal static void InvokeAfterLocaleChanged(IMonitor monitor, string oldLocale, string newLocale) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterLocaleChanged)}", ContentEvents.AfterLocaleChanged?.GetInvocationList(), null, new EventArgsValueChanged(oldLocale, newLocale)); + } + } +} diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs new file mode 100644 index 00000000..80d0f547 --- /dev/null +++ b/src/SMAPI/Events/ControlEvents.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player uses a controller, keyboard, or mouse. + public static class ControlEvents + { + /********* + ** Events + *********/ + /// Raised when the changes. That happens when the player presses or releases a key. + public static event EventHandler KeyboardChanged; + + /// Raised when the player presses a keyboard key. + public static event EventHandler KeyPressed; + + /// Raised when the player releases a keyboard key. + public static event EventHandler KeyReleased; + + /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. + public static event EventHandler MouseChanged; + + /// The player pressed a controller button. This event isn't raised for trigger buttons. + public static event EventHandler ControllerButtonPressed; + + /// The player released a controller button. This event isn't raised for trigger buttons. + public static event EventHandler ControllerButtonReleased; + + /// The player pressed a controller trigger button. + public static event EventHandler ControllerTriggerPressed; + + /// The player released a controller trigger button. + public static event EventHandler ControllerTriggerReleased; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The previous keyboard state. + /// The current keyboard state. + internal static void InvokeKeyboardChanged(IMonitor monitor, KeyboardState priorState, KeyboardState newState) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyboardChanged)}", ControlEvents.KeyboardChanged?.GetInvocationList(), null, new EventArgsKeyboardStateChanged(priorState, newState)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The previous mouse state. + /// The current mouse state. + /// The previous mouse position on the screen adjusted for the zoom level. + /// The current mouse position on the screen adjusted for the zoom level. + internal static void InvokeMouseChanged(IMonitor monitor, MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.MouseChanged)}", ControlEvents.MouseChanged?.GetInvocationList(), null, new EventArgsMouseStateChanged(priorState, newState, priorPosition, newPosition)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The keyboard button that was pressed. + internal static void InvokeKeyPressed(IMonitor monitor, Keys key) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyPressed)}", ControlEvents.KeyPressed?.GetInvocationList(), null, new EventArgsKeyPressed(key)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The keyboard button that was released. + internal static void InvokeKeyReleased(IMonitor monitor, Keys key) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyReleased)}", ControlEvents.KeyReleased?.GetInvocationList(), null, new EventArgsKeyPressed(key)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The controller button that was pressed. + internal static void InvokeButtonPressed(IMonitor monitor, Buttons button) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(PlayerIndex.One, button)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The controller button that was released. + internal static void InvokeButtonReleased(IMonitor monitor, Buttons button) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(PlayerIndex.One, button)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The trigger button that was pressed. + /// The current trigger value. + internal static void InvokeTriggerPressed(IMonitor monitor, Buttons button, float value) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(PlayerIndex.One, button, value)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The trigger button that was pressed. + /// The current trigger value. + internal static void InvokeTriggerReleased(IMonitor monitor, Buttons button, float value) + { + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(PlayerIndex.One, button, value)); + } + } +} diff --git a/src/SMAPI/Events/EventArgsClickableMenuChanged.cs b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs new file mode 100644 index 00000000..2a2aa163 --- /dev/null +++ b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs @@ -0,0 +1,31 @@ +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsClickableMenuChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous menu. + public IClickableMenu NewMenu { get; } + + /// The current menu. + public IClickableMenu PriorMenu { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous menu. + /// The current menu. + public EventArgsClickableMenuChanged(IClickableMenu priorMenu, IClickableMenu newMenu) + { + this.NewMenu = newMenu; + this.PriorMenu = priorMenu; + } + } +} diff --git a/src/SMAPI/Events/EventArgsClickableMenuClosed.cs b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs new file mode 100644 index 00000000..5e6585f0 --- /dev/null +++ b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs @@ -0,0 +1,26 @@ +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsClickableMenuClosed : EventArgs + { + /********* + ** Accessors + *********/ + /// The menu that was closed. + public IClickableMenu PriorMenu { get; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The menu that was closed. + public EventArgsClickableMenuClosed(IClickableMenu priorMenu) + { + this.PriorMenu = priorMenu; + } + } +} diff --git a/src/SMAPI/Events/EventArgsControllerButtonPressed.cs b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs new file mode 100644 index 00000000..3243b80b --- /dev/null +++ b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerButtonPressed : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was pressed. + public Buttons ButtonPressed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the button. + /// The controller button that was pressed. + public EventArgsControllerButtonPressed(PlayerIndex playerIndex, Buttons button) + { + this.PlayerIndex = playerIndex; + this.ButtonPressed = button; + } + } +} diff --git a/src/SMAPI/Events/EventArgsControllerButtonReleased.cs b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs new file mode 100644 index 00000000..e05a080b --- /dev/null +++ b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerButtonReleased : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was pressed. + public Buttons ButtonReleased { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the button. + /// The controller button that was released. + public EventArgsControllerButtonReleased(PlayerIndex playerIndex, Buttons button) + { + this.PlayerIndex = playerIndex; + this.ButtonReleased = button; + } + } +} diff --git a/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs new file mode 100644 index 00000000..a2087733 --- /dev/null +++ b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerTriggerPressed : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was pressed. + public Buttons ButtonPressed { get; } + + /// The current trigger value. + public float Value { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the trigger button. + /// The trigger button that was pressed. + /// The current trigger value. + public EventArgsControllerTriggerPressed(PlayerIndex playerIndex, Buttons button, float value) + { + this.PlayerIndex = playerIndex; + this.ButtonPressed = button; + this.Value = value; + } + } +} diff --git a/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs new file mode 100644 index 00000000..d2eecbec --- /dev/null +++ b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerTriggerReleased : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was released. + public Buttons ButtonReleased { get; } + + /// The current trigger value. + public float Value { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the trigger button. + /// The trigger button that was released. + /// The current trigger value. + public EventArgsControllerTriggerReleased(PlayerIndex playerIndex, Buttons button, float value) + { + this.PlayerIndex = playerIndex; + this.ButtonReleased = button; + this.Value = value; + } + } +} diff --git a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs new file mode 100644 index 00000000..25d3ebf3 --- /dev/null +++ b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs @@ -0,0 +1,31 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsCurrentLocationChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's current location. + public GameLocation NewLocation { get; } + + /// The player's previous location. + public GameLocation PriorLocation { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's previous location. + /// The player's current location. + public EventArgsCurrentLocationChanged(GameLocation priorLocation, GameLocation newLocation) + { + this.NewLocation = newLocation; + this.PriorLocation = priorLocation; + } + } +} diff --git a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs new file mode 100644 index 00000000..fb8c821e --- /dev/null +++ b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsGameLocationsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The current list of game locations. + public List NewLocations { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current list of game locations. + public EventArgsGameLocationsChanged(List newLocations) + { + this.NewLocations = newLocations; + } + } +} diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs new file mode 100644 index 00000000..66cb19f2 --- /dev/null +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed or released. + public class EventArgsInput : EventArgs + { + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; set; } + + /// Whether the input is considered a 'click' by the game for enabling action. + public bool IsClick { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) + { + this.Button = button; + this.Cursor = cursor; + this.IsClick = isClick; + } + + /// Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event. + public void SuppressButton() + { + this.SuppressButton(this.Button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void SuppressButton(SButton button) + { + // keyboard + if (this.Button.TryGetKeyboard(out Keys key)) + Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Except(new[] { key }).ToArray()); + + // controller + else if (this.Button.TryGetController(out Buttons controllerButton)) + { + var newState = GamePad.GetState(PlayerIndex.One); + var thumbsticks = Game1.oldPadState.ThumbSticks; + var triggers = Game1.oldPadState.Triggers; + var buttons = Game1.oldPadState.Buttons; + var dpad = Game1.oldPadState.DPad; + + switch (controllerButton) + { + // d-pad + case Buttons.DPadDown: + dpad = new GamePadDPad(dpad.Up, newState.DPad.Down, dpad.Left, dpad.Right); + break; + case Buttons.DPadLeft: + dpad = new GamePadDPad(dpad.Up, dpad.Down, newState.DPad.Left, dpad.Right); + break; + case Buttons.DPadRight: + dpad = new GamePadDPad(dpad.Up, dpad.Down, dpad.Left, newState.DPad.Right); + break; + case Buttons.DPadUp: + dpad = new GamePadDPad(newState.DPad.Up, dpad.Down, dpad.Left, dpad.Right); + break; + + // trigger + case Buttons.LeftTrigger: + triggers = new GamePadTriggers(newState.Triggers.Left, triggers.Right); + break; + case Buttons.RightTrigger: + triggers = new GamePadTriggers(triggers.Left, newState.Triggers.Right); + break; + + // thumbstick + case Buttons.LeftThumbstickDown: + case Buttons.LeftThumbstickLeft: + case Buttons.LeftThumbstickRight: + case Buttons.LeftThumbstickUp: + thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Left, thumbsticks.Right); + break; + case Buttons.RightThumbstickDown: + case Buttons.RightThumbstickLeft: + case Buttons.RightThumbstickRight: + case Buttons.RightThumbstickUp: + thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Right, thumbsticks.Left); + break; + + // buttons + default: + var mask = + (buttons.A == ButtonState.Pressed ? Buttons.A : 0) + | (buttons.B == ButtonState.Pressed ? Buttons.B : 0) + | (buttons.Back == ButtonState.Pressed ? Buttons.Back : 0) + | (buttons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0) + | (buttons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0) + | (buttons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0) + | (buttons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0) + | (buttons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0) + | (buttons.Start == ButtonState.Pressed ? Buttons.Start : 0) + | (buttons.X == ButtonState.Pressed ? Buttons.X : 0) + | (buttons.Y == ButtonState.Pressed ? Buttons.Y : 0); + mask = mask ^ controllerButton; + buttons = new GamePadButtons(mask); + break; + } + + Game1.oldPadState = new GamePadState(thumbsticks, triggers, buttons, dpad); + } + } + } +} diff --git a/src/SMAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs new file mode 100644 index 00000000..0c742d12 --- /dev/null +++ b/src/SMAPI/Events/EventArgsIntChanged.cs @@ -0,0 +1,29 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an integer field that changed value. + public class EventArgsIntChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous value. + public int PriorInt { get; } + + /// The current value. + public int NewInt { get; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous value. + /// The current value. + public EventArgsIntChanged(int priorInt, int newInt) + { + this.PriorInt = priorInt; + this.NewInt = newInt; + } + } +} diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs new file mode 100644 index 00000000..1ee02842 --- /dev/null +++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsInventoryChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's inventory. + public List Inventory { get; } + + /// The added items. + public List Added { get; } + + /// The removed items. + public List Removed { get; } + + /// The items whose stack sizes changed. + public List QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's inventory. + /// The inventory changes. + public EventArgsInventoryChanged(List inventory, List changedItems) + { + this.Inventory = inventory; + this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); + this.Removed = changedItems.Where(n => n.ChangeType == ChangeType.Removed).ToList(); + this.QuantityChanged = changedItems.Where(n => n.ChangeType == ChangeType.StackChange).ToList(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsKeyPressed.cs b/src/SMAPI/Events/EventArgsKeyPressed.cs new file mode 100644 index 00000000..d9d81e10 --- /dev/null +++ b/src/SMAPI/Events/EventArgsKeyPressed.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsKeyPressed : EventArgs + { + /********* + ** Accessors + *********/ + /// The keyboard button that was pressed. + public Keys KeyPressed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The keyboard button that was pressed. + public EventArgsKeyPressed(Keys key) + { + this.KeyPressed = key; + } + } +} diff --git a/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs new file mode 100644 index 00000000..14e397ce --- /dev/null +++ b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsKeyboardStateChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous keyboard state. + public KeyboardState NewState { get; } + + /// The current keyboard state. + public KeyboardState PriorState { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous keyboard state. + /// The current keyboard state. + public EventArgsKeyboardStateChanged(KeyboardState priorState, KeyboardState newState) + { + this.PriorState = priorState; + this.NewState = newState; + } + } +} diff --git a/src/SMAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs new file mode 100644 index 00000000..fe6696d4 --- /dev/null +++ b/src/SMAPI/Events/EventArgsLevelUp.cs @@ -0,0 +1,52 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLevelUp : EventArgs + { + /********* + ** Accessors + *********/ + /// The player skill that leveled up. + public LevelType Type { get; } + + /// The new skill level. + public int NewLevel { get; } + + /// The player skill types. + public enum LevelType + { + /// The combat skill. + Combat, + + /// The farming skill. + Farming, + + /// The fishing skill. + Fishing, + + /// The foraging skill. + Foraging, + + /// The mining skill. + Mining, + + /// The luck skill. + Luck + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player skill that leveled up. + /// The new skill level. + public EventArgsLevelUp(LevelType type, int newLevel) + { + this.Type = type; + this.NewLevel = newLevel; + } + } +} diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs new file mode 100644 index 00000000..058999e9 --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationObjectsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The current list of objects in the current location. + public SerializableDictionary NewObjects { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current list of objects in the current location. + public EventArgsLocationObjectsChanged(SerializableDictionary newObjects) + { + this.NewObjects = newObjects; + } + } +} diff --git a/src/SMAPI/Events/EventArgsMineLevelChanged.cs b/src/SMAPI/Events/EventArgsMineLevelChanged.cs new file mode 100644 index 00000000..c82fed35 --- /dev/null +++ b/src/SMAPI/Events/EventArgsMineLevelChanged.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsMineLevelChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous mine level. + public int PreviousMineLevel { get; } + + /// The current mine level. + public int CurrentMineLevel { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous mine level. + /// The current mine level. + public EventArgsMineLevelChanged(int previousMineLevel, int currentMineLevel) + { + this.PreviousMineLevel = previousMineLevel; + this.CurrentMineLevel = currentMineLevel; + } + } +} diff --git a/src/SMAPI/Events/EventArgsMouseStateChanged.cs b/src/SMAPI/Events/EventArgsMouseStateChanged.cs new file mode 100644 index 00000000..57298164 --- /dev/null +++ b/src/SMAPI/Events/EventArgsMouseStateChanged.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsMouseStateChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous mouse state. + public MouseState PriorState { get; } + + /// The current mouse state. + public MouseState NewState { get; } + + /// The previous mouse position on the screen adjusted for the zoom level. + public Point PriorPosition { get; } + + /// The current mouse position on the screen adjusted for the zoom level. + public Point NewPosition { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous mouse state. + /// The current mouse state. + /// The previous mouse position on the screen adjusted for the zoom level. + /// The current mouse position on the screen adjusted for the zoom level. + public EventArgsMouseStateChanged(MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) + { + this.PriorState = priorState; + this.NewState = newState; + this.PriorPosition = priorPosition; + this.NewPosition = newPosition; + } + } +} diff --git a/src/SMAPI/Events/EventArgsValueChanged.cs b/src/SMAPI/Events/EventArgsValueChanged.cs new file mode 100644 index 00000000..1d25af49 --- /dev/null +++ b/src/SMAPI/Events/EventArgsValueChanged.cs @@ -0,0 +1,31 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a field that changed value. + /// The value type. + public class EventArgsValueChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous value. + public T PriorValue { get; } + + /// The current value. + public T NewValue { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous value. + /// The current value. + public EventArgsValueChanged(T priorValue, T newValue) + { + this.PriorValue = priorValue; + this.NewValue = newValue; + } + } +} \ No newline at end of file diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs new file mode 100644 index 00000000..b477376e --- /dev/null +++ b/src/SMAPI/Events/GameEvents.cs @@ -0,0 +1,96 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the game changes state. + public static class GameEvents + { + /********* + ** Events + *********/ + /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . + internal static event EventHandler InitializeInternal; + + /// Raised when the game updates its state (≈60 times per second). + public static event EventHandler UpdateTick; + + /// Raised every other tick (≈30 times per second). + public static event EventHandler SecondUpdateTick; + + /// Raised every fourth tick (≈15 times per second). + public static event EventHandler FourthUpdateTick; + + /// Raised every eighth tick (≈8 times per second). + public static event EventHandler EighthUpdateTick; + + /// Raised every 15th tick (≈4 times per second). + public static event EventHandler QuarterSecondTick; + + /// Raised every 30th tick (≈twice per second). + public static event EventHandler HalfSecondTick; + + /// Raised every 60th tick (≈once per second). + public static event EventHandler OneSecondTick; + + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeInitialize(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); + } + + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeSecondUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeFourthUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeEighthUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeQuarterSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeHalfSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeOneSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); + } + } +} diff --git a/src/SMAPI/Events/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs new file mode 100644 index 00000000..fff51bed --- /dev/null +++ b/src/SMAPI/Events/GraphicsEvents.cs @@ -0,0 +1,116 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised during the game's draw loop, when the game is rendering content to the window. + public static class GraphicsEvents + { + /********* + ** Events + *********/ + /**** + ** Generic events + ****/ + /// Raised after the game window is resized. + public static event EventHandler Resize; + + /**** + ** Main render events + ****/ + /// Raised before drawing the world to the screen. + public static event EventHandler OnPreRenderEvent; + + /// Raised after drawing the world to the screen. + public static event EventHandler OnPostRenderEvent; + + /**** + ** HUD events + ****/ + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public static event EventHandler OnPreRenderHudEvent; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public static event EventHandler OnPostRenderHudEvent; + + /**** + ** GUI events + ****/ + /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public static event EventHandler OnPreRenderGuiEvent; + + /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public static event EventHandler OnPostRenderGuiEvent; + + + /********* + ** Internal methods + *********/ + /**** + ** Generic events + ****/ + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeResize(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); + } + + /**** + ** Main render events + ****/ + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeOnPreRenderEvent(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderEvent)}", GraphicsEvents.OnPreRenderEvent?.GetInvocationList()); + } + + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeOnPostRenderEvent(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderEvent)}", GraphicsEvents.OnPostRenderEvent?.GetInvocationList()); + } + + /// Get whether there are any post-render event listeners. + internal static bool HasPostRenderListeners() + { + return GraphicsEvents.OnPostRenderEvent != null; + } + + /**** + ** GUI events + ****/ + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeOnPreRenderGuiEvent(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderGuiEvent)}", GraphicsEvents.OnPreRenderGuiEvent?.GetInvocationList()); + } + + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeOnPostRenderGuiEvent(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderGuiEvent)}", GraphicsEvents.OnPostRenderGuiEvent?.GetInvocationList()); + } + + /**** + ** HUD events + ****/ + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeOnPreRenderHudEvent(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderHudEvent)}", GraphicsEvents.OnPreRenderHudEvent?.GetInvocationList()); + } + + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeOnPostRenderHudEvent(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderHudEvent)}", GraphicsEvents.OnPostRenderHudEvent?.GetInvocationList()); + } + } +} diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs new file mode 100644 index 00000000..c31eb698 --- /dev/null +++ b/src/SMAPI/Events/InputEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player uses a controller, keyboard, or mouse button. + public static class InputEvents + { + /********* + ** Events + *********/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public static event EventHandler ButtonPressed; + + /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + public static event EventHandler ButtonReleased; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + { + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + { + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + } + } +} diff --git a/src/SMAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs new file mode 100644 index 00000000..f9ae6df6 --- /dev/null +++ b/src/SMAPI/Events/ItemStackChange.cs @@ -0,0 +1,20 @@ +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Represents an inventory slot that changed. + public class ItemStackChange + { + /********* + ** Accessors + *********/ + /// The item in the slot. + public Item Item { get; set; } + + /// The amount by which the item's stack size changed. + public int StackChange { get; set; } + + /// How the inventory slot changed. + public ChangeType ChangeType { get; set; } + } +} \ No newline at end of file diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs new file mode 100644 index 00000000..b834bc1c --- /dev/null +++ b/src/SMAPI/Events/LocationEvents.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player transitions between game locations, a location is added or removed, or the objects in the current location change. + public static class LocationEvents + { + /********* + ** Events + *********/ + /// Raised after the player warps to a new location. + public static event EventHandler CurrentLocationChanged; + + /// Raised after a game location is added or removed. + public static event EventHandler LocationsChanged; + + /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). + public static event EventHandler LocationObjectsChanged; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The player's previous location. + /// The player's current location. + internal static void InvokeCurrentLocationChanged(IMonitor monitor, GameLocation priorLocation, GameLocation newLocation) + { + monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.CurrentLocationChanged)}", LocationEvents.CurrentLocationChanged?.GetInvocationList(), null, new EventArgsCurrentLocationChanged(priorLocation, newLocation)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The current list of game locations. + internal static void InvokeLocationsChanged(IMonitor monitor, List newLocations) + { + monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationsChanged)}", LocationEvents.LocationsChanged?.GetInvocationList(), null, new EventArgsGameLocationsChanged(newLocations)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The current list of objects in the current location. + internal static void InvokeOnNewLocationObject(IMonitor monitor, SerializableDictionary newObjects) + { + monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationObjectsChanged)}", LocationEvents.LocationObjectsChanged?.GetInvocationList(), null, new EventArgsLocationObjectsChanged(newObjects)); + } + } +} diff --git a/src/SMAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs new file mode 100644 index 00000000..bd8d897e --- /dev/null +++ b/src/SMAPI/Events/MenuEvents.cs @@ -0,0 +1,40 @@ +using System; +using StardewModdingAPI.Framework; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Events raised when a game menu is opened or closed (including internal menus like the title screen). + public static class MenuEvents + { + /********* + ** Events + *********/ + /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. + public static event EventHandler MenuChanged; + + /// Raised after a game menu is closed. + public static event EventHandler MenuClosed; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The previous menu. + /// The current menu. + internal static void InvokeMenuChanged(IMonitor monitor, IClickableMenu priorMenu, IClickableMenu newMenu) + { + monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuChanged)}", MenuEvents.MenuChanged?.GetInvocationList(), null, new EventArgsClickableMenuChanged(priorMenu, newMenu)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The menu that was closed. + internal static void InvokeMenuClosed(IMonitor monitor, IClickableMenu priorMenu) + { + monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuClosed)}", MenuEvents.MenuClosed?.GetInvocationList(), null, new EventArgsClickableMenuClosed(priorMenu)); + } + } +} diff --git a/src/SMAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs new file mode 100644 index 00000000..9cf7edac --- /dev/null +++ b/src/SMAPI/Events/MineEvents.cs @@ -0,0 +1,28 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised when something happens in the mines. + public static class MineEvents + { + /********* + ** Events + *********/ + /// Raised after the player warps to a new level of the mine. + public static event EventHandler MineLevelChanged; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The previous mine level. + /// The current mine level. + internal static void InvokeMineLevelChanged(IMonitor monitor, int previousMineLevel, int currentMineLevel) + { + monitor.SafelyRaiseGenericEvent($"{nameof(MineEvents)}.{nameof(MineEvents.MineLevelChanged)}", MineEvents.MineLevelChanged?.GetInvocationList(), null, new EventArgsMineLevelChanged(previousMineLevel, currentMineLevel)); + } + } +} diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs new file mode 100644 index 00000000..5a9a9d5f --- /dev/null +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player data changes. + public static class PlayerEvents + { + /********* + ** Events + *********/ + /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). + public static event EventHandler InventoryChanged; + + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public static event EventHandler LeveledUp; + + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates monitoring and logging. + /// The player's inventory. + /// The inventory changes. + internal static void InvokeInventoryChanged(IMonitor monitor, List inventory, IEnumerable changedItems) + { + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); + } + + /// Rase a event. + /// Encapsulates monitoring and logging. + /// The player skill that leveled up. + /// The new skill level. + internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel) + { + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel)); + } + } +} diff --git a/src/SMAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs new file mode 100644 index 00000000..50e6d729 --- /dev/null +++ b/src/SMAPI/Events/SaveEvents.cs @@ -0,0 +1,56 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised before and after the player saves/loads the game. + public static class SaveEvents + { + /********* + ** Events + *********/ + /// Raised before the game begins writes data to the save file. + public static event EventHandler BeforeSave; + + /// Raised after the game finishes writing data to the save file. + public static event EventHandler AfterSave; + + /// Raised after the player loads a save slot. + public static event EventHandler AfterLoad; + + /// Raised after the game returns to the title screen. + public static event EventHandler AfterReturnToTitle; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeBeforeSave(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeSave)}", SaveEvents.BeforeSave?.GetInvocationList(), null, EventArgs.Empty); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeAfterSave(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterSave)}", SaveEvents.AfterSave?.GetInvocationList(), null, EventArgs.Empty); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeAfterLoad(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterLoad)}", SaveEvents.AfterLoad?.GetInvocationList(), null, EventArgs.Empty); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeAfterReturnToTitle(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterReturnToTitle)}", SaveEvents.AfterReturnToTitle?.GetInvocationList(), null, EventArgs.Empty); + } + } +} diff --git a/src/SMAPI/Events/TimeEvents.cs b/src/SMAPI/Events/TimeEvents.cs new file mode 100644 index 00000000..9aea5e04 --- /dev/null +++ b/src/SMAPI/Events/TimeEvents.cs @@ -0,0 +1,37 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the in-game date or time changes. + public static class TimeEvents + { + /********* + ** Events + *********/ + /// Raised after the game begins a new day, including when loading a save. + public static event EventHandler AfterDayStarted; + + /// Raised after the in-game clock changes. + public static event EventHandler TimeOfDayChanged; + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeAfterDayStarted(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The previous time in military time format (e.g. 6:00pm is 1800). + /// The current time in military time format (e.g. 6:10pm is 1810). + internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime) + { + monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); + } + } +} diff --git a/src/SMAPI/Framework/Command.cs b/src/SMAPI/Framework/Command.cs new file mode 100644 index 00000000..943e018d --- /dev/null +++ b/src/SMAPI/Framework/Command.cs @@ -0,0 +1,40 @@ +using System; + +namespace StardewModdingAPI.Framework +{ + /// A command that can be submitted through the SMAPI console to interact with SMAPI. + internal class Command + { + /********* + ** Accessor + *********/ + /// The friendly name for the mod that registered the command. + public string ModName { get; } + + /// The command name, which the user must type to trigger it. + public string Name { get; } + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Documentation { get; } + + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + public Action Callback { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The friendly name for the mod that registered the command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + public Command(string modName, string name, string documentation, Action callback) + { + this.ModName = modName; + this.Name = name; + this.Documentation = documentation; + this.Callback = callback; + } + } +} diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs new file mode 100644 index 00000000..79a23d03 --- /dev/null +++ b/src/SMAPI/Framework/CommandManager.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework +{ + /// Manages console commands. + internal class CommandManager + { + /********* + ** Properties + *********/ + /// The commands registered with SMAPI. + private readonly IDictionary Commands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Add a console command. + /// The friendly mod name for this instance. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// Whether to allow a null argument; this should only used for backwards compatibility. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public void Add(string modName, string name, string documentation, Action callback, bool allowNullCallback = false) + { + name = this.GetNormalisedName(name); + + // validate format + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name), "Can't register a command with no name."); + if (name.Any(char.IsWhiteSpace)) + throw new FormatException($"Can't register the '{name}' command because the name can't contain whitespace."); + if (callback == null && !allowNullCallback) + throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback."); + + // ensure uniqueness + if (this.Commands.ContainsKey(name)) + throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name."); + + // add command + this.Commands.Add(name, new Command(modName, name, documentation, callback)); + } + + /// Get a command by its unique name. + /// The command name. + /// Returns the matching command, or null if not found. + public Command Get(string name) + { + name = this.GetNormalisedName(name); + this.Commands.TryGetValue(name, out Command command); + return command; + } + + /// Get all registered commands. + public IEnumerable GetAll() + { + return this.Commands + .Values + .OrderBy(p => p.Name); + } + + /// Trigger a command. + /// The raw command input. + /// Returns whether a matching command was triggered. + public bool Trigger(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return false; + + string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + string name = args[0]; + args = args.Skip(1).ToArray(); + + return this.Trigger(name, args); + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + // get normalised name + name = this.GetNormalisedName(name); + if (name == null) + return false; + + // get command + if (this.Commands.TryGetValue(name, out Command command)) + { + command.Callback.Invoke(name, arguments); + return true; + } + return false; + } + + + /********* + ** Private methods + *********/ + /// Get a normalised command name. + /// The command name. + private string GetNormalisedName(string name) + { + name = name?.Trim().ToLower(); + return !string.IsNullOrWhiteSpace(name) + ? name + : null; + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs new file mode 100644 index 00000000..1ab9eebd --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetData.cs @@ -0,0 +1,44 @@ +using System; + +namespace StardewModdingAPI.Framework.Content +{ + /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. + /// The interface value type. + internal class AssetData : AssetInfo, IAssetData + { + /********* + ** Accessors + *********/ + /// The content data being read. + public TValue Data { get; protected set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetData(string locale, string assetName, TValue data, Func getNormalisedPath) + : base(locale, assetName, data.GetType(), getNormalisedPath) + { + this.Data = data; + } + + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + public void ReplaceWith(TValue value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); + if (!this.DataType.IsInstanceOfType(value)) + throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); + + this.Data = value; + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs new file mode 100644 index 00000000..e9b29b12 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForDictionary : AssetData>, IAssetDataForDictionary + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + public void Set(TKey key, TValue value) + { + this.Data[key] = value; + } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + public void Set(TKey key, Func value) + { + this.Data[key] = value(this.Data[key]); + } + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + public void Set(Func replacer) + { + foreach (var pair in this.Data.ToArray()) + this.Data[pair.Key] = replacer(pair.Key, pair.Value); + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs new file mode 100644 index 00000000..45c5588b --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForImage : AssetData, IAssetDataForImage + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + // get texture + Texture2D target = this.Data; + + // get areas + sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); + targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + + // validate + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); + if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // get source data + int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; + Color[] sourceData = new Color[pixelCount]; + source.GetData(0, sourceArea, sourceData, 0, pixelCount); + + // merge data in overlay mode + if (patchMode == PatchMode.Overlay) + { + Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; + target.GetData(0, targetArea, newData, 0, newData.Length); + for (int i = 0; i < sourceData.Length; i++) + { + Color pixel = sourceData[i]; + if (pixel.A != 0) // not transparent + newData[i] = pixel; + } + sourceData = newData; + } + + // patch target texture + target.SetData(0, targetArea, sourceData, 0, pixelCount); + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs new file mode 100644 index 00000000..f30003e4 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to content being read from a data file. + internal class AssetDataForObject : AssetData, IAssetData + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Construct an instance. + /// The asset metadata. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(IAssetInfo info, object data, Func getNormalisedPath) + : this(info.Locale, info.AssetName, data, getNormalisedPath) { } + + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary balue. + /// The content being read isn't a dictionary. + public IAssetDataForDictionary AsDictionary() + { + return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath); + } + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + public IAssetDataForImage AsImage() + { + return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath); + } + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + public TData GetData() + { + if (!(this.Data is TData)) + throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); + return (TData)this.Data; + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs new file mode 100644 index 00000000..d580dc06 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + internal class AssetInfo : IAssetInfo + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + public string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + public string AssetName { get; } + + /// The content data type. + public Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content type being read. + /// Normalises an asset key to match the cache key. + public AssetInfo(string locale, string assetName, Type type, Func getNormalisedPath) + { + this.Locale = locale; + this.AssetName = assetName; + this.DataType = type; + this.GetNormalisedPath = getNormalisedPath; + } + + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + public bool AssetNameEquals(string path) + { + path = this.GetNormalisedPath(path); + return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); + } + + + /********* + ** Protected methods + *********/ + /// Get a human-readable type name. + /// The type to name. + protected string GetFriendlyTypeName(Type type) + { + // dictionary + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type[] genericArgs = type.GetGenericArguments(); + return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; + } + + // texture + if (type == typeof(Texture2D)) + return type.Name; + + // native type + if (type == typeof(int)) + return "int"; + if (type == typeof(string)) + return "string"; + + // default + return type.FullName; + } + } +} diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs new file mode 100644 index 00000000..d46f23a3 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagerShim.cs @@ -0,0 +1,50 @@ +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// A minimal content manager which defers to SMAPI's main content manager. + internal class ContentManagerShim : LocalizedContentManager + { + /********* + ** Properties + *********/ + /// SMAPI's underlying content manager. + private readonly SContentManager ContentManager; + + + /********* + ** Accessors + *********/ + /// The content manager's name for logs (if any). + public string Name { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's underlying content manager. + /// The content manager's name for logs (if any). + public ContentManagerShim(SContentManager contentManager, string name) + : base(contentManager.ServiceProvider, contentManager.RootDirectory, contentManager.CurrentCulture, contentManager.LanguageCodeOverride) + { + this.ContentManager = contentManager; + this.Name = name; + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T Load(string assetName) + { + return this.ContentManager.LoadFor(assetName, this); + } + + /// Dispose held resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + this.ContentManager.DisposeFor(this); + } + } +} diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs new file mode 100644 index 00000000..db02b3d1 --- /dev/null +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Framework +{ + /// Defines a position on a given map at different reference points. + internal class CursorPosition : ICursorPosition + { + /********* + ** Accessors + *********/ + /// The pixel position relative to the top-left corner of the visible screen. + public Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + public Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + public Vector2 GrabTile { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The pixel position relative to the top-left corner of the visible screen. + /// The tile position relative to the top-left corner of the map. + /// The tile position that the game considers under the cursor for purposes of clicking actions. + public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + { + this.ScreenPixels = screenPixels; + this.Tile = tile; + this.GrabTile = grabTile; + } + } +} diff --git a/src/SMAPI/Framework/DeprecationLevel.cs b/src/SMAPI/Framework/DeprecationLevel.cs new file mode 100644 index 00000000..c0044053 --- /dev/null +++ b/src/SMAPI/Framework/DeprecationLevel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework +{ + /// Indicates how deprecated something is. + internal enum DeprecationLevel + { + /// It's deprecated but won't be removed soon. Mod authors have some time to update their mods. Deprecation warnings should be logged, but not written to the console. + Notice, + + /// Mods should no longer be using it. Deprecation messages should be debug entries in the console. + Info, + + /// The code will be removed soon. Deprecation messages should be warnings in the console. + PendingRemoval + } +} \ No newline at end of file diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs new file mode 100644 index 00000000..b07c6c7d --- /dev/null +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework +{ + /// Manages deprecation warnings. + internal class DeprecationManager + { + /********* + ** Properties + *********/ + /// The deprecations which have already been logged (as 'mod name::noun phrase::version'). + private readonly HashSet LoggedDeprecations = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging for a given module. + /// Tracks the installed mods. + public DeprecationManager(IMonitor monitor, ModRegistry modRegistry) + { + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Log a deprecation warning. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// How deprecated the code is. + public void Warn(string nounPhrase, string version, DeprecationLevel severity) + { + this.Warn(this.ModRegistry.GetModFromStack(), nounPhrase, version, severity); + } + + /// Log a deprecation warning. + /// The friendly mod name which used the deprecated code. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// How deprecated the code is. + public void Warn(string source, string nounPhrase, string version, DeprecationLevel severity) + { + // ignore if already warned + if (!this.MarkWarned(source ?? "", nounPhrase, version)) + return; + + // build message + string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; + if (source == null) + message += $"{Environment.NewLine}{Environment.StackTrace}"; + + // log message + switch (severity) + { + case DeprecationLevel.Notice: + this.Monitor.Log(message, LogLevel.Trace); + break; + + case DeprecationLevel.Info: + this.Monitor.Log(message, LogLevel.Debug); + break; + + case DeprecationLevel.PendingRemoval: + this.Monitor.Log(message, LogLevel.Warn); + break; + + default: + throw new NotSupportedException($"Unknown deprecation level '{severity}'"); + } + } + + /// Mark a deprecation warning as already logged. + /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). + /// The SMAPI version which deprecated it. + /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. + public bool MarkWarned(string nounPhrase, string version) + { + return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version); + } + + /// Mark a deprecation warning as already logged. + /// The friendly name of the assembly which used the deprecated code. + /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). + /// The SMAPI version which deprecated it. + /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. + public bool MarkWarned(string source, string nounPhrase, string version) + { + if (string.IsNullOrWhiteSpace(source)) + throw new InvalidOperationException("The deprecation source cannot be empty."); + + string key = $"{source}::{nounPhrase}::{version}"; + if (this.LoggedDeprecations.Contains(key)) + return false; + this.LoggedDeprecations.Add(key); + return true; + } + } +} diff --git a/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs new file mode 100644 index 00000000..ec9279f1 --- /dev/null +++ b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// An exception thrown when an assembly can't be loaded by SMAPI, with all the relevant details in the message. + internal class SAssemblyLoadFailedException : Exception + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + public SAssemblyLoadFailedException(string message) + : base(message) { } + } +} diff --git a/src/SMAPI/Framework/Exceptions/SContentLoadException.cs b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs new file mode 100644 index 00000000..85d85e3d --- /dev/null +++ b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// An implementation of used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework. + internal class SContentLoadException : ContentLoadException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SContentLoadException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/SMAPI/Framework/Exceptions/SParseException.cs b/src/SMAPI/Framework/Exceptions/SParseException.cs new file mode 100644 index 00000000..f7133ee7 --- /dev/null +++ b/src/SMAPI/Framework/Exceptions/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs new file mode 100644 index 00000000..48159f61 --- /dev/null +++ b/src/SMAPI/Framework/GameVersion.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework +{ + /// An implementation of that correctly handles the non-semantic versions used by older Stardew Valley releases. + internal class GameVersion : SemanticVersion + { + /********* + ** Private methods + *********/ + /// A mapping of game to semantic versions. + private static readonly IDictionary VersionMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["1.01"] = "1.0.1", + ["1.02"] = "1.0.2", + ["1.03"] = "1.0.3", + ["1.04"] = "1.0.4", + ["1.05"] = "1.0.5", + ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. + ["1.051b"] = "1.0.6-prelease2", + ["1.06"] = "1.0.6", + ["1.07"] = "1.0.7", + ["1.07a"] = "1.0.8-prerelease1", + ["1.11"] = "1.1.1" + }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game version string. + public GameVersion(string version) + : base(GameVersion.GetSemanticVersionString(version)) { } + + /// Get a string representation of the version. + public override string ToString() + { + return GameVersion.GetGameVersionString(base.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Convert a game version string to a semantic version string. + /// The game version string. + private static string GetSemanticVersionString(string gameVersion) + { + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; + } + + /// Convert a game version string to a semantic version string. + /// The game version string. + private static string GetGameVersionString(string gameVersion) + { + foreach (var mapping in GameVersion.VersionMap) + { + if (mapping.Value.Equals(gameVersion, StringComparison.InvariantCultureIgnoreCase)) + return mapping.Key; + } + return gameVersion; + } + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs new file mode 100644 index 00000000..c21734a7 --- /dev/null +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -0,0 +1,47 @@ +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModLoading; + +namespace StardewModdingAPI.Framework +{ + /// Metadata for a mod. + internal interface IModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + string DisplayName { get; } + + /// The mod's full directory path. + string DirectoryPath { get; } + + /// The mod manifest. + IManifest Manifest { get; } + + /// >Metadata about the mod from SMAPI's internal data (if any). + ModDataRecord DataRecord { get; } + + /// The metadata resolution status. + ModMetadataStatus Status { get; } + + /// The reason the metadata is invalid, if any. + string Error { get; } + + /// The mod instance (if it was loaded). + IMod Mod { get; } + + + /********* + ** Public methods + *********/ + /// Set the mod status. + /// The metadata resolution status. + /// The reason the metadata is invalid, if any. + /// Return the instance for chaining. + IModMetadata SetStatus(ModMetadataStatus status, string error = null); + + /// Set the mod instance. + /// The mod instance to set. + IModMetadata SetMod(IMod mod); + } +} diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs new file mode 100644 index 00000000..3709e05d --- /dev/null +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// Provides extension methods for SMAPI's internal use. + internal static class InternalExtensions + { + /**** + ** IMonitor + ****/ + /// Safely raise an event, and intercept any exceptions thrown by its handlers. + /// Encapsulates monitoring and logging. + /// The event name for error messages. + /// The event handlers. + /// The event sender. + /// The event arguments (or null to pass ). + public static void SafelyRaisePlainEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender = null, EventArgs args = null) + { + if (handlers == null) + return; + + foreach (EventHandler handler in handlers.Cast()) + { + // handle SMAPI exiting + if (monitor.IsExiting) + { + monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn); + return; + } + + // raise event + try + { + handler.Invoke(sender, args ?? EventArgs.Empty); + } + catch (Exception ex) + { + monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + } + + /// Safely raise an event, and intercept any exceptions thrown by its handlers. + /// The event argument object type. + /// Encapsulates monitoring and logging. + /// The event name for error messages. + /// The event handlers. + /// The event sender. + /// The event arguments. + public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) + { + if (handlers == null) + return; + + foreach (EventHandler handler in handlers.Cast>()) + { + try + { + handler.Invoke(sender, args); + } + catch (Exception ex) + { + monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + } + + /// Log a message for the player or developer the first time it occurs. + /// The monitor through which to log the message. + /// The hash of logged messages. + /// The message to log. + /// The log severity level. + public static void LogOnce(this IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + + /**** + ** Exceptions + ****/ + /// Get a string representation of an exception suitable for writing to the error log. + /// The error to summarise. + public static string GetLogSummary(this Exception exception) + { + switch (exception) + { + case TypeLoadException ex: + return $"Failed loading type '{ex.TypeName}': {exception}"; + + case ReflectionTypeLoadException ex: + string summary = exception.ToString(); + foreach (Exception childEx in ex.LoaderExceptions) + summary += $"\n\n{childEx.GetLogSummary()}"; + return summary; + + default: + return exception.ToString(); + } + } + + /**** + ** Sprite batch + ****/ + /// Get whether the sprite batch is between a begin and end pair. + /// The sprite batch to check. + /// The reflection helper with which to access private fields. + public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection) + { + // get field name + const string fieldName = +#if SMAPI_FOR_WINDOWS + "inBeginEndPair"; +#else + "_beginCalled"; +#endif + + // get result + return reflection.GetPrivateField(Game1.spriteBatch, fieldName).GetValue(); + } + } +} diff --git a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs new file mode 100644 index 00000000..b8f2c34e --- /dev/null +++ b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs @@ -0,0 +1,86 @@ +using System; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages console output interception. + internal class ConsoleInterceptionManager : IDisposable + { + /********* + ** Properties + *********/ + /// The intercepting console writer. + private readonly InterceptingTextWriter Output; + + + /********* + ** Accessors + *********/ + /// Whether the current console supports color formatting. + public bool SupportsColor { get; } + + /// The event raised when a message is written to the console directly. + public event Action OnMessageIntercepted; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ConsoleInterceptionManager() + { + // redirect output through interceptor + this.Output = new InterceptingTextWriter(Console.Out); + this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); + Console.SetOut(this.Output); + + // test color support + this.SupportsColor = this.TestColorSupport(); + } + + /// Get an exclusive lock and write to the console output without interception. + /// The action to perform within the exclusive write block. + public void ExclusiveWriteWithoutInterception(Action action) + { + lock (Console.Out) + { + try + { + this.Output.ShouldIntercept = false; + action(); + } + finally + { + this.Output.ShouldIntercept = true; + } + } + } + + /// Release all resources. + public void Dispose() + { + Console.SetOut(this.Output.Out); + this.Output.Dispose(); + } + + + /********* + ** private methods + *********/ + /// Test whether the current console supports color formatting. + private bool TestColorSupport() + { + try + { + this.ExclusiveWriteWithoutInterception(() => + { + Console.ForegroundColor = Console.ForegroundColor; + }); + return true; + } + catch (Exception) + { + return false; // Mono bug + } + } + } +} diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs new file mode 100644 index 00000000..9ca61b59 --- /dev/null +++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Text; + +namespace StardewModdingAPI.Framework.Logging +{ + /// A text writer which allows intercepting output. + internal class InterceptingTextWriter : TextWriter + { + /********* + ** Accessors + *********/ + /// The underlying console output. + public TextWriter Out { get; } + + /// The character encoding in which the output is written. + public override Encoding Encoding => this.Out.Encoding; + + /// Whether to intercept console output. + public bool ShouldIntercept { get; set; } + + /// The event raised when a message is written to the console directly. + public event Action OnMessageIntercepted; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying output writer. + public InterceptingTextWriter(TextWriter output) + { + this.Out = output; + } + + /// Writes a subarray of characters to the text string or stream. + /// The character array to write data from. + /// The character position in the buffer at which to start retrieving data. + /// The number of characters to write. + public override void Write(char[] buffer, int index, int count) + { + if (this.ShouldIntercept) + this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); + else + this.Out.Write(buffer, index, count); + } + + /// Writes a character to the text string or stream. + /// The character to write to the text stream. + /// Console log messages from the game should be caught by . This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone. + public override void Write(char ch) + { + this.Out.Write(ch); + } + + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + this.OnMessageIntercepted = null; + } + } +} diff --git a/src/SMAPI/Framework/Logging/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs new file mode 100644 index 00000000..8cfe0527 --- /dev/null +++ b/src/SMAPI/Framework/Logging/LogFileManager.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages reading and writing to log file. + internal class LogFileManager : IDisposable + { + /********* + ** Properties + *********/ + /// The underlying stream writer. + private readonly StreamWriter Stream; + + + /********* + ** Accessors + *********/ + /// The full path to the log file being written. + public string Path { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The log file to write. + public LogFileManager(string path) + { + this.Path = path; + + // create log directory if needed + string logDir = System.IO.Path.GetDirectoryName(path); + if (logDir == null) + throw new ArgumentException($"The log path '{path}' is not valid."); + Directory.CreateDirectory(logDir); + + // open log file stream + this.Stream = new StreamWriter(path, append: false) { AutoFlush = true }; + } + + /// Write a message to the log. + /// The message to log. + public void WriteLine(string message) + { + // always use Windows-style line endings for convenience + // (Linux/Mac editors are fine with them, Windows editors often require them) + this.Stream.Write(message + "\r\n"); + } + + /// Release all resources. + public void Dispose() + { + this.Stream.Dispose(); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs new file mode 100644 index 00000000..16032da1 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs @@ -0,0 +1,23 @@ +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// The common base class for mod helpers. + internal abstract class BaseHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod for which the helper was created. + public string ModID { get; } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + protected BaseHelper(string modID) + { + this.ModID = modID; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs new file mode 100644 index 00000000..bdedb07c --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs @@ -0,0 +1,54 @@ +using System; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for managing console commands. + internal class CommandHelper : BaseHelper, ICommandHelper + { + /********* + ** Accessors + *********/ + /// The friendly mod name for this instance. + private readonly string ModName; + + /// Manages console commands. + private readonly CommandManager CommandManager; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The friendly mod name for this instance. + /// Manages console commands. + public CommandHelper(string modID, string modName, CommandManager commandManager) + : base(modID) + { + this.ModName = modName; + this.CommandManager = commandManager; + } + + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public ICommandHelper Add(string name, string documentation, Action callback) + { + this.CommandManager.Add(this.ModName, name, documentation, callback); + return this; + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + return this.CommandManager.Trigger(name, arguments); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs new file mode 100644 index 00000000..4440ae40 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Exceptions; +using StardewValley; +using xTile; +using xTile.Format; +using xTile.Tiles; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for loading content assets. + internal class ContentHelper : BaseHelper, IContentHelper + { + /********* + ** Properties + *********/ + /// SMAPI's underlying content manager. + private readonly SContentManager ContentManager; + + /// The absolute path to the mod folder. + private readonly string ModFolderPath; + + /// The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName"). + private readonly string ModFolderPathFromContent; + + /// The friendly mod name for use in errors. + private readonly string ModName; + + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + + + /********* + ** Accessors + *********/ + /// The game's current locale code (like pt-BR). + public string CurrentLocale => this.ContentManager.GetLocale(); + + /// The game's current locale as an enum value. + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + + /// The observable implementation of . + internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); + + /// The observable implementation of . + internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); + + /// Interceptors which provide the initial versions of matching content assets. + public IList AssetLoaders => this.ObservableAssetLoaders; + + /// Interceptors which edit matching content assets after they're loaded. + public IList AssetEditors => this.ObservableAssetEditors; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's underlying content manager. + /// The absolute path to the mod folder. + /// The unique ID of the relevant mod. + /// The friendly mod name for use in errors. + /// Encapsulates monitoring and logging. + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + : base(modID) + { + this.ContentManager = contentManager; + this.ModFolderPath = modFolderPath; + this.ModName = modName; + this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.Monitor = monitor; + } + + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T Load(string key, ContentSource source = ContentSource.ModFolder) + { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); + + this.AssertValidAssetKeyFormat(key); + try + { + switch (source) + { + case ContentSource.GameContent: + return this.ContentManager.Load(key); + + case ContentSource.ModFolder: + // get file + FileInfo file = this.GetModFile(key); + if (!file.Exists) + throw GetContentError($"there's no matching file at path '{file.FullName}'."); + + // get asset path + string assetPath = this.GetModAssetPath(key, file.FullName); + + // try cache + if (this.ContentManager.IsLoaded(assetPath)) + return this.ContentManager.Load(assetPath); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + { + T asset = this.ContentManager.Load(assetPath); + if (asset is Map) + this.FixLocalMapTilesheets(asset as Map, key); + return asset; + } + + // unpacked map + case ".tbin": + { + // validate + if (typeof(T) != typeof(Map)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + this.FixLocalMapTilesheets(map, key); + + // inject map + this.ContentManager.Inject(assetPath, map); + return (T)(object)map; + } + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.ContentManager.Inject(assetPath, texture); + return (T)(object)texture; + } + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + + default: + throw GetContentError($"unknown content source '{source}'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); + } + } + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) + { + switch (source) + { + case ContentSource.GameContent: + return this.ContentManager.NormaliseAssetName(key); + + case ContentSource.ModFolder: + FileInfo file = this.GetModFile(key); + return this.ContentManager.NormaliseAssetName(this.GetModAssetPath(key, file.FullName)); + + default: + throw new NotSupportedException($"Unknown content source '{source}'."); + } + } + + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to invalidate in the content folder. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + public bool InvalidateCache(string key) + { + this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); + return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + public bool InvalidateCache() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); + return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + } + + /********* + ** Private methods + *********/ + /// Fix the tilesheets for a map loaded from the mod folder. + /// The map whose tilesheets to fix. + /// The map asset key within the mod folder. + /// The map tilesheets could not be loaded. + /// + /// The game's logic for tilesheets in is a bit specialised. It boils down to this: + /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the Content folder. + /// * Else it's loaded from Content\Maps with a seasonal prefix. + /// + /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. + /// Instead we use a more heuristic approach: check relative to the map file first, then relative to + /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, we try + /// for a seasonal variation and then an exact match. + /// + /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. + /// + private void FixLocalMapTilesheets(Map map, string mapKey) + { + // check map info + if (!map.TileSheets.Any()) + return; + mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder + + // fix tilesheets + foreach (TileSheet tilesheet in map.TileSheets) + { + string imageSource = tilesheet.ImageSource; + + // get seasonal name (if applicable) + string seasonalImageSource = null; + if (Game1.currentSeason != null) + { + string filename = Path.GetFileName(imageSource); + bool hasSeasonalPrefix = + filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase); + if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_")) + { + string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase)); + seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}"; + } + } + + // load best match + try + { + string key = + this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource) + ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource); + if (key != null) + { + tilesheet.ImageSource = key; + continue; + } + } + catch (Exception ex) + { + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); + } + + // none found + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder."); + } + } + + /// Load a tilesheet image source if the file exists. + /// The folder path containing the map, relative to the mod folder. + /// The tilesheet image source to load. + /// Returns the loaded asset key (if it was loaded successfully). + /// See remarks on . + private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource) + { + if (imageSource == null) + return null; + + // check relative to map file + { + string localKey = Path.Combine(relativeMapFolder, imageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + { + try + { + this.Load(localKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex); + } + + return this.GetActualAssetKey(localKey); + } + } + + // check relative to content folder + { + foreach (string candidateKey in new[] { imageSource, $@"Maps\{imageSource}" }) + { + string contentKey = candidateKey.EndsWith(".png") + ? candidateKey.Substring(0, imageSource.Length - 4) + : candidateKey; + + try + { + this.Load(contentKey, ContentSource.GameContent); + return contentKey; + } + catch + { + // ignore file-not-found errors + // TODO: while it's useful to suppress a asset-not-found error here to avoid + // confusion, this is a pretty naive approach. Even if the file doesn't exist, + // the file may have been loaded through an IAssetLoader which failed. So even + // if the content file doesn't exist, that doesn't mean the error here is a + // content-not-found error. Unfortunately XNA doesn't provide a good way to + // detect the error type. + if (this.GetContentFolderFile(contentKey).Exists) + throw; + } + } + } + + // not found + return null; + } + + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")] + private void AssertValidAssetKeyFormat(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + } + + /// Get a file from the mod folder. + /// The asset path relative to the mod folder. + private FileInfo GetModFile(string path) + { + // try exact match + path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); + FileInfo file = new FileInfo(path); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(path + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Get a file from the game's content folder. + /// The asset key. + private FileInfo GetContentFolderFile(string key) + { + // get file path + string path = Path.Combine(this.ContentManager.FullRootDirectory, key); + if (!path.EndsWith(".xnb")) + path += ".xnb"; + + // get file + return new FileInfo(path); + } + + /// Get the asset path which loads a mod folder through a content manager. + /// The file path relative to the mod's folder. + /// The absolute file path. + private string GetModAssetPath(string localPath, string absolutePath) + { +#if SMAPI_FOR_WINDOWS + // XNA doesn't allow absolute asset paths, so get a path relative to the content folder + return Path.Combine(this.ModFolderPathFromContent, localPath); +#else + // MonoGame is weird about relative paths on Mac, but allows absolute paths + return absolutePath; +#endif + } + + /// Get a directory path relative to a given root. + /// The root path from which the path should be relative. + /// The target file path. + private string GetRelativePath(string rootPath, string targetPath) + { + // convert to URIs + Uri from = new Uri(rootPath + "/"); + Uri to = new Uri(targetPath + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); + + // get relative path + return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) + .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs new file mode 100644 index 00000000..665b9cf4 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides simplified APIs for writing mods. + internal class ModHelper : BaseHelper, IModHelper, IDisposable + { + /********* + ** Properties + *********/ + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + + /********* + ** Accessors + *********/ + /// The full path to the mod's folder. + public string DirectoryPath { get; } + + /// An API for loading content assets. + public IContentHelper Content { get; } + + /// An API for accessing private game code. + public IReflectionHelper Reflection { get; } + + /// an API for fetching metadata about loaded mods. + public IModRegistry ModRegistry { get; } + + /// An API for managing console commands. + public ICommandHelper ConsoleCommands { get; } + + /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public ITranslationHelper Translation { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID. + /// The full path to the mod's folder. + /// Encapsulate SMAPI's JSON parsing. + /// An API for loading content assets. + /// An API for managing console commands. + /// an API for fetching metadata about loaded mods. + /// An API for accessing private game code. + /// An API for reading translations stored in the mod's i18n folder. + /// An argument is null or empty. + /// The path does not exist on disk. + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) + : base(modID) + { + // validate directory + if (string.IsNullOrWhiteSpace(modDirectory)) + throw new ArgumentNullException(nameof(modDirectory)); + if (!Directory.Exists(modDirectory)) + throw new InvalidOperationException("The specified mod directory does not exist."); + + // initialise + this.DirectoryPath = modDirectory; + this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); + this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); + this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); + this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); + this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); + } + + /**** + ** Mod config file + ****/ + /// Read the mod's configuration file (and create it if needed). + /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. + public TConfig ReadConfig() + where TConfig : class, new() + { + TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); + this.WriteConfig(config); // create file or fill in missing fields + return config; + } + + /// Save to the mod's configuration file. + /// The config class type. + /// The config settings to save. + public void WriteConfig(TConfig config) + where TConfig : class, new() + { + this.WriteJsonFile("config.json", config); + } + + /**** + ** Generic JSON files + ****/ + /// Read a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + public TModel ReadJsonFile(string path) + where TModel : class + { + path = Path.Combine(this.DirectoryPath, path); + return this.JsonHelper.ReadJsonFile(path); + } + + /// Save to a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// The model to save. + public void WriteJsonFile(string path, TModel model) + where TModel : class + { + path = Path.Combine(this.DirectoryPath, path); + this.JsonHelper.WriteJsonFile(path, model); + } + + + /**** + ** Disposal + ****/ + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // nothing to dispose yet + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs new file mode 100644 index 00000000..9e824694 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides metadata about installed mods. + internal class ModRegistryHelper : BaseHelper, IModRegistry + { + /********* + ** Properties + *********/ + /// The underlying mod registry. + private readonly ModRegistry Registry; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The underlying mod registry. + public ModRegistryHelper(string modID, ModRegistry registry) + : base(modID) + { + this.Registry = registry; + } + + /// Get metadata for all loaded mods. + public IEnumerable GetAll() + { + return this.Registry.GetAll(); + } + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + public IManifest Get(string uniqueID) + { + return this.Registry.Get(uniqueID); + } + + /// Get whether a mod has been loaded. + /// The mod's unique ID. + public bool IsLoaded(string uniqueID) + { + return this.Registry.IsLoaded(uniqueID); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs new file mode 100644 index 00000000..8d435416 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -0,0 +1,200 @@ +using System; +using StardewModdingAPI.Framework.Reflection; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class ReflectionHelper : BaseHelper, IReflectionHelper + { + /********* + ** Properties + *********/ + /// The underlying reflection helper. + private readonly Reflector Reflector; + + /// The mod name for error messages. + private readonly string ModName; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The mod name for error messages. + /// The underlying reflection helper. + public ReflectionHelper(string modID, string modName, Reflector reflector) + : base(modID) + { + this.ModName = modName; + this.Reflector = reflector; + } + + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + this.AssertAccessAllowed(obj); + return this.Reflector.GetPrivateField(obj, name, required); + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + this.AssertAccessAllowed(type); + return this.Reflector.GetPrivateField(type, name, required); + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + this.AssertAccessAllowed(obj); + return this.Reflector.GetPrivateProperty(obj, name, required); + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + this.AssertAccessAllowed(type); + return this.Reflector.GetPrivateProperty(type, name, required); + } + + /**** + ** Field values + ** (shorthand since this is the most common case) + ****/ + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(object obj, string name, bool required = true) + { + this.AssertAccessAllowed(obj); + IPrivateField field = this.GetPrivateField(obj, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(Type type, string name, bool required = true) + { + this.AssertAccessAllowed(type); + IPrivateField field = this.GetPrivateField(type, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + this.AssertAccessAllowed(obj); + return this.Reflector.GetPrivateMethod(obj, name, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + this.AssertAccessAllowed(type); + return this.Reflector.GetPrivateMethod(type, name, required); + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + this.AssertAccessAllowed(obj); + return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + this.AssertAccessAllowed(type); + return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + } + + + /********* + ** Private methods + *********/ + /// Assert that mods can use the reflection helper to access the given type. + /// The type being accessed. + private void AssertAccessAllowed(Type type) + { + // validate type namespace + if (type.Namespace != null) + { + string rootSmapiNamespace = typeof(Program).Namespace; + if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); + } + } + + /// Assert that mods can use the reflection helper to access the given type. + /// The object being accessed. + private void AssertAccessAllowed(object obj) + { + if (obj != null) + this.AssertAccessAllowed(obj.GetType()); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs new file mode 100644 index 00000000..bbe3a81a --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + internal class TranslationHelper : BaseHelper, ITranslationHelper + { + /********* + ** Properties + *********/ + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The translations for each locale. + private readonly IDictionary> All = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + /// The translations for the current locale, with locale fallback taken into account. + private IDictionary ForLocale; + + + /********* + ** Accessors + *********/ + /// The current locale. + public string Locale { get; private set; } + + /// The game's current language code. + public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The name of the relevant mod for error messages. + /// The initial locale. + /// The game's current language code. + public TranslationHelper(string modID, string modName, string locale, LocalizedContentManager.LanguageCode languageCode) + : base(modID) + { + // save data + this.ModName = modName; + + // set locale + this.SetLocale(locale, languageCode); + } + + /// Get all translations for the current locale. + public IEnumerable GetTranslations() + { + return this.ForLocale.Values.ToArray(); + } + + /// Get a translation for the current locale. + /// The translation key. + public Translation Get(string key) + { + this.ForLocale.TryGetValue(key, out Translation translation); + return translation ?? new Translation(this.ModName, this.Locale, key, null); + } + + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + public Translation Get(string key, object tokens) + { + return this.Get(key).Tokens(tokens); + } + + /// Set the translations to use. + /// The translations to use. + internal TranslationHelper SetTranslations(IDictionary> translations) + { + // reset translations + this.All.Clear(); + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // rebuild cache + this.SetLocale(this.Locale, this.LocaleEnum); + + return this; + } + + /// Set the current locale and precache translations. + /// The current locale. + /// The game's current language code. + internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) + { + this.Locale = locale.ToLower().Trim(); + this.LocaleEnum = localeEnum; + + this.ForLocale = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (string next in this.GetRelevantLocales(this.Locale)) + { + // skip if locale not defined + if (!this.All.TryGetValue(next, out IDictionary translations)) + continue; + + // add missing translations + foreach (var pair in translations) + { + if (!this.ForLocale.ContainsKey(pair.Key)) + this.ForLocale.Add(pair.Key, new Translation(this.ModName, this.Locale, pair.Key, pair.Value)); + } + } + } + + + /********* + ** Private methods + *********/ + /// Get the locales which can provide translations for the given locale, in precedence order. + /// The locale for which to find valid locales. + private IEnumerable GetRelevantLocales(string locale) + { + // given locale + yield return locale; + + // broader locales (like pt-BR => pt) + while (true) + { + int dashIndex = locale.LastIndexOf('-'); + if (dashIndex <= 0) + break; + + locale = locale.Substring(0, dashIndex); + yield return locale; + } + + // default + if (locale != "default") + yield return "default"; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs new file mode 100644 index 00000000..4378798c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// A minimal assembly definition resolver which resolves references to known assemblies. + internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + { + /********* + ** Properties + *********/ + /// The known assemblies. + private readonly IDictionary Loaded = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Add known assemblies to the resolver. + /// The known assemblies. + public void Add(params AssemblyDefinition[] assemblies) + { + foreach (AssemblyDefinition assembly in assemblies) + { + this.Loaded[assembly.Name.Name] = assembly; + this.Loaded[assembly.Name.FullName] = assembly; + } + } + + /// Resolve an assembly reference. + /// The assembly name. + public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); + + /// Resolve an assembly reference. + /// The assembly name. + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); + + /// Resolve an assembly reference. + /// The assembly full name (including version, etc). + public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); + + /// Resolve an assembly reference. + /// The assembly full name (including version, etc). + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); + + + /********* + ** Private methods + *********/ + /// Resolve a known assembly definition based on its short or full name. + /// The assembly's short or full name. + private AssemblyDefinition ResolveName(string name) + { + return this.Loaded.ContainsKey(name) + ? this.Loaded[name] + : null; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs new file mode 100644 index 00000000..11be19fc --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates the result of an assembly load. + internal enum AssemblyLoadStatus + { + /// The assembly was loaded successfully. + Okay = 1, + + /// The assembly could not be loaded. + Failed = 2, + + /// The assembly is already loaded. + AlreadyLoaded = 3 + } +} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs new file mode 100644 index 00000000..1e3c4a05 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Metadata; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Preprocesses and loads mod assemblies. + internal class AssemblyLoader + { + /********* + ** Properties + *********/ + /// Metadata for mapping assemblies to the current platform. + private readonly PlatformAssemblyMap AssemblyMap; + + /// A type => assembly lookup for types which should be rewritten. + private readonly IDictionary TypeAssemblies; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Whether to enable developer mode logging. + private readonly bool IsDeveloperMode; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current game platform. + /// Encapsulates monitoring and logging. + /// Whether to enable developer mode logging. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) + { + this.Monitor = monitor; + this.IsDeveloperMode = isDeveloperMode; + this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); + + // generate type => assembly lookup for types which should be rewritten + this.TypeAssemblies = new Dictionary(); + foreach (Assembly assembly in this.AssemblyMap.Targets) + { + ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; + foreach (TypeDefinition type in module.GetTypes()) + { + if (!type.IsPublic) + continue; // no need to rewrite + if (type.Namespace.Contains("<")) + continue; // ignore assembly metadata + this.TypeAssemblies[type.FullName] = assembly; + } + } + } + + /// Preprocess and load an assembly. + /// The mod for which the assembly is being loaded. + /// The assembly file path. + /// Assume the mod is compatible, even if incompatible code is detected. + /// Returns the rewrite metadata for the preprocessed assembly. + /// An incompatible CIL instruction was found while rewriting the assembly. + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) + { + // get referenced local assemblies + AssemblyParseResult[] assemblies; + { + AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); + HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded + assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); + } + + // validate load + if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) + { + throw new SAssemblyLoadFailedException(!File.Exists(assemblyPath) + ? $"Could not load '{assemblyPath}' because it doesn't exist." + : $"Could not load '{assemblyPath}'." + ); + } + if (assemblies.Last().Status == AssemblyLoadStatus.AlreadyLoaded) // mod assembly is last in dependency order + throw new SAssemblyLoadFailedException($"Could not load '{assemblyPath}' because it was already loaded. Do you have two copies of this mod?"); + + // rewrite & load assemblies in leaf-to-root order + bool oneAssembly = assemblies.Length == 1; + Assembly lastAssembly = null; + HashSet loggedMessages = new HashSet(); + foreach (AssemblyParseResult assembly in assemblies) + { + if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) + continue; + + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + if (changed) + { + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + { + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + } + } + + // last assembly loaded is the root + return lastAssembly; + } + + /// Resolve an assembly by its name. + /// The assembly name. + /// + /// This implementation returns the first loaded assembly which matches the short form of + /// the assembly name, to resolve assembly resolution issues when rewriting + /// assemblies (especially with Mono). Since this is meant to be called on , + /// the implicit assumption is that loading the exact assembly failed. + /// + public Assembly ResolveAssembly(string name) + { + string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) + return AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(p => p.GetName().Name == shortName); + } + + + /********* + ** Private methods + *********/ + /**** + ** Assembly parsing + ****/ + /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. + /// The assembly file to load. + /// The assembly names that should be skipped. + /// A resolver which resolves references to known assemblies. + /// Returns the rewrite metadata for the preprocessed assembly. + private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) + { + // validate + if (file.Directory == null) + throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); + if (!file.Exists) + yield break; // not a local assembly + + // read assembly + byte[] assemblyBytes = File.ReadAllBytes(file.FullName); + AssemblyDefinition assembly; + using (Stream readStream = new MemoryStream(assemblyBytes)) + assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); + + // skip if already visited + if (visitedAssemblyNames.Contains(assembly.Name.Name)) + yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); + visitedAssemblyNames.Add(assembly.Name.Name); + + // yield referenced assemblies + foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) + { + FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); + foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) + yield return result; + } + + // yield assembly + yield return new AssemblyParseResult(file, assembly, AssemblyLoadStatus.Okay); + } + + /**** + ** Assembly rewriting + ****/ + /// Rewrite the types referenced by an assembly. + /// The mod for which the assembly is being loaded. + /// The assembly to rewrite. + /// Assume the mod is compatible, even if incompatible code is detected. + /// The messages that have already been logged for this mod. + /// A string to prefix to log messages. + /// Returns whether the assembly was modified. + /// An incompatible CIL instruction was found while rewriting the assembly. + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + { + ModuleDefinition module = assembly.MainModule; + string filename = $"{assembly.Name.Name}.dll"; + + // swap assembly references if needed (e.g. XNA => MonoGame) + bool platformChanged = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) + { + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); + } + + // find (and optionally rewrite) incompatible instructions + bool anyRewritten = false; + IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); + foreach (MethodDefinition method in this.GetMethods(module)) + { + // check method definition + foreach (IInstructionHandler handler in handlers) + { + InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; + } + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + foreach (Instruction instruction in cil.Body.Instructions.ToArray()) + { + foreach (IInstructionHandler handler in handlers) + { + InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; + } + } + } + + return platformChanged || anyRewritten; + } + + /// Process the result from an instruction handler. + /// The mod being analysed. + /// The instruction handler. + /// The result returned by the handler. + /// The messages already logged for the current mod. + /// Assume the mod is compatible, even if incompatible code is detected. + /// A string to prefix to log messages. + /// The assembly filename for log messages. + private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) + { + switch (result) + { + case InstructionHandleResult.Rewritten: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); + break; + + case InstructionHandleResult.NotCompatible: + if (!assumeCompatible) + throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedGamePatch: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedSaveSerialiser: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedDynamic: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", +#if SMAPI_FOR_WINDOWS + this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug +#else + LogLevel.Warn +#endif + ); + break; + + case InstructionHandleResult.None: + break; + + default: + throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); + } + } + + /// Get the correct reference to use for compatibility with the current platform. + /// The type reference to rewrite. + private void ChangeTypeScope(TypeReference type) + { + // check skip conditions + if (type == null || type.FullName.StartsWith("System.")) + return; + + // get assembly + if (!this.TypeAssemblies.TryGetValue(type.FullName, out Assembly assembly)) + return; + + // replace scope + AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; + type.Scope = assemblyRef; + } + + /// Get all methods in a module. + /// The module to search. + private IEnumerable GetMethods(ModuleDefinition module) + { + return ( + from type in module.GetTypes() + where type.HasMethods + from method in type.Methods + where method.HasBody + select method + ); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs new file mode 100644 index 00000000..b56a776c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -0,0 +1,36 @@ +using System.IO; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata about a parsed assembly definition. + internal class AssemblyParseResult + { + /********* + ** Accessors + *********/ + /// The original assembly file. + public readonly FileInfo File; + + /// The assembly definition. + public readonly AssemblyDefinition Definition; + + /// The result of the assembly load. + public AssemblyLoadStatus Status; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The original assembly file. + /// The assembly definition. + /// The result of the assembly load. + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status) + { + this.File = file; + this.Definition = assembly; + this.Status = status; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs new file mode 100644 index 00000000..e4beb7a9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given event. + internal class EventFinder : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The event name for which to find references. + private readonly string EventName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The event name for which to find references. + /// The result to return for matching instructions. + public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.EventName = eventName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{eventName} event"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && (methodRef.Name == "add_" + this.EventName || methodRef.Name == "remove_" + this.EventName); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs new file mode 100644 index 00000000..00805815 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given field. + internal class FieldFinder : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The field name for which to find references. + private readonly string FieldName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The field name for which to find references. + /// The result to return for matching instructions. + public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.FieldName = fieldName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{fieldName} field"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + return + fieldRef != null + && fieldRef.DeclaringType.FullName == this.FullTypeName + && fieldRef.Name == this.FieldName; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs new file mode 100644 index 00000000..5358f181 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given method. + internal class MethodFinder : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The method name for which to find references. + private readonly string MethodName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The method name for which to find references. + /// The result to return for matching instructions. + public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.MethodName = methodName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{methodName} method"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && methodRef.Name == this.MethodName; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs new file mode 100644 index 00000000..e54c86cf --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given property. + internal class PropertyFinder : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The property name for which to find references. + private readonly string PropertyName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The property name for which to find references. + /// The result to return for matching instructions. + public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.PropertyName = propertyName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{propertyName} property"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && (methodRef.Name == "get_" + this.PropertyName || methodRef.Name == "set_" + this.PropertyName); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs new file mode 100644 index 00000000..45349def --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given type. + internal class TypeFinder : IInstructionHandler + { + /********* + ** Accessors + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to match. + /// The result to return for matching instructions. + public TypeFinder(string fullTypeName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.Result = result; + this.NounPhrase = $"{fullTypeName} type"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(method) + ? this.Result + : InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The method deifnition. + protected bool IsMatch(MethodDefinition method) + { + if (this.IsMatch(method.ReturnType)) + return true; + + foreach (VariableDefinition variable in method.Body.Variables) + { + if (this.IsMatch(variable.VariableType)) + return true; + } + + return false; + } + + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + return + this.IsMatch(fieldRef.DeclaringType) // field on target class + || this.IsMatch(fieldRef.FieldType); // field value is target class + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null) + { + return + this.IsMatch(methodRef.DeclaringType) // method on target class + || this.IsMatch(methodRef.ReturnType) // method returns target class + || methodRef.Parameters.Any(p => this.IsMatch(p.ParameterType)); // method parameters + } + + return false; + } + + /// Get whether a type reference matches the expected type. + /// The type to check. + protected bool IsMatch(TypeReference type) + { + // root type + if (type.FullName == this.FullTypeName) + return true; + + // generic arguments + if (type is GenericInstanceType genericType) + { + if (genericType.GenericArguments.Any(this.IsMatch)) + return true; + } + + // generic parameters (e.g. constraints) + if (type.GenericParameters.Any(this.IsMatch)) + return true; + + return false; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs new file mode 100644 index 00000000..8830cc74 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -0,0 +1,34 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Performs predefined logic for detected CIL instructions. + internal interface IInstructionHandler + { + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the handler matches. + string NounPhrase { get; } + + + /********* + ** Methods + *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + } +} diff --git a/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs new file mode 100644 index 00000000..17ec24b1 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs @@ -0,0 +1,35 @@ +using System; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// An exception raised when an incompatible instruction is found while loading a mod assembly. + internal class IncompatibleInstructionException : Exception + { + /********* + ** Accessors + *********/ + /// A brief noun phrase which describes the incompatible instruction that was found. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + public IncompatibleInstructionException(string nounPhrase) + : base($"Found an incompatible CIL instruction ({nounPhrase}).") + { + this.NounPhrase = nounPhrase; + } + + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + /// A message which describes the error. + public IncompatibleInstructionException(string nounPhrase, string message) + : base(message) + { + this.NounPhrase = nounPhrase; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs new file mode 100644 index 00000000..0ae598fc --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates how an instruction was handled. + internal enum InstructionHandleResult + { + /// No special handling is needed. + None, + + /// The instruction was successfully rewritten for compatibility. + Rewritten, + + /// The instruction is not compatible and can't be rewritten for compatibility. + NotCompatible, + + /// The instruction is compatible, but patches the game in a way that may impact stability. + DetectedGamePatch, + + /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. + DetectedSaveSerialiser, + + /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. + DetectedDynamic + } +} diff --git a/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs new file mode 100644 index 00000000..075e237a --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright. + internal class InvalidModStateException : Exception + { + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public InvalidModStateException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs new file mode 100644 index 00000000..0774b487 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// The status of a given mod in the dependency-sorting algorithm. + internal enum ModDependencyStatus + { + /// The mod hasn't been visited yet. + Queued, + + /// The mod is currently being analysed as part of a dependency chain. + Checking, + + /// The mod has already been sorted. + Sorted, + + /// The mod couldn't be sorted due to a metadata issue (e.g. missing dependencies). + Failed + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs new file mode 100644 index 00000000..5055da75 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -0,0 +1,68 @@ +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for a mod. + internal class ModMetadata : IModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string DisplayName { get; } + + /// The mod's full directory path. + public string DirectoryPath { get; } + + /// The mod manifest. + public IManifest Manifest { get; } + + /// Metadata about the mod from SMAPI's internal data (if any). + public ModDataRecord DataRecord { get; } + + /// The metadata resolution status. + public ModMetadataStatus Status { get; private set; } + + /// The reason the metadata is invalid, if any. + public string Error { get; private set; } + + /// The mod instance (if it was loaded). + public IMod Mod { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's display name. + /// The mod's full directory path. + /// The mod manifest. + /// Metadata about the mod from SMAPI's internal data (if any). + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) + { + this.DisplayName = displayName; + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.DataRecord = dataRecord; + } + + /// Set the mod status. + /// The metadata resolution status. + /// The reason the metadata is invalid, if any. + /// Return the instance for chaining. + public IModMetadata SetStatus(ModMetadataStatus status, string error = null) + { + this.Status = status; + this.Error = error; + return this; + } + + /// Set the mod instance. + /// The mod instance to set. + public IModMetadata SetMod(IMod mod) + { + this.Mod = mod; + return this; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs new file mode 100644 index 00000000..ab65f7b4 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates the status of a mod's metadata resolution. + internal enum ModMetadataStatus + { + /// The mod has been found, but hasn't been processed yet. + Found, + + /// The mod cannot be loaded. + Failed + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs new file mode 100644 index 00000000..d0ef1b08 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Finds and processes mod metadata. + internal class ModResolver + { + /********* + ** Public methods + *********/ + /// Get manifest metadata for each folder in the given root path. + /// The root path to search for mods. + /// The JSON helper with which to read manifests. + /// Metadata about mods from SMAPI's internal data. + /// Returns the manifests by relative folder. + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable dataRecords) + { + dataRecords = dataRecords.ToArray(); + + foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) + { + // read file + Manifest manifest = null; + string path = Path.Combine(modDir.FullName, "manifest.json"); + string error = null; + try + { + // read manifest + manifest = jsonHelper.ReadJsonFile(path); + + // validate + if (manifest == null) + { + error = File.Exists(path) + ? "its manifest is invalid." + : "it doesn't have a manifest."; + } + else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) + error = "its manifest doesn't set an entry DLL."; + } + catch (SParseException ex) + { + error = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; + } + + // get internal data record (if any) + ModDataRecord dataRecord = null; + if (manifest != null) + { + string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); + } + + // add default update keys + if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) + manifest.UpdateKeys = dataRecord.UpdateKeys; + + // build metadata + string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) + ? manifest.Name + : modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + ModMetadataStatus status = error == null + ? ModMetadataStatus.Found + : ModMetadataStatus.Failed; + + yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); + } + } + + /// Validate manifest metadata. + /// The mod manifests to validate. + /// The current SMAPI version. + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, IDictionary vendorModUrls) + { + mods = mods.ToArray(); + + // validate each manifest + foreach (IModMetadata mod in mods) + { + // skip if already failed + if (mod.Status == ModMetadataStatus.Failed) + continue; + + // validate compatibility + ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); + switch (compatibility?.Status) + { + case ModStatus.Obsolete: + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + continue; + + case ModStatus.AssumeBroken: + { + // get reason + string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + + // get update URLs + List updateUrls = new List(); + foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) + { + string[] parts = key.Split(new[] { ':' }, 2); + if (parts.Length != 2) + continue; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + updateUrls.Add(string.Format(urlTemplate, modID)); + } + if (mod.DataRecord.AlternativeUrl != null) + updateUrls.Add(mod.DataRecord.AlternativeUrl); + + // build error + string error = $"{reasonPhrase}. Please check for a "; + if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + error += "newer version"; + else + error += $"version newer than {compatibility.UpperVersion}"; + error += " at " + string.Join(" or ", updateUrls); + + mod.SetStatus(ModMetadataStatus.Failed, error); + } + continue; + } + + // validate SMAPI version + if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) + { + mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + continue; + } + + // validate DLL path + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); + if (!File.Exists(assemblyPath)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + + // validate required fields + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + } + } + + // validate IDs are unique + { + var duplicatesByID = mods + .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) + .Where(p => p.Count() > 1); + foreach (var group in duplicatesByID) + { + foreach (IModMetadata mod in group) + { + if (mod.Status == ModMetadataStatus.Failed) + continue; // don't replace metadata error + mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); + } + } + } + } + + /// Sort the given mods by the order they should be loaded. + /// The mods to process. + public IEnumerable ProcessDependencies(IEnumerable mods) + { + // initialise metadata + mods = mods.ToArray(); + var sortedMods = new Stack(); + var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); + + // handle failed mods + foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed)) + { + states[mod] = ModDependencyStatus.Failed; + sortedMods.Push(mod); + } + + // sort mods + foreach (IModMetadata mod in mods) + this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); + + return sortedMods.Reverse(); + } + + + /********* + ** Private methods + *********/ + /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. + /// The full list of mods being validated. + /// The mod whose dependencies to process. + /// The dependency state for each mod. + /// The list in which to save mods sorted by dependency order. + /// The current change of mod dependencies. + /// Returns the mod dependency status. + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) + { + // check if already visited + switch (states[mod]) + { + // already sorted or failed + case ModDependencyStatus.Sorted: + case ModDependencyStatus.Failed: + return states[mod]; + + // dependency loop + case ModDependencyStatus.Checking: + // This should never happen. The higher-level mod checks if the dependency is + // already being checked, so it can fail without visiting a mod twice. If this + // case is hit, that logic didn't catch the dependency loop for some reason. + throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName}))."); + + // not visited yet, start processing + case ModDependencyStatus.Queued: + break; + + // sanity check + default: + throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); + } + + // no dependencies, mark sorted + if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) + { + sortedMods.Push(mod); + return states[mod] = ModDependencyStatus.Sorted; + } + + // get dependencies + var dependencies = + ( + from entry in mod.Manifest.Dependencies + let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + orderby entry.UniqueID + select new + { + ID = entry.UniqueID, + MinVersion = entry.MinimumVersion, + Mod = dependencyMod, + IsRequired = entry.IsRequired + } + ) + .ToArray(); + + // missing required dependencies, mark failed + { + string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); + if (failedIDs.Any()) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + return states[mod] = ModDependencyStatus.Failed; + } + } + + // dependency min version not met, mark failed + { + string[] failedLabels = + ( + from entry in dependencies + where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) + select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" + ) + .ToArray(); + if (failedLabels.Any()) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); + return states[mod] = ModDependencyStatus.Failed; + } + } + + // process dependencies + { + states[mod] = ModDependencyStatus.Checking; + + // recursively sort dependencies + foreach (var dependency in dependencies) + { + IModMetadata requiredMod = dependency.Mod; + var subchain = new List(currentChain) { mod }; + + // ignore missing optional dependency + if (!dependency.IsRequired && requiredMod == null) + continue; + + // detect dependency loop + if (states[requiredMod] == ModDependencyStatus.Checking) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); + return states[mod] = ModDependencyStatus.Failed; + } + + // recursively process each dependency + var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); + switch (substatus) + { + // sorted successfully + case ModDependencyStatus.Sorted: + break; + + // failed, which means this mod can't be loaded either + case ModDependencyStatus.Failed: + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); + return states[mod] = ModDependencyStatus.Failed; + + // unexpected status + case ModDependencyStatus.Queued: + case ModDependencyStatus.Checking: + throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status."); + + // sanity check + default: + throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); + } + } + + // all requirements sorted successfully + sortedMods.Push(mod); + return states[mod] = ModDependencyStatus.Sorted; + } + } + + /// Get all mod folders in a root folder, passing through empty folders as needed. + /// The root folder path to search. + private IEnumerable GetModFolders(string rootPath) + { + foreach (string modRootPath in Directory.GetDirectories(rootPath)) + { + DirectoryInfo directory = new DirectoryInfo(modRootPath); + + // if a folder only contains another folder, check the inner folder instead + while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) + directory = directory.GetDirectories().First(); + + yield return directory; + } + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Platform.cs b/src/SMAPI/Framework/ModLoading/Platform.cs new file mode 100644 index 00000000..45e881c4 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Platform.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// The game's platform version. + internal enum Platform + { + /// The Linux/Mac version of the game. + Mono, + + /// The Windows version of the game. + Windows + } +} diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs new file mode 100644 index 00000000..463f45e8 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for mapping assemblies to the current . + internal class PlatformAssemblyMap + { + /********* + ** Accessors + *********/ + /**** + ** Data + ****/ + /// The target game platform. + public readonly Platform TargetPlatform; + + /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). + public readonly string[] RemoveNames; + + /**** + ** Metadata + ****/ + /// The assemblies to target. Equivalent types should be rewritten to use these assemblies. + public readonly Assembly[] Targets; + + /// An assembly => reference cache. + public readonly IDictionary TargetReferences; + + /// An assembly => module cache. + public readonly IDictionary TargetModules; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The target game platform. + /// The assembly short names to remove (like Stardew Valley). + /// The assemblies to target. + public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) + { + // save data + this.TargetPlatform = targetPlatform; + this.RemoveNames = removeAssemblyNames; + + // cache assembly metadata + this.Targets = targetAssemblies; + this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs new file mode 100644 index 00000000..56a60a72 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Provides helper methods for field rewriters. + internal static class RewriteHelper + { + /********* + ** Public methods + *********/ + /// Get the field reference from an instruction if it matches. + /// The IL instruction. + public static FieldReference AsFieldReference(Instruction instruction) + { + return instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld + ? (FieldReference)instruction.Operand + : null; + } + + /// Get the method reference from an instruction if it matches. + /// The IL instruction. + public static MethodReference AsMethodReference(Instruction instruction) + { + return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt + ? (MethodReference)instruction.Operand + : null; + } + + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + public static bool IsSameType(Type type, TypeReference reference) + { + // same namespace & name + if (type.Namespace != reference.Namespace || type.Name != reference.Name) + return false; + + // same generic parameters + if (type.IsGenericType) + { + if (!reference.IsGenericInstance) + return false; + + Type[] defGenerics = type.GetGenericArguments(); + TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); + if (defGenerics.Length != refGenerics.Length) + return false; + for (int i = 0; i < defGenerics.Length; i++) + { + if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) + return false; + } + } + + return true; + } + + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) + { + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterInfo[] definitionParameters = definition.GetParameters(); + ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); + if (referenceParameters.Length != definitionParameters.Length) + return false; + for (int i = 0; i < referenceParameters.Length; i++) + { + if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) + return false; + } + return true; + } + + /// Get whether a type has a method whose signature matches the one expected by a method reference. + /// The type to check. + /// The method reference. + public static bool HasMatchingSignature(Type type, MethodReference reference) + { + return type + .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) + .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs new file mode 100644 index 00000000..63358b39 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites references to one field with another. + internal class FieldReplaceRewriter : FieldFinder + { + /********* + ** Properties + *********/ + /// The new field to reference. + private readonly FieldInfo ToField; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + /// The new field name to reference. + public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) + : base(type.FullName, fromFieldName, InstructionHandleResult.None) + { + this.ToField = type.GetField(toFieldName); + if (this.ToField == null) + throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + FieldReference newRef = module.Import(this.ToField); + cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); + return InstructionHandleResult.Rewritten; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs new file mode 100644 index 00000000..a20b8bee --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -0,0 +1,51 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites field references into property references. + internal class FieldToPropertyRewriter : FieldFinder + { + /********* + ** Properties + *********/ + /// The type whose field to which references should be rewritten. + private readonly Type Type; + + /// The field name to rewrite. + private readonly string FieldName; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + public FieldToPropertyRewriter(Type type, string fieldName) + : base(type.FullName, fieldName, InstructionHandleResult.None) + { + this.Type = type; + this.FieldName = fieldName; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; + MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.FieldName}")); + cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); + return InstructionHandleResult.Rewritten; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs new file mode 100644 index 00000000..974fcf4c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -0,0 +1,88 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites method references from one parent type to another if the signatures match. + internal class MethodParentRewriter : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The type whose methods to remap. + private readonly Type FromType; + + /// The type with methods to map to. + private readonly Type ToType; + + /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. + private readonly bool OnlyIfPlatformChanged; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose methods to remap. + /// The type with methods to map to. + /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. + public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) + { + this.FromType = fromType; + this.ToType = toType; + this.NounPhrase = $"{fromType.Name} methods"; + this.OnlyIfPlatformChanged = onlyIfPlatformChanged; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return InstructionHandleResult.None; + + MethodReference methodRef = (MethodReference)instruction.Operand; + methodRef.DeclaringType = module.Import(this.ToType); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + /// Whether the mod was compiled on a different platform. + protected bool IsMatch(Instruction instruction, bool platformChanged) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && (platformChanged || !this.OnlyIfPlatformChanged) + && methodRef.DeclaringType.FullName == this.FromType.FullName + && RewriteHelper.HasMatchingSignature(this.ToType, methodRef); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs new file mode 100644 index 00000000..74f2fcdd --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -0,0 +1,154 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites all references to a type. + internal class TypeReferenceRewriter : TypeFinder + { + /********* + ** Properties + *********/ + /// The full type name to which to find references. + private readonly string FromTypeName; + + /// The new type to reference. + private readonly Type ToType; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to which to find references. + /// The new type to reference. + public TypeReferenceRewriter(string fromTypeFullName, Type toType) + : base(fromTypeFullName, InstructionHandleResult.None) + { + this.FromTypeName = fromTypeFullName; + this.ToType = toType; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + bool rewritten = false; + + // return type + if (this.IsMatch(method.ReturnType)) + { + method.ReturnType = this.RewriteIfNeeded(module, method.ReturnType); + rewritten = true; + } + + // parameters + foreach (ParameterDefinition parameter in method.Parameters) + { + if (this.IsMatch(parameter.ParameterType)) + { + parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType); + rewritten = true; + } + } + + // generic parameters + for (int i = 0; i < method.GenericParameters.Count; i++) + { + var parameter = method.GenericParameters[i]; + if (this.IsMatch(parameter)) + { + TypeReference newType = this.RewriteIfNeeded(module, parameter); + if (newType != parameter) + method.GenericParameters[i] = new GenericParameter(parameter.Name, newType); + rewritten = true; + } + } + + // local variables + foreach (VariableDefinition variable in method.Body.Variables) + { + if (this.IsMatch(variable.VariableType)) + { + variable.VariableType = this.RewriteIfNeeded(module, variable.VariableType); + rewritten = true; + } + } + + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) + return InstructionHandleResult.None; + + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + fieldRef.DeclaringType = this.RewriteIfNeeded(module, fieldRef.DeclaringType); + fieldRef.FieldType = this.RewriteIfNeeded(module, fieldRef.FieldType); + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null) + { + methodRef.DeclaringType = this.RewriteIfNeeded(module, methodRef.DeclaringType); + methodRef.ReturnType = this.RewriteIfNeeded(module, methodRef.ReturnType); + foreach (var parameter in methodRef.Parameters) + parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType); + } + + // type reference + if (instruction.Operand is TypeReference typeRef) + { + TypeReference newRef = this.RewriteIfNeeded(module, typeRef); + if (typeRef != newRef) + cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); + } + + return InstructionHandleResult.Rewritten; + } + + /********* + ** Private methods + *********/ + /// Get the adjusted type reference if it matches, else the same value. + /// The assembly module containing the instruction. + /// The type to replace if it matches. + private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type) + { + // root type + if (type.FullName == this.FromTypeName) + return module.Import(this.ToType); + + // generic arguments + if (type is GenericInstanceType genericType) + { + for (int i = 0; i < genericType.GenericArguments.Count; i++) + genericType.GenericArguments[i] = this.RewriteIfNeeded(module, genericType.GenericArguments[i]); + } + + // generic parameters (e.g. constraints) + for (int i = 0; i < type.GenericParameters.Count; i++) + type.GenericParameters[i] = new GenericParameter(this.RewriteIfNeeded(module, type.GenericParameters[i])); + + return type; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs new file mode 100644 index 00000000..322a7df1 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs @@ -0,0 +1,90 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites virtual calls to the method. + internal class VirtualEntryCallRemover : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The type containing the method. + private readonly Type ToType; + + /// The name of the method. + private readonly string MethodName; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public VirtualEntryCallRemover() + { + this.ToType = typeof(Mod); + this.MethodName = nameof(Mod.Entry); + this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + // get instructions comprising method call + int index = cil.Body.Instructions.IndexOf(instruction); + Instruction loadArg0 = cil.Body.Instructions[index - 2]; + Instruction loadArg1 = cil.Body.Instructions[index - 1]; + if (loadArg0.OpCode != OpCodes.Ldarg_0) + throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); + if (loadArg1.OpCode != OpCodes.Ldarg_1) + throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); + + // remove method call + cil.Remove(loadArg0); + cil.Remove(loadArg1); + cil.Remove(instruction); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.ToType.FullName + && methodRef.Name == this.MethodName; + } + } +} diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs new file mode 100644 index 00000000..9dde7a20 --- /dev/null +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace StardewModdingAPI.Framework +{ + /// Tracks the installed mods. + internal class ModRegistry + { + /********* + ** Properties + *********/ + /// The registered mod data. + private readonly List Mods = new List(); + + /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). + private readonly IDictionary ModNamesByAssembly = new Dictionary(); + + + /********* + ** Public methods + *********/ + /**** + ** Basic metadata + ****/ + /// Get metadata for all loaded mods. + public IEnumerable GetAll() + { + return this.Mods.Select(p => p.Manifest); + } + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + public IManifest Get(string uniqueID) + { + // normalise search ID + if (string.IsNullOrWhiteSpace(uniqueID)) + return null; + uniqueID = uniqueID.Trim(); + + // find match + return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Get whether a mod has been loaded. + /// The mod's unique ID. + public bool IsLoaded(string uniqueID) + { + return this.Get(uniqueID) != null; + } + + /**** + ** Mod data + ****/ + /// Register a mod as a possible source of deprecation warnings. + /// The mod metadata. + public void Add(IModMetadata metadata) + { + this.Mods.Add(metadata); + this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName; + } + + /// Get all enabled mods. + public IEnumerable GetMods() + { + return (from mod in this.Mods select mod); + } + + /// Get the friendly mod name which defines a type. + /// The type to check. + /// Returns the mod name, or null if the type isn't part of a known mod. + public string GetModFrom(Type type) + { + // null + if (type == null) + return null; + + // known type + string assemblyName = type.Assembly.FullName; + if (this.ModNamesByAssembly.ContainsKey(assemblyName)) + return this.ModNamesByAssembly[assemblyName]; + + // not found + return null; + } + + /// Get the friendly name for the closest assembly registered as a source of deprecation warnings. + /// Returns the source name, or null if no registered assemblies were found. + public string GetModFromStack() + { + // get stack frames + StackTrace stack = new StackTrace(); + StackFrame[] frames = stack.GetFrames(); + if (frames == null) + return null; + + // search stack for a source assembly + foreach (StackFrame frame in frames) + { + MethodBase method = frame.GetMethod(); + string name = this.GetModFrom(method.ReflectedType); + if (name != null) + return name; + } + + // no known assembly found + return null; + } + } +} diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs new file mode 100644 index 00000000..b85787e5 --- /dev/null +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.Models +{ + /// A manifest which describes a mod for SMAPI. + internal class Manifest : IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + [JsonConverter(typeof(SFieldConverter))] + public ISemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + [JsonConverter(typeof(SFieldConverter))] + public ISemanticVersion MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the method. + public string EntryDll { get; set; } + + /// The other mods that must be loaded before this mod. + [JsonConverter(typeof(SFieldConverter))] + public IManifestDependency[] Dependencies { get; set; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + } +} diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs new file mode 100644 index 00000000..5646b335 --- /dev/null +++ b/src/SMAPI/Framework/Models/ManifestDependency.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// A mod dependency listed in a mod manifest. + internal class ManifestDependency : IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique mod ID to require. + /// The minimum required version (if any). + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) + { + this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; + this.IsRequired = required; + } + } +} diff --git a/src/SMAPI/Framework/Models/ModCompatibility.cs b/src/SMAPI/Framework/Models/ModCompatibility.cs new file mode 100644 index 00000000..54737e6c --- /dev/null +++ b/src/SMAPI/Framework/Models/ModCompatibility.cs @@ -0,0 +1,55 @@ +using System; + +namespace StardewModdingAPI.Framework.Models +{ + /// Specifies the compatibility of a given mod version range. + internal class ModCompatibility + { + /********* + ** Accessors + *********/ + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + /// The mod compatibility. + public ModStatus Status { get; } + + /// The reason phrase to show in log output, or null to use the default value. + /// For example, "this version is incompatible with the latest version of the game". + public string ReasonPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range. + /// The mod compatibility. + /// The reason phrase to show in log output, or null to use the default value. + public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) + { + // extract version strings + string[] versions = versionRange.Split('~'); + if (versions.Length != 2) + throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); + + // initialise + this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; + this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; + this.Status = status; + this.ReasonPhrase = reasonPhrase; + } + + /// Get whether a given version is contained within this compatibility range. + /// The version to check. + public bool MatchesVersion(ISemanticVersion version) + { + return + (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); + } + } +} diff --git a/src/SMAPI/Framework/Models/ModDataID.cs b/src/SMAPI/Framework/Models/ModDataID.cs new file mode 100644 index 00000000..d19434fa --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataID.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.Models +{ + /// Uniquely identifies a mod in SMAPI's internal data. + /// + /// This represents a custom format which uniquely identifies a mod across all versions, even + /// if its field values change or it doesn't specify a unique ID. This is mapped to a string + /// with the following format: + /// + /// 1. If the mod's identifier changed over time, multiple variants can be separated by the | + /// character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. + /// + internal class ModDataID + { + /********* + ** Properties + *********/ + /// The unique sets of field values which identify this mod. + private readonly FieldSnapshot[] Snapshots; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModDataID() { } + + /// Construct an instance. + /// The mod identifier string (see remarks on ). + public ModDataID(string data) + { + this.Snapshots = + ( + from string part in data.Split('|') + let str = part.Trim() + select str.StartsWith("{") + ? JsonConvert.DeserializeObject(str) + : new FieldSnapshot { ID = str } + ) + .ToArray(); + } + + /// Get whether this ID matches a given mod manifest. + /// The mod's unique ID, or a substitute ID if it isn't set in the manifest. + /// The manifest to check. + public bool Matches(string id, IManifest manifest) + { + return this.Snapshots.Any(snapshot => + snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) + && ( + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) + ); + } + + + /********* + ** Private models + *********/ + /// A unique set of fields which identifies the mod. + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The mod name, or null to ignore the mod name. + public string Name { get; set; } + + /// The author name, or null to ignore the author. + public string Author { get; set; } + } + } +} diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs new file mode 100644 index 00000000..c6a12188 --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataRecord.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata about a mod from SMAPI's internal data. + internal class ModDataRecord + { + /********* + ** Accessors + *********/ + /// The unique mod identifier. + [JsonConverter(typeof(SFieldConverter))] + public ModDataID ID { get; set; } + + /// A value to inject into field if it's not already set. + public string[] UpdateKeys { get; set; } + + /// The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible. + public string AlternativeUrl { get; set; } + + /// The compatibility of given mod versions (if any). + [JsonConverter(typeof(SFieldConverter))] + public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; + + /// Map local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Map remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get the compatibility record for a given version, if any. + /// The mod version to check. + public ModCompatibility GetCompatibility(ISemanticVersion version) + { + return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); + } + + /// Get a semantic local version for update checks. + /// The local version to normalise. + public string GetLocalVersionForUpdateChecks(string version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + } +} diff --git a/src/SMAPI/Framework/Models/ModStatus.cs b/src/SMAPI/Framework/Models/ModStatus.cs new file mode 100644 index 00000000..343ccb7e --- /dev/null +++ b/src/SMAPI/Framework/Models/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Indicates how SMAPI should treat a mod. + internal enum ModStatus + { + /// Don't override the status. + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, + + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible + } +} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs new file mode 100644 index 00000000..401e1a3a --- /dev/null +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// The SMAPI configuration settings. + internal class SConfig + { + /******** + ** Accessors + ********/ + /// Whether to enable development features. + public bool DeveloperMode { get; set; } + + /// Whether to check for newer versions of SMAPI and mods on startup. + public bool CheckForUpdates { get; set; } + + /// SMAPI's GitHub project name, used to perform update checks. + public string GitHubProjectName { get; set; } + + /// The base URL for SMAPI's web API, used to perform update checks. + public string WebApiBaseUrl { get; set; } + + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } + + /// Extra metadata about mods. + public ModDataRecord[] ModData { get; set; } + } +} diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs new file mode 100644 index 00000000..bf338386 --- /dev/null +++ b/src/SMAPI/Framework/Monitor.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using StardewModdingAPI.Framework.Logging; + +namespace StardewModdingAPI.Framework +{ + /// Encapsulates monitoring and logic for a given module. + internal class Monitor : IMonitor + { + /********* + ** Properties + *********/ + /// The name of the module which logs messages using this instance. + private readonly string Source; + + /// Manages access to the console output. + private readonly ConsoleInterceptionManager ConsoleManager; + + /// The log file to which to write messages. + private readonly LogFileManager LogFile; + + /// The maximum length of the values. + private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); + + /// The console text color for each log level. + private static readonly IDictionary Colors = Monitor.GetConsoleColorScheme(); + + /// Propagates notification that SMAPI should exit. + private readonly CancellationTokenSource ExitTokenSource; + + + /********* + ** Accessors + *********/ + /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. + public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; + + /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. + internal bool ShowFullStampInConsole { get; set; } + + /// Whether to show trace messages in the console. + internal bool ShowTraceInConsole { get; set; } + + /// Whether to write anything to the console. This should be disabled if no console is available. + internal bool WriteToConsole { get; set; } = true; + + /// Whether to write anything to the log file. This should almost always be enabled. + internal bool WriteToFile { get; set; } = true; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The name of the module which logs messages using this instance. + /// Manages access to the console output. + /// The log file to which to write messages. + /// Propagates notification that SMAPI should exit. + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource) + { + // validate + if (string.IsNullOrWhiteSpace(source)) + throw new ArgumentException("The log source cannot be empty."); + + // initialise + this.Source = source; + this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); + this.ConsoleManager = consoleManager; + this.ExitTokenSource = exitTokenSource; + } + + /// Log a message for the player or developer. + /// The message to log. + /// The log severity level. + public void Log(string message, LogLevel level = LogLevel.Debug) + { + this.LogImpl(this.Source, message, level, Monitor.Colors[level]); + } + + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The reason for the shutdown. + public void ExitGameImmediately(string reason) + { + this.LogFatal($"{this.Source} requested an immediate game shutdown: {reason}"); + this.ExitTokenSource.Cancel(); + } + + /// Write a newline to the console and log file. + internal void Newline() + { + if (this.WriteToConsole) + this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); + if (this.WriteToFile) + this.LogFile.WriteLine(""); + } + + + /********* + ** Private methods + *********/ + /// Log a fatal error message. + /// The message to log. + private void LogFatal(string message) + { + this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); + } + + /// Write a message line to the log. + /// The name of the mod logging the message. + /// The message to log. + /// The log level. + /// The console foreground color. + /// The console background color (or null to leave it as-is). + private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) + { + // generate message + string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); + + string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; + string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; + + // write to console + if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) + { + this.ConsoleManager.ExclusiveWriteWithoutInterception(() => + { + if (this.ConsoleManager.SupportsColor) + { + if (background.HasValue) + Console.BackgroundColor = background.Value; + Console.ForegroundColor = color; + Console.WriteLine(consoleMessage); + Console.ResetColor(); + } + else + Console.WriteLine(consoleMessage); + }); + } + + // write to log file + if (this.WriteToFile) + this.LogFile.WriteLine(fullMessage); + } + + /// Get the color scheme to use for the current console. + private static IDictionary GetConsoleColorScheme() + { + // scheme for dark console background + if (Monitor.IsDark(Console.BackgroundColor)) + { + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warn] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.Magenta + }; + } + + // scheme for light console background + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.Black, + [LogLevel.Warn] = ConsoleColor.DarkYellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.DarkMagenta + }; + } + + /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. + /// The color to check. + private static bool IsDark(ConsoleColor color) + { + switch (color) + { + case ConsoleColor.Black: + case ConsoleColor.Blue: + case ConsoleColor.DarkBlue: + case ConsoleColor.DarkRed: + case ConsoleColor.Red: + return true; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs new file mode 100644 index 00000000..30faca37 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/CacheEntry.cs @@ -0,0 +1,30 @@ +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A cached member reflection result. + internal struct CacheEntry + { + /********* + ** Accessors + *********/ + /// Whether the lookup found a valid match. + public bool IsValid; + + /// The reflection data for this member (or null if invalid). + public MemberInfo MemberInfo; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Whether the lookup found a valid match. + /// The reflection data for this member (or null if invalid). + public CacheEntry(bool isValid, MemberInfo memberInfo) + { + this.IsValid = isValid; + this.MemberInfo = memberInfo; + } + } +} diff --git a/src/SMAPI/Framework/Reflection/PrivateField.cs b/src/SMAPI/Framework/Reflection/PrivateField.cs new file mode 100644 index 00000000..0bf45969 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/PrivateField.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A private field obtained through reflection. + /// The field value type. + internal class PrivateField : IPrivateField + { + /********* + ** Properties + *********/ + /// The type that has the field. + private readonly Type ParentType; + + /// The object that has the instance field (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.FieldInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the field. + /// The object that has the instance field (if applicable). + /// The reflection metadata. + /// Whether the field is static. + /// The or is null. + /// The is null for a non-static field, or not null for a static field. + public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (field == null) + throw new ArgumentNullException(nameof(field)); + if (isStatic && obj != null) + throw new ArgumentException("A static field cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static field must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.FieldInfo = field; + } + + /// Get the field value. + public TValue GetValue() + { + try + { + return (TValue)this.FieldInfo.GetValue(this.Parent); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex); + } + } + + /// Set the field value. + //// The value to set. + public void SetValue(TValue value) + { + try + { + this.FieldInfo.SetValue(this.Parent, value); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex); + } + } + } +} diff --git a/src/SMAPI/Framework/Reflection/PrivateMethod.cs b/src/SMAPI/Framework/Reflection/PrivateMethod.cs new file mode 100644 index 00000000..ba2374f4 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/PrivateMethod.cs @@ -0,0 +1,99 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A private method obtained through reflection. + internal class PrivateMethod : IPrivateMethod + { + /********* + ** Properties + *********/ + /// The type that has the method. + private readonly Type ParentType; + + /// The object that has the instance method (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.MethodInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the method. + /// The object that has the instance method(if applicable). + /// The reflection metadata. + /// Whether the field is static. + /// The or is null. + /// The is null for a non-static method, or not null for a static method. + public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (method == null) + throw new ArgumentNullException(nameof(method)); + if (isStatic && obj != null) + throw new ArgumentException("A static method cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static method must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.MethodInfo = method; + } + + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + public TValue Invoke(params object[] arguments) + { + // invoke method + object result; + try + { + result = this.MethodInfo.Invoke(this.Parent, arguments); + } + catch (Exception ex) + { + throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + } + + // cast return value + try + { + return (TValue)result; + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); + } + } + + /// Invoke the method. + /// The method arguments to pass in. + public void Invoke(params object[] arguments) + { + // invoke method + try + { + this.MethodInfo.Invoke(this.Parent, arguments); + } + catch (Exception ex) + { + throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + } + } + } +} \ No newline at end of file diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs new file mode 100644 index 00000000..08204b7e --- /dev/null +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A private property obtained through reflection. + /// The property value type. + internal class PrivateProperty : IPrivateProperty + { + /********* + ** Properties + *********/ + /// The type that has the field. + private readonly Type ParentType; + + /// The object that has the instance field (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the field. + /// The object that has the instance field (if applicable). + /// The reflection metadata. + /// Whether the field is static. + /// The or is null. + /// The is null for a non-static field, or not null for a static field. + public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (property == null) + throw new ArgumentNullException(nameof(property)); + if (isStatic && obj != null) + throw new ArgumentException("A static property cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static property must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.PropertyInfo = property; + } + + /// Get the property value. + public TValue GetValue() + { + try + { + return (TValue)this.PropertyInfo.GetValue(this.Parent); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex); + } + } + + /// Set the property value. + //// The value to set. + public void SetValue(TValue value) + { + try + { + this.PropertyInfo.SetValue(this.Parent, value); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex); + } + } + } +} diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs new file mode 100644 index 00000000..5c2d90fa --- /dev/null +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Caching; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class Reflector + { + /********* + ** Properties + *********/ + /// The cached fields and methods found via reflection. + private readonly MemoryCache Cache = new MemoryCache(typeof(Reflector).FullName); + + /// The sliding cache expiration time. + private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + + + /********* + ** Public methods + *********/ + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); + + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && field == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); + return field; + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && field == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); + return field; + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); + + // get property from hierarchy + IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && property == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); + return property; + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && property == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); + return property; + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); + return method; + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + // validate parent + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + // get field from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); + return method; + } + + + /********* + ** Private methods + *********/ + /// Get a field from the type hierarchy. + /// The expected field type. + /// The type which has the field. + /// The object which has the field. + /// The field name. + /// The reflection binding which flags which indicates what type of field to find. + private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => + { + FieldInfo fieldInfo = null; + for (; type != null && fieldInfo == null; type = type.BaseType) + fieldInfo = type.GetField(name, bindingFlags); + return fieldInfo; + }); + + return field != null + ? new PrivateField(type, obj, field, isStatic) + : null; + } + + /// Get a property from the type hierarchy. + /// The expected property type. + /// The type which has the property. + /// The object which has the property. + /// The property name. + /// The reflection binding which flags which indicates what type of property to find. + private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => + { + PropertyInfo propertyInfo = null; + for (; type != null && propertyInfo == null; type = type.BaseType) + propertyInfo = type.GetProperty(name, bindingFlags); + return propertyInfo; + }); + + return property != null + ? new PrivateProperty(type, obj, property, isStatic) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags); + return methodInfo; + }); + + return method != null + ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + /// The argument types of the method signature to find. + private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); + return methodInfo; + }); + return method != null + ? new PrivateMethod(type, obj, method, isStatic) + : null; + } + + /// Get a method or field through the cache. + /// The expected type. + /// The cache key. + /// Fetches a new value to cache. + private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo + { + // get from cache + if (this.Cache.Contains(key)) + { + CacheEntry entry = (CacheEntry)this.Cache[key]; + return entry.IsValid + ? (TMemberInfo)entry.MemberInfo + : default(TMemberInfo); + } + + // fetch & cache new value + TMemberInfo result = fetch(); + CacheEntry cacheEntry = new CacheEntry(result != null, result); + this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); + return result; + } + } +} diff --git a/src/SMAPI/Framework/RequestExitDelegate.cs b/src/SMAPI/Framework/RequestExitDelegate.cs new file mode 100644 index 00000000..12d0ea0c --- /dev/null +++ b/src/SMAPI/Framework/RequestExitDelegate.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Framework +{ + /// A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The module which requested an immediate exit. + /// The reason provided for the shutdown. + internal delegate void RequestExitDelegate(string module, string reason); +} \ No newline at end of file diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs new file mode 100644 index 00000000..43de6e96 --- /dev/null +++ b/src/SMAPI/Framework/SContentManager.cs @@ -0,0 +1,531 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Metadata; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's implementation of the game's content manager which lets it raise content events. + internal class SContentManager : LocalizedContentManager + { + /********* + ** Properties + *********/ + /// The possible directory separator characters in an asset key. + private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + + /// The preferred directory separator chaeacter in an asset key. + private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// The underlying content manager's asset cache. + private readonly IDictionary Cache; + + /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. + private readonly Func NormaliseAssetNameForPlatform; + + /// The private method which generates the locale portion of an asset name. + private readonly IPrivateMethod GetKeyLocale; + + /// The language codes used in asset keys. + private readonly IDictionary KeyLocales; + + /// Provides metadata for core game assets. + private readonly CoreAssets CoreAssets; + + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + + /// A lookup of the content managers which loaded each asset. + private readonly IDictionary> AssetLoaders = new Dictionary>(); + + + /********* + ** Accessors + *********/ + /// Interceptors which provide the initial versions of matching assets. + internal IDictionary> Loaders { get; } = new Dictionary>(); + + /// Interceptors which edit matching assets after they're loaded. + internal IDictionary> Editors { get; } = new Dictionary>(); + + /// The absolute path to the . + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The current language code for which to localise content. + /// Encapsulates monitoring and logging. + public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) + : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) + { + // validate + if (monitor == null) + throw new ArgumentNullException(nameof(monitor)); + + // initialise + var reflection = new Reflector(); + this.Monitor = monitor; + + // get underlying fields for interception + this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); + this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); + + // get asset key normalisation logic + if (Constants.TargetPlatform == Platform.Windows) + { + IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); + this.NormaliseAssetNameForPlatform = path => method.Invoke(path); + } + else + this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + + // get asset data + this.CoreAssets = new CoreAssets(this.NormaliseAssetName); + this.KeyLocales = this.GetKeyLocales(reflection); + } + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + public string NormalisePathSeparators(string path) + { + string[] parts = path.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + string normalised = string.Join(SContentManager.PreferredPathSeparator, parts); + if (path.StartsWith(SContentManager.PreferredPathSeparator)) + normalised = SContentManager.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Normalise an asset name so it's consistent with the underlying cache. + /// The asset key. + public string NormaliseAssetName(string assetName) + { + assetName = this.NormalisePathSeparators(assetName); + if (assetName.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)) + return assetName.Substring(0, assetName.Length - 4); + return this.NormaliseAssetNameForPlatform(assetName); + } + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public bool IsLoaded(string assetName) + { + lock (this.Cache) + { + assetName = this.NormaliseAssetName(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T Load(string assetName) + { + return this.LoadFor(assetName, this); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The content manager instance for which to load the asset. + public T LoadFor(string assetName, ContentManager instance) + { + lock (this.Cache) + { + assetName = this.NormaliseAssetName(assetName); + + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) + { + this.TrackAssetLoader(assetName, instance); + return base.Load(assetName); + } + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Cache[assetName] = data; + this.TrackAssetLoader(assetName, instance); + return data; + } + } + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + public void Inject(string assetName, T value) + { + lock (this.Cache) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + this.TrackAssetLoader(assetName, this); + } + } + + /// Get the current content locale. + public string GetLocale() + { + return this.GetKeyLocale.Invoke(); + } + + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + lock (this.Cache) + { + IEnumerable GetAllAssetKeys() + { + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string _); + yield return assetKey; + } + } + + return GetAllAssetKeys().Distinct(); + } + } + + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns whether any cache entries were invalidated. + public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return false; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + + // invalidate matching keys + return this.InvalidateCache((assetName, assetType) => + { + // get asset metadata + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); + + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(assetType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { info }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(assetType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { info })); + }); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns whether any cache entries were invalidated. + public bool InvalidateCache(Func predicate, bool dispose = false) + { + lock (this.Cache) + { + // find matching asset keys + HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out _); + Type type = this.Cache[cacheKey].GetType(); + if (predicate(assetKey, type)) + { + purgeAssetKeys.Add(assetKey); + purgeCacheKeys.Add(cacheKey); + } + } + + // purge assets + foreach (string key in purgeCacheKeys) + { + if (dispose && this.Cache[key] is IDisposable disposable) + disposable.Dispose(); + this.Cache.Remove(key); + this.AssetLoaders.Remove(key); + } + + // reload core game assets + int reloaded = 0; + foreach (string key in purgeAssetKeys) + { + if (this.CoreAssets.ReloadForKey(this, key)) + reloaded++; + } + + // report result + if (purgeCacheKeys.Any()) + { + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + return true; + } + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + return false; + } + } + + /// Dispose assets for the given content manager shim. + /// The content manager whose assets to dispose. + internal void DisposeFor(ContentManagerShim shim) + { + this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); + + foreach (var entry in this.AssetLoaders) + entry.Value.Remove(shim); + this.InvalidateCache((key, type) => !this.AssetLoaders[key].Any(), dispose: true); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + private bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName) + || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset + } + + /// Track that a content manager loaded an asset. + /// The asset key that was loaded. + /// The content manager that loaded the asset. + private void TrackAssetLoader(string key, ContentManager manager) + { + if (!this.AssetLoaders.TryGetValue(key, out HashSet hash)) + hash = this.AssetLoaders[key] = new HashSet(); + hash.Add(manager); + } + + /// Get the locale codes (like ja-JP) used in asset keys. + /// Simplifies access to private game code. + private IDictionary GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; + } + + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset key. + /// The asset locale code (or null if not localised). + private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.KeyLocales.ContainsKey(suffix)) + { + assetKey = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetKey = cacheKey; + localeCode = null; + } + + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) + { + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Value.CanLoad(info); + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); + this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + return null; + } + + // fetch asset from loader + IModMetadata mod = loaders[0].Key; + IAssetLoader loader = loaders[0].Value; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.NormaliseAssetName); + } + + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); + + // edit asset + foreach (var entry in this.GetInterceptors(this.Editors)) + { + // check for match + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // try edit + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // validate edit + if (asset.Data == null) + { + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + else if (!(asset.Data is T)) + { + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + } + + // return result + return asset; + } + + /// Get all registered interceptors from a list. + private IEnumerable> GetInterceptors(IDictionary> entries) + { + foreach (var entry in entries) + { + IModMetadata metadata = entry.Key; + IList interceptors = entry.Value; + + // special case if mod is an interceptor + if (metadata.Mod is T modAsInterceptor) + yield return new KeyValuePair(metadata, modAsInterceptor); + + // registered editors + foreach (T interceptor in interceptors) + yield return new KeyValuePair(metadata, interceptor); + } + } + + /// Dispose held resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); + base.Dispose(disposing); + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs new file mode 100644 index 00000000..7287cab7 --- /dev/null +++ b/src/SMAPI/Framework/SGame.cs @@ -0,0 +1,1403 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Utilities; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Tools; +using xTile.Dimensions; +using xTile.Layers; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's extension of the game's core , used to inject events. + internal class SGame : Game1 + { + /********* + ** Properties + *********/ + /**** + ** SMAPI state + ****/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. + private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second + + /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. + private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second + + /// The number of ticks until SMAPI should notify mods that the game has loaded. + /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. + private int AfterLoadTimer = 5; + + /// Whether the game is returning to the menu. + private bool IsExitingToTitle; + + /// Whether the game is saving and SMAPI has already raised . + private bool IsBetweenSaveEvents; + + /**** + ** Game state + ****/ + /// A record of the buttons pressed as of the previous tick. + private SButton[] PreviousPressedButtons = new SButton[0]; + + /// A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick. + private KeyboardState PreviousKeyState; + + /// A record of the controller state (i.e. the up/down state for each button) as of the previous tick. + private GamePadState PreviousControllerState; + + /// A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick. + private MouseState PreviousMouseState; + + /// The previous mouse position on the screen adjusted for the zoom level. + private Point PreviousMousePosition; + + /// The window size value at last check. + private Point PreviousWindowSize; + + /// The save ID at last check. + private ulong PreviousSaveID; + + /// A hash of at last check. + private int PreviousGameLocations; + + /// A hash of the current location's at last check. + private int PreviousLocationObjects; + + /// The player's inventory at last check. + private IDictionary PreviousItems; + + /// The player's combat skill level at last check. + private int PreviousCombatLevel; + + /// The player's farming skill level at last check. + private int PreviousFarmingLevel; + + /// The player's fishing skill level at last check. + private int PreviousFishingLevel; + + /// The player's foraging skill level at last check. + private int PreviousForagingLevel; + + /// The player's mining skill level at last check. + private int PreviousMiningLevel; + + /// The player's luck skill level at last check. + private int PreviousLuckLevel; + + /// The player's location at last check. + private GameLocation PreviousGameLocation; + + /// The active game menu at last check. + private IClickableMenu PreviousActiveMenu; + + /// The mine level at last check. + private int PreviousMineLevel; + + /// The time of day (in 24-hour military format) at last check. + private int PreviousTime; + + /// The previous content locale. + private LocalizedContentManager.LanguageCode? PreviousLocale; + + /// An index incremented on every tick and reset every 60th tick (0–59). + private int CurrentUpdateTick; + + /// Whether this is the very first update tick since the game started. + private bool FirstUpdate; + + /// The current game instance. + private static SGame Instance; + + /**** + ** Private wrappers + ****/ + /// Simplifies access to private game code. + private static Reflector Reflection; + + // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + /// Used to access private fields and methods. + private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); + private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); + private static float _fps + { + set => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); + } + private static Task _newDayTask => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_newDayTask)).GetValue(); + private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); + public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop + public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); + private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); + private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(); + private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); + private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); + // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + + + /********* + ** Accessors + *********/ + /// SMAPI's content manager. + public SContentManager SContentManager { get; } + + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + /// Simplifies access to private game code. + internal SGame(IMonitor monitor, Reflector reflection) + { + // initialise + this.Monitor = monitor; + this.FirstUpdate = true; + SGame.Instance = this; + SGame.Reflection = reflection; + + // set XNA option required by Stardew Valley + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + + // override content manager + this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); + this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); + Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); + reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager + } + + /**** + ** Intercepted methods & events + ****/ + /// Constructor a content manager to read XNB files. + /// The service provider to use to locate services. + /// The root directory to search for content. + protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) + { + // return default if SMAPI's content manager isn't initialised yet + if (this.SContentManager == null) + { + this.Monitor?.Log("SMAPI's content manager isn't initialised; skipping content manager interception.", LogLevel.Trace); + return base.CreateContentManager(serviceProvider, rootDirectory); + } + + // return single instance if valid + if (serviceProvider != this.Content.ServiceProvider) + throw new InvalidOperationException("SMAPI uses a single content manager internally. You can't get a new content manager with a different service provider."); + if (rootDirectory != this.Content.RootDirectory) + throw new InvalidOperationException($"SMAPI uses a single content manager internally. You can't get a new content manager with a different root directory (current is {this.Content.RootDirectory}, requested {rootDirectory})."); + return new ContentManagerShim(this.SContentManager, "(generated instance)"); + } + + /// The method called when the game is updating its state. This happens roughly 60 times per second. + /// A snapshot of the game timing state. + protected override void Update(GameTime gameTime) + { + try + { + /********* + ** Skip conditions + *********/ + // SMAPI exiting, stop processing game updates + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); + return; + } + + // While a background new-day task is in progress, the game skips its own update logic + // and defers to the XNA Update method. Running mod code in parallel to the background + // update is risky, because data changes can conflict (e.g. collection changed during + // enumeration errors) and data may change unexpectedly from one mod instruction to the + // next. + // + // Therefore we can just run Game1.Update here without raising any SMAPI events. There's + // a small chance that the task will finish after we defer but before the game checks, + // which means technically events should be raised, but the effects of missing one + // update tick are neglible and not worth the complications of bypassing Game1.Update. + if (SGame._newDayTask != null) + { + base.Update(gameTime); + return; + } + + // While the game is writing to the save file in the background, mods can unexpectedly + // fail since they don't have exclusive access to resources (e.g. collection changed + // during enumeration errors). To avoid problems, events are not invoked while a save + // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is + // opened (since the save hasn't started yet), but all other events should be suppressed. + if (Context.IsSaving) + { + // raise before-save + if (!this.IsBetweenSaveEvents) + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save.", LogLevel.Trace); + SaveEvents.InvokeBeforeSave(this.Monitor); + } + + // suppress non-save events + base.Update(gameTime); + return; + } + if (this.IsBetweenSaveEvents) + { + // raise after-save + this.IsBetweenSaveEvents = false; + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + SaveEvents.InvokeAfterSave(this.Monitor); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + + /********* + ** Game loaded events + *********/ + if (this.FirstUpdate) + { + GameEvents.InvokeInitialize(this.Monitor); + } + + /********* + ** Locale changed events + *********/ + if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) + { + var oldValue = this.PreviousLocale; + var newValue = LocalizedContentManager.CurrentLanguageCode; + + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + + if (oldValue != null) + ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.PreviousLocale = newValue; + } + + /********* + ** After load events + *********/ + if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) + { + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) + this.AfterLoadTimer--; + + if (this.AfterLoadTimer == 0) + { + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + Context.IsWorldReady = true; + + SaveEvents.InvokeAfterLoad(this.Monitor); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + } + + /********* + ** Exit to title events + *********/ + // before exit to title + if (Game1.exitToTitle) + this.IsExitingToTitle = true; + + // after exit to title + if (Context.IsWorldReady && this.IsExitingToTitle && Game1.activeClickableMenu is TitleMenu) + { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + + this.IsExitingToTitle = false; + this.CleanupAfterReturnToTitle(); + SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + } + + /********* + ** Window events + *********/ + // Here we depend on the game's viewport instead of listening to the Window.Resize + // event because we need to notify mods after the game handles the resize, so the + // game's metadata (like Game1.viewport) are updated. That's a bit complicated + // since the game adds & removes its own handler on the fly. + if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) + { + Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); + GraphicsEvents.InvokeResize(this.Monitor); + this.PreviousWindowSize = size; + } + + /********* + ** Input events (if window has focus) + *********/ + if (Game1.game1.IsActive) + { + // get latest state + KeyboardState keyState; + GamePadState controllerState; + MouseState mouseState; + Point mousePosition; + try + { + keyState = Keyboard.GetState(); + controllerState = GamePad.GetState(PlayerIndex.One); + mouseState = Mouse.GetState(); + mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); + } + catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + { + keyState = this.PreviousKeyState; + controllerState = this.PreviousControllerState; + mouseState = this.PreviousMouseState; + mousePosition = this.PreviousMousePosition; + } + + // analyse state + SButton[] currentlyPressedKeys = this.GetPressedButtons(keyState, mouseState, controllerState).ToArray(); + SButton[] previousPressedKeys = this.PreviousPressedButtons; + SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); + SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); + bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); + + // get cursor position + ICursorPosition cursor; + { + // cursor position + Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); + Vector2 tile = new Vector2((Game1.viewport.X + screenPixels.X) / Game1.tileSize, (Game1.viewport.Y + screenPixels.Y) / Game1.tileSize); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); + } + + // raise button pressed + foreach (SButton button in framePressedKeys) + { + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); + + // legacy events + if (button.TryGetKeyboard(out Keys key)) + { + if (key != Keys.None) + ControlEvents.InvokeKeyPressed(this.Monitor, key); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); + else + ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); + } + } + + // raise button released + foreach (SButton button in frameReleasedKeys) + { + bool wasClick = + (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click + || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); + + // legacy events + if (button.TryGetKeyboard(out Keys key)) + { + if (key != Keys.None) + ControlEvents.InvokeKeyReleased(this.Monitor, key); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); + else + ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); + } + } + + // raise legacy state-changed events + if (keyState != this.PreviousKeyState) + ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousKeyState, keyState); + if (mouseState != this.PreviousMouseState) + ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousMouseState, mouseState, this.PreviousMousePosition, mousePosition); + + // track state + this.PreviousMouseState = mouseState; + this.PreviousMousePosition = mousePosition; + this.PreviousKeyState = keyState; + this.PreviousControllerState = controllerState; + this.PreviousPressedButtons = currentlyPressedKeys; + } + + /********* + ** Menu events + *********/ + if (Game1.activeClickableMenu != this.PreviousActiveMenu) + { + IClickableMenu previousMenu = this.PreviousActiveMenu; + IClickableMenu newMenu = Game1.activeClickableMenu; + + // log context + if (this.VerboseLogging) + { + if (previousMenu == null) + this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); + else if (newMenu == null) + this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); + else + this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); + } + + // raise menu events + if (newMenu != null) + MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + else + MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); + + // update previous menu + // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) + this.PreviousActiveMenu = newMenu; + } + + /********* + ** World & player events + *********/ + if (Context.IsWorldReady) + { + // raise current location changed + if (Game1.currentLocation != this.PreviousGameLocation) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); + LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + } + + // raise location list changed + if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + + // raise events that shouldn't be triggered on initial load + if (Game1.uniqueIDForThisGame == this.PreviousSaveID) + { + // raise player leveled up a skill + if (Game1.player.combatLevel != this.PreviousCombatLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + if (Game1.player.farmingLevel != this.PreviousFarmingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + if (Game1.player.fishingLevel != this.PreviousFishingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + if (Game1.player.foragingLevel != this.PreviousForagingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + if (Game1.player.miningLevel != this.PreviousMiningLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + if (Game1.player.luckLevel != this.PreviousLuckLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); + + // raise player inventory changed + ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); + if (changedItems.Any()) + PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems); + + // raise current location's object list changed + if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) + LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + + // raise time changed + if (Game1.timeOfDay != this.PreviousTime) + TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + + // raise mine level changed + if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) + MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + } + + // update state + this.PreviousGameLocations = this.GetHash(Game1.locations); + this.PreviousGameLocation = Game1.currentLocation; + this.PreviousCombatLevel = Game1.player.combatLevel; + this.PreviousFarmingLevel = Game1.player.farmingLevel; + this.PreviousFishingLevel = Game1.player.fishingLevel; + this.PreviousForagingLevel = Game1.player.foragingLevel; + this.PreviousMiningLevel = Game1.player.miningLevel; + this.PreviousLuckLevel = Game1.player.luckLevel; + this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); + this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); + this.PreviousTime = Game1.timeOfDay; + this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; + this.PreviousSaveID = Game1.uniqueIDForThisGame; + } + + /********* + ** Game update + *********/ + try + { + base.Update(gameTime); + } + catch (Exception ex) + { + this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + } + + /********* + ** Update events + *********/ + GameEvents.InvokeUpdateTick(this.Monitor); + if (this.FirstUpdate) + this.FirstUpdate = false; + if (this.CurrentUpdateTick % 2 == 0) + GameEvents.InvokeSecondUpdateTick(this.Monitor); + if (this.CurrentUpdateTick % 4 == 0) + GameEvents.InvokeFourthUpdateTick(this.Monitor); + if (this.CurrentUpdateTick % 8 == 0) + GameEvents.InvokeEighthUpdateTick(this.Monitor); + if (this.CurrentUpdateTick % 15 == 0) + GameEvents.InvokeQuarterSecondTick(this.Monitor); + if (this.CurrentUpdateTick % 30 == 0) + GameEvents.InvokeHalfSecondTick(this.Monitor); + if (this.CurrentUpdateTick % 60 == 0) + GameEvents.InvokeOneSecondTick(this.Monitor); + this.CurrentUpdateTick += 1; + if (this.CurrentUpdateTick >= 60) + this.CurrentUpdateTick = 0; + + this.UpdateCrashTimer.Reset(); + } + catch (Exception ex) + { + // log error + this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error); + + // exit if irrecoverable + if (!this.UpdateCrashTimer.Decrement()) + this.Monitor.ExitGameImmediately("the game crashed when updating, and SMAPI was unable to recover the game."); + } + } + + /// The method called to draw everything to the screen. + /// A snapshot of the game timing state. + protected override void Draw(GameTime gameTime) + { + Context.IsInDrawLoop = true; + try + { + this.DrawImpl(gameTime); + this.DrawCrashTimer.Reset(); + } + catch (Exception ex) + { + // log error + this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); + + // exit if irrecoverable + if (!this.DrawCrashTimer.Decrement()) + { + this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); + return; + } + + // recover sprite batch + try + { + if (Game1.spriteBatch.IsOpen(SGame.Reflection)) + { + this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); + Game1.spriteBatch.End(); + } + } + catch (Exception innerEx) + { + this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error); + } + } + Context.IsInDrawLoop = false; + } + + /// Replicate the game's draw logic with some changes for SMAPI. + /// A snapshot of the game timing state. + /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] + private void DrawImpl(GameTime gameTime) + { + if (Game1.debugMode) + { + if (SGame._fpsStopwatch.IsRunning) + { + float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; + SGame._fpsList.Add(totalSeconds); + while (SGame._fpsList.Count >= 120) + SGame._fpsList.RemoveAt(0); + float num = 0.0f; + foreach (float fps in SGame._fpsList) + num += fps; + SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); + } + SGame._fpsStopwatch.Restart(); + } + else + { + if (SGame._fpsStopwatch.IsRunning) + SGame._fpsStopwatch.Reset(); + SGame._fps = 0.0f; + SGame._fpsList.Clear(); + } + if (SGame._newDayTask != null) + { + this.GraphicsDevice.Clear(this.bgColor); + //base.Draw(gameTime); + } + else + { + if ((double)Game1.options.zoomLevel != 1.0) + this.GraphicsDevice.SetRenderTarget(this.screenWrapper); + if (this.IsSaving) + { + this.GraphicsDevice.Clear(this.bgColor); + IClickableMenu activeClickableMenu = Game1.activeClickableMenu; + if (activeClickableMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + try + { + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + catch (Exception ex) + { + this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + activeClickableMenu.exitThisMenu(); + } + Game1.spriteBatch.End(); + } + //base.Draw(gameTime); + this.renderScreenBuffer(); + } + else + { + this.GraphicsDevice.Clear(this.bgColor); + if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + try + { + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else if ((int)Game1.gameMode == 11) + { + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + Game1.spriteBatch.End(); + } + else if (Game1.currentMinigame != null) + { + Game1.currentMinigame.draw(Game1.spriteBatch); + if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.End(); + } + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else if (Game1.showingEndOfNightStuff) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.activeClickableMenu != null) + { + try + { + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + } + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else if ((int)Game1.gameMode == 6) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + string str1 = ""; + for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) + str1 += "."; + string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); + string str3 = str1; + string s = str2 + str3; + string str4 = "..."; + string str5 = str2 + str4; + int widthOfString = SpriteText.getWidthOfString(str5); + int height = 64; + int x = 64; + int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; + SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else + { + Microsoft.Xna.Framework.Rectangle rectangle; + if ((int)Game1.gameMode == 0) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + } + else + { + if (Game1.drawLighting) + { + this.GraphicsDevice.SetRenderTarget(Game1.lightmap); + this.GraphicsDevice.Clear(Color.White * 0.0f); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight)); + for (int index = 0; index < Game1.currentLightSources.Count; ++index) + { + if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(index).position, (int)((double)Game1.currentLightSources.ElementAt(index).radius * (double)Game1.tileSize * 4.0))) + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); + } + Game1.spriteBatch.End(); + this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); + } + if (Game1.bloomDay && Game1.bloom != null) + Game1.bloom.BeginDraw(); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); + if (Game1.background != null) + Game1.background.draw(Game1.spriteBatch); + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.currentLocation.drawWater(Game1.spriteBatch); + if (Game1.CurrentEvent == null) + { + foreach (NPC character in Game1.currentLocation.characters) + { + if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + } + } + else + { + foreach (NPC actor in Game1.CurrentEvent.actors) + { + if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + } + } + Microsoft.Xna.Framework.Rectangle bounds; + if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + double x = (double)Game1.shadowTexture.Bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); + int num3 = 0; + double num4 = 0.0; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + } + Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.CurrentEvent == null) + { + foreach (NPC character in Game1.currentLocation.characters) + { + if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale; + int num3 = 0; + double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + } + } + } + else + { + foreach (NPC actor in Game1.CurrentEvent.actors) + { + if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3)))); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale; + int num3 = 0; + double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + } + } + } + if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + double x = (double)Game1.shadowTexture.Bounds.Center.X; + rectangle = Game1.shadowTexture.Bounds; + double y = (double)rectangle.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); + int num3 = 0; + double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + } + if (Game1.displayFarmer) + Game1.player.draw(Game1.spriteBatch); + if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) + Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); + if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0)); + Game1.currentLocation.draw(Game1.spriteBatch); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; + } + if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Name.Equals("Farm")) + this.drawFarmBuildings(); + if (Game1.tvStation >= 0) + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + if (Game1.panMode) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); + foreach (Warp warp in Game1.currentLocation.warps) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); + } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U) + { + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + } + if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawPlayerHeldObject(Game1.player); + else if (Game1.displayFarmer && Game1.player.ActiveObject != null) + { + if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) + { + Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); + rectangle = Game1.player.GetBoundingBox(); + Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); + Size size1 = Game1.viewport.Size; + if (layer1.PickTile(mapDisplayLocation1, size1) != null) + { + Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); + rectangle = Game1.player.GetBoundingBox(); + Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); + Size size2 = Game1.viewport.Size; + if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) + goto label_127; + } + else + goto label_127; + } + Game1.drawPlayerHeldObject(Game1.player); + } + label_127: + if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + { + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + } + if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + { + Color color = Color.White; + switch ((int)((double)Game1.toolHold / 600.0) + 2) + { + case 1: + color = Tool.copperColor; + break; + case 2: + color = Tool.steelColor; + break; + case 3: + color = Tool.goldColor; + break; + case 4: + color = Tool.iridiumColor; + break; + } + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color); + } + if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) + { + foreach (WeatherDebris weatherDebris in Game1.debrisWeather) + weatherDebris.draw(Game1.spriteBatch); + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); + if (Game1.screenGlow) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); + if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) + Game1.player.CurrentTool.draw(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize))))) + { + for (int index = 0; index < Game1.rainDrops.Length; ++index) + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + } + Game1.spriteBatch.End(); + //base.Draw(gameTime); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + foreach (NPC actor in Game1.currentLocation.currentEvent.actors) + { + if (actor.isEmoting) + { + Vector2 localPosition = actor.getLocalPosition(Game1.viewport); + localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3); + if (actor.age == 2) + localPosition.Y += (float)(Game1.tileSize / 2); + else if (actor.gender == 1) + localPosition.Y += (float)(Game1.tileSize / 6); + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); + } + } + } + Game1.spriteBatch.End(); + if (Game1.drawLighting) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); + if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); + Game1.spriteBatch.End(); + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.drawGrid) + { + int x1 = -Game1.viewport.X % Game1.tileSize; + float num1 = (float)(-Game1.viewport.Y % Game1.tileSize); + int x2 = x1; + while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) + { + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); + x2 += Game1.tileSize; + } + float num2 = num1; + while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) + { + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); + num2 += (float)Game1.tileSize; + } + } + if (Game1.currentBillboard != 0) + this.drawBillboard(); + if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) + { + GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); + this.drawHUD(); + GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); + } + else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) + Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); + if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) + { + for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) + Game1.hudMessages[i].draw(Game1.spriteBatch, i); + } + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) + this.drawDialogueBox(); + Viewport viewport; + if (Game1.progressBar) + { + SpriteBatch spriteBatch1 = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; + rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea; + int y1 = rectangle.Bottom - Game1.tileSize * 2; + int dialogueWidth = Game1.dialogueWidth; + int height1 = Game1.tileSize / 2; + Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); + Color lightGray = Color.LightGray; + spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); + SpriteBatch spriteBatch2 = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; + viewport = Game1.graphics.GraphicsDevice.Viewport; + rectangle = viewport.TitleSafeArea; + int y2 = rectangle.Bottom - Game1.tileSize * 2; + int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); + int height2 = Game1.tileSize / 2; + Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); + Color dimGray = Color.DimGray; + spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + } + if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) + Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Blue * 0.2f; + spriteBatch.Draw(staminaRect, bounds, color); + } + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } + else if ((double)Game1.flashAlpha > 0.0) + { + if (Game1.options.screenFlash) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.White * Math.Min(1f, Game1.flashAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } + Game1.flashAlpha -= 0.1f; + } + if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) + this.drawDialogueBox(); + foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) + overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0); + if (Game1.debugMode) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + SpriteFont smallFont = Game1.smallFont; + object[] objArray = new object[10]; + int index1 = 0; + string str1; + if (!Game1.panMode) + str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize); + else + str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize); + objArray[index1] = (object)str1; + int index2 = 1; + string str2 = " mouseTransparency: "; + objArray[index2] = (object)str2; + int index3 = 2; + float cursorTransparency = Game1.mouseCursorTransparency; + objArray[index3] = (object)cursorTransparency; + int index4 = 3; + string str3 = " mousePosition: "; + objArray[index4] = (object)str3; + int index5 = 4; + int mouseX = Game1.getMouseX(); + objArray[index5] = (object)mouseX; + int index6 = 5; + string str4 = ","; + objArray[index6] = (object)str4; + int index7 = 6; + int mouseY = Game1.getMouseY(); + objArray[index7] = (object)mouseY; + int index8 = 7; + string newLine = Environment.NewLine; + objArray[index8] = (object)newLine; + int index9 = 8; + string str5 = "debugOutput: "; + objArray[index9] = (object)str5; + int index10 = 9; + string debugOutput = Game1.debugOutput; + objArray[index10] = (object)debugOutput; + string text = string.Concat(objArray); + Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); + Color red = Color.Red; + double num1 = 0.0; + Vector2 zero = Vector2.Zero; + double num2 = 1.0; + int num3 = 0; + double num4 = 0.99999988079071; + spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4); + } + if (Game1.showKeyHelp) + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + if (Game1.activeClickableMenu != null) + { + try + { + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + } + else if (Game1.farmEvent != null) + Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + Game1.spriteBatch.End(); + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + + if (GraphicsEvents.HasPostRenderListeners()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + Game1.spriteBatch.End(); + } + + this.renderScreenBuffer(); + } + } + } + } + + /**** + ** Methods + ****/ + /// Perform any cleanup needed when the player unloads a save and returns to the title screen. + private void CleanupAfterReturnToTitle() + { + Context.IsWorldReady = false; + this.AfterLoadTimer = 5; + this.PreviousSaveID = 0; + } + + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) + { + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; + } + } + + /// Get the player inventory changes between two states. + /// The player's current inventory. + /// The player's previous inventory. + private IEnumerable GetInventoryChanges(IEnumerable current, IDictionary previous) + { + current = current.Where(n => n != null).ToArray(); + foreach (Item item in current) + { + // stack size changed + if (previous != null && previous.ContainsKey(item)) + { + if (previous[item] != item.Stack) + yield return new ItemStackChange { Item = item, StackChange = item.Stack - previous[item], ChangeType = ChangeType.StackChange }; + } + + // new item + else + yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; + } + + // removed items + if (previous != null) + { + foreach (var entry in previous) + { + if (current.Any(i => i == entry.Key)) + continue; + + yield return new ItemStackChange { Item = entry.Key, StackChange = -entry.Key.Stack, ChangeType = ChangeType.Removed }; + } + } + } + + /// Get a hash value for an enumeration. + /// The enumeration of items to hash. + private int GetHash(IEnumerable enumerable) + { + int hash = 0; + foreach (object v in enumerable) + hash ^= v.GetHashCode(); + return hash; + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs new file mode 100644 index 00000000..3193aa3c --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Xna.Framework.Input; +using Newtonsoft.Json; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Encapsulates SMAPI's JSON file parsing. + internal class JsonHelper + { + /********* + ** Accessors + *********/ + /// The JSON settings to use when serialising and deserialising files. + private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded + Converters = new List + { + new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) + } + }; + + + /********* + ** Public methods + *********/ + /// Read a JSON file. + /// The model type. + /// The absolete file path. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The given path is empty or invalid. + public TModel ReadJsonFile(string fullPath) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // read file + string json; + try + { + json = File.ReadAllText(fullPath); + } + catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + { + return null; + } + + // deserialise model + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException ex) + { + string message = $"The file at {fullPath} doesn't seem to be valid JSON."; + + string text = File.ReadAllText(fullPath); + if (text.Contains("“") || text.Contains("”")) + message += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + + message += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(message); + } + } + + /// Save to a JSON file. + /// The model type. + /// The absolete file path. + /// The model to save. + /// The given path is empty or invalid. + public void WriteJsonFile(string fullPath, TModel model) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // create directory if needed + string dir = Path.GetDirectoryName(fullPath); + if (dir == null) + throw new ArgumentException("The file path is invalid.", nameof(fullPath)); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + // write file + string json = JsonConvert.SerializeObject(model, this.JsonSettings); + File.WriteAllText(fullPath, json); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SFieldConverter.cs b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs new file mode 100644 index 00000000..917c950d --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Overrides how SMAPI reads and writes and fields. + internal class SFieldConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return + objectType == typeof(ISemanticVersion) + || objectType == typeof(IManifestDependency[]) + || objectType == typeof(ModDataID) + || objectType == typeof(ModCompatibility[]); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // semantic version + if (objectType == typeof(ISemanticVersion)) + { + JToken token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.Object: + { + JObject obj = (JObject)token; + int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); + string build = obj.Value(nameof(ISemanticVersion.Build)); + return new SemanticVersion(major, minor, patch, build); + } + + case JTokenType.String: + { + string str = token.Value(); + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta."); + return version; + } + + default: + throw new SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); + } + } + + // manifest dependencies + if (objectType == typeof(IManifestDependency[])) + { + List result = new List(); + foreach (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); + string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); + bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); + } + return result.ToArray(); + } + + // mod data ID + if (objectType == typeof(ModDataID)) + { + JToken token = JToken.Load(reader); + return new ModDataID(token.Value()); + } + + // mod compatibility records + if (objectType == typeof(ModCompatibility[])) + { + List result = new List(); + foreach (JProperty property in JObject.Load(reader).Properties()) + { + string range = property.Name; + ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value(nameof(ModCompatibility.Status))); + string reasonPhrase = property.Value.Value(nameof(ModCompatibility.ReasonPhrase)); + + result.Add(new ModCompatibility(range, status, reasonPhrase)); + } + return result.ToArray(); + } + + // unknown + throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs new file mode 100644 index 00000000..37108556 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// A variant of which only converts certain enums. + internal class SelectiveStringEnumConverter : StringEnumConverter + { + /********* + ** Properties + *********/ + /// The enum type names to convert. + private readonly HashSet Types; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The enum types to convert. + public SelectiveStringEnumConverter(params Type[] types) + { + this.Types = new HashSet(types.Select(p => p.FullName)); + } + + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type type) + { + return + base.CanConvert(type) + && this.Types.Contains((Nullable.GetUnderlyingType(type) ?? type).FullName); + } + } +} diff --git a/src/SMAPI/Framework/Utilities/ContextHash.cs b/src/SMAPI/Framework/Utilities/ContextHash.cs new file mode 100644 index 00000000..6c0fdc90 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/ContextHash.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// A wrapper meant for tracking recursive contexts. + /// The key type. + internal class ContextHash : HashSet + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public ContextHash() { } + + /// Construct an instance. + /// The implementation to use when comparing values in the set, or null to use the default comparer for the set type. + public ContextHash(IEqualityComparer comparer) + : base(comparer) { } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The key to add. + /// The action to perform. + /// The specified key is already added. + public void Track(T key, Action action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + action(); + } + finally + { + this.Remove(key); + } + } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The value type returned by the method. + /// The key to add. + /// The action to perform. + public TResult Track(T key, Func action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + return action(); + } + finally + { + this.Remove(key); + } + } + } +} diff --git a/src/SMAPI/Framework/Utilities/Countdown.cs b/src/SMAPI/Framework/Utilities/Countdown.cs new file mode 100644 index 00000000..921a35ce --- /dev/null +++ b/src/SMAPI/Framework/Utilities/Countdown.cs @@ -0,0 +1,44 @@ +namespace StardewModdingAPI.Framework.Utilities +{ + /// Counts down from a baseline value. + internal class Countdown + { + /********* + ** Accessors + *********/ + /// The initial value from which to count down. + public int Initial { get; } + + /// The current value. + public int Current { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial value from which to count down. + public Countdown(int initial) + { + this.Initial = initial; + this.Current = initial; + } + + /// Reduce the current value by one. + /// Returns whether the value was decremented (i.e. wasn't already zero). + public bool Decrement() + { + if (this.Current <= 0) + return false; + + this.Current--; + return true; + } + + /// Restart the countdown. + public void Reset() + { + this.Current = this.Initial; + } + } +} diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs new file mode 100644 index 00000000..f3c7de28 --- /dev/null +++ b/src/SMAPI/Framework/WebApiClient.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Framework +{ + /// Provides methods for interacting with the SMAPI web API. + internal class WebApiClient + { + /********* + ** Properties + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) + { +#if !SMAPI_FOR_WINDOWS + baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } + + /// Get the latest SMAPI version. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params string[] modKeys) + { + return this.Post>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys) + ); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The body content type. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + /// The body content to post. + private TResult Post(string url, TBody content) + { + /*** + ** Note: avoid HttpClient for Mac compatibility. + ***/ + using (WebClient client = new WebClient()) + { + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response); + } + } + } +} diff --git a/src/SMAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs new file mode 100644 index 00000000..c3021144 --- /dev/null +++ b/src/SMAPI/IAssetData.cs @@ -0,0 +1,47 @@ +using System; + +namespace StardewModdingAPI +{ + /// Generic metadata and methods for a content asset being loaded. + /// The expected data type. + public interface IAssetData : IAssetInfo + { + /********* + ** Accessors + *********/ + /// The content data being read. + TValue Data { get; } + + + /********* + ** Public methods + *********/ + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + void ReplaceWith(TValue value); + } + + /// Generic metadata and methods for a content asset being loaded. + public interface IAssetData : IAssetData + { + /********* + ** Public methods + *********/ + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary value. + /// The content being read isn't a dictionary. + IAssetDataForDictionary AsDictionary(); + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + IAssetDataForImage AsImage(); + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + TData GetData(); + } +} diff --git a/src/SMAPI/IAssetDataForDictionary.cs b/src/SMAPI/IAssetDataForDictionary.cs new file mode 100644 index 00000000..53c24346 --- /dev/null +++ b/src/SMAPI/IAssetDataForDictionary.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + public interface IAssetDataForDictionary : IAssetData> + { + /********* + ** Public methods + *********/ + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + void Set(TKey key, TValue value); + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + void Set(TKey key, Func value); + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + void Set(Func replacer); + } +} diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs new file mode 100644 index 00000000..4584a20e --- /dev/null +++ b/src/SMAPI/IAssetDataForImage.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + public interface IAssetDataForImage : IAssetData + { + /********* + ** Public methods + *********/ + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + /// The content being read isn't an image. + void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); + } +} diff --git a/src/SMAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs new file mode 100644 index 00000000..d2c6f295 --- /dev/null +++ b/src/SMAPI/IAssetEditor.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI +{ + /// Edits matching content assets. + public interface IAssetEditor + { + /********* + ** Public methods + *********/ + /// Get whether this instance can edit the given asset. + /// Basic metadata about the asset being loaded. + bool CanEdit(IAssetInfo asset); + + /// Edit a matched asset. + /// A helper which encapsulates metadata about an asset and enables changes to it. + void Edit(IAssetData asset); + } +} diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs new file mode 100644 index 00000000..5dd58e2e --- /dev/null +++ b/src/SMAPI/IAssetInfo.cs @@ -0,0 +1,28 @@ +using System; + +namespace StardewModdingAPI +{ + /// Basic metadata for a content asset. + public interface IAssetInfo + { + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + string AssetName { get; } + + /// The content data type. + Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + bool AssetNameEquals(string path); + } +} diff --git a/src/SMAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs new file mode 100644 index 00000000..ad97b941 --- /dev/null +++ b/src/SMAPI/IAssetLoader.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI +{ + /// Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use instead. + public interface IAssetLoader + { + /********* + ** Public methods + *********/ + /// Get whether this instance can load the initial version of the given asset. + /// Basic metadata about the asset being loaded. + bool CanLoad(IAssetInfo asset); + + /// Load a matched asset. + /// Basic metadata about the asset being loaded. + T Load(IAssetInfo asset); + } +} diff --git a/src/SMAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs new file mode 100644 index 00000000..fb562e32 --- /dev/null +++ b/src/SMAPI/ICommandHelper.cs @@ -0,0 +1,26 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for managing console commands. + public interface ICommandHelper : IModLinked + { + /********* + ** Public methods + *********/ + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + ICommandHelper Add(string name, string documentation, Action callback); + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + bool Trigger(string name, string[] arguments); + } +} diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs new file mode 100644 index 00000000..b78b165b --- /dev/null +++ b/src/SMAPI/IContentHelper.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Provides an API for loading content assets. + public interface IContentHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// Interceptors which provide the initial versions of matching content assets. + IList AssetLoaders { get; } + + /// Interceptors which edit matching content assets after they're loaded. + IList AssetEditors { get; } + + /// The game's current locale code (like pt-BR). + string CurrentLocale { get; } + + /// The game's current locale as an enum value. + LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } + + + /********* + ** Public methods + *********/ + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T Load(string key, ContentSource source = ContentSource.ModFolder); + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); + + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to invalidate in the content folder. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + bool InvalidateCache(string key); + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + bool InvalidateCache(); + } +} diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs new file mode 100644 index 00000000..ddb8eb49 --- /dev/null +++ b/src/SMAPI/ICursorPosition.cs @@ -0,0 +1,17 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI +{ + /// Represents a cursor position in the different coordinate systems. + public interface ICursorPosition + { + /// The pixel position relative to the top-left corner of the visible screen. + Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + Vector2 GrabTile { get; } + } +} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs new file mode 100644 index 00000000..9db1d538 --- /dev/null +++ b/src/SMAPI/IManifest.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// A manifest which describes a mod for SMAPI. + public interface IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + string Name { get; } + + /// A brief description of the mod. + string Description { get; } + + /// The mod author's name. + string Author { get; } + + /// The mod version. + ISemanticVersion Version { get; } + + /// The minimum SMAPI version required by this mod, if any. + ISemanticVersion MinimumApiVersion { get; } + + /// The unique mod ID. + string UniqueID { get; } + + /// The name of the DLL in the directory that has the method. + string EntryDll { get; } + + /// The other mods that must be loaded before this mod. + IManifestDependency[] Dependencies { get; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; set; } + + /// Any manifest fields which didn't match a valid field. + IDictionary ExtraFields { get; } + } +} diff --git a/src/SMAPI/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs new file mode 100644 index 00000000..e86cd1f4 --- /dev/null +++ b/src/SMAPI/IManifestDependency.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI +{ + /// A mod dependency listed in a mod manifest. + public interface IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } + } +} diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs new file mode 100644 index 00000000..35ac7c0f --- /dev/null +++ b/src/SMAPI/IMod.cs @@ -0,0 +1,26 @@ +namespace StardewModdingAPI +{ + /// The implementation for a Stardew Valley mod. + public interface IMod + { + /********* + ** Accessors + *********/ + /// Provides simplified APIs for writing mods. + IModHelper Helper { get; } + + /// Writes messages to the console and log file. + IMonitor Monitor { get; } + + /// The mod's manifest. + IManifest ModManifest { get; } + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + void Entry(IModHelper helper); + } +} \ No newline at end of file diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs new file mode 100644 index 00000000..116e8508 --- /dev/null +++ b/src/SMAPI/IModHelper.cs @@ -0,0 +1,58 @@ +namespace StardewModdingAPI +{ + /// Provides simplified APIs for writing mods. + public interface IModHelper + { + /********* + ** Accessors + *********/ + /// The full path to the mod's folder. + string DirectoryPath { get; } + + /// An API for loading content assets. + IContentHelper Content { get; } + + /// Simplifies access to private game code. + IReflectionHelper Reflection { get; } + + /// Metadata about loaded mods. + IModRegistry ModRegistry { get; } + + /// An API for managing console commands. + ICommandHelper ConsoleCommands { get; } + + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + ITranslationHelper Translation { get; } + + + /********* + ** Public methods + *********/ + /**** + ** Mod config file + ****/ + /// Read the mod's configuration file (and create it if needed). + /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. + TConfig ReadConfig() where TConfig : class, new(); + + /// Save to the mod's configuration file. + /// The config class type. + /// The config settings to save. + void WriteConfig(TConfig config) where TConfig : class, new(); + + /**** + ** Generic JSON files + ****/ + /// Read a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + TModel ReadJsonFile(string path) where TModel : class; + + /// Save to a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// The model to save. + void WriteJsonFile(string path, TModel model) where TModel : class; + } +} \ No newline at end of file diff --git a/src/SMAPI/IModLinked.cs b/src/SMAPI/IModLinked.cs new file mode 100644 index 00000000..172ee30c --- /dev/null +++ b/src/SMAPI/IModLinked.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// An instance linked to a mod. + public interface IModLinked + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod for which the instance was created. + string ModID { get; } + } +} diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs new file mode 100644 index 00000000..5ef3fd65 --- /dev/null +++ b/src/SMAPI/IModRegistry.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Provides an API for fetching metadata about loaded mods. + public interface IModRegistry : IModLinked + { + /// Get metadata for all loaded mods. + IEnumerable GetAll(); + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + IManifest Get(string uniqueID); + + /// Get whether a mod has been loaded. + /// The mod's unique ID. + bool IsLoaded(string uniqueID); + } +} \ No newline at end of file diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs new file mode 100644 index 00000000..62c479bc --- /dev/null +++ b/src/SMAPI/IMonitor.cs @@ -0,0 +1,25 @@ +namespace StardewModdingAPI +{ + /// Encapsulates monitoring and logging for a given module. + public interface IMonitor + { + /********* + ** Accessors + *********/ + /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. + bool IsExiting { get; } + + + /********* + ** Methods + *********/ + /// Log a message for the player or developer. + /// The message to log. + /// The log severity level. + void Log(string message, LogLevel level = LogLevel.Debug); + + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The reason for the shutdown. + void ExitGameImmediately(string reason); + } +} diff --git a/src/SMAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs new file mode 100644 index 00000000..3e681c12 --- /dev/null +++ b/src/SMAPI/IPrivateField.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A private field obtained through reflection. + /// The field value type. + public interface IPrivateField + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the field value. + TValue GetValue(); + + /// Set the field value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/SMAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs new file mode 100644 index 00000000..67fc8b3c --- /dev/null +++ b/src/SMAPI/IPrivateMethod.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A private method obtained through reflection. + public interface IPrivateMethod + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + TValue Invoke(params object[] arguments); + + /// Invoke the method. + /// The method arguments to pass in. + void Invoke(params object[] arguments); + } +} \ No newline at end of file diff --git a/src/SMAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs new file mode 100644 index 00000000..8d67fa7a --- /dev/null +++ b/src/SMAPI/IPrivateProperty.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A private property obtained through reflection. + /// The property value type. + public interface IPrivateProperty + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the property value. + TValue GetValue(); + + /// Set the property value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs new file mode 100644 index 00000000..fb2c7861 --- /dev/null +++ b/src/SMAPI/IReflectionHelper.cs @@ -0,0 +1,67 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for accessing private game code. + public interface IReflectionHelper : IModLinked + { + /********* + ** Public methods + *********/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateField GetPrivateField(object obj, string name, bool required = true); + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateField GetPrivateField(Type type, string name, bool required = true); + + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true); + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true); + + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + TValue GetPrivateValue(object obj, string name, bool required = true); + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// This is a shortcut for followed by . + TValue GetPrivateValue(Type type, string name, bool required = true); + + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); + } +} diff --git a/src/SMAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs new file mode 100644 index 00000000..0483c97b --- /dev/null +++ b/src/SMAPI/ISemanticVersion.cs @@ -0,0 +1,59 @@ +using System; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + int PatchVersion { get; } + + /// An optional build tag. + string Build { get; } + + + /********* + ** Accessors + *********/ + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsOlderThan(string other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsNewerThan(string other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + bool IsBetween(string min, string max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/SMAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs new file mode 100644 index 00000000..c4b72444 --- /dev/null +++ b/src/SMAPI/ITranslationHelper.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public interface ITranslationHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// The current locale. + string Locale { get; } + + /// The game's current language code. + LocalizedContentManager.LanguageCode LocaleEnum { get; } + + + /********* + ** Public methods + *********/ + /// Get all translations for the current locale. + IEnumerable GetTranslations(); + + /// Get a translation for the current locale. + /// The translation key. + Translation Get(string key); + + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + Translation Get(string key, object tokens); + } +} diff --git a/src/SMAPI/LogLevel.cs b/src/SMAPI/LogLevel.cs new file mode 100644 index 00000000..89647876 --- /dev/null +++ b/src/SMAPI/LogLevel.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI +{ + /// The log severity levels. + public enum LogLevel + { + /// Tracing info intended for developers. + Trace, + + /// Troubleshooting info that may be relevant to the player. + Debug, + + /// Info relevant to the player. This should be used judiciously. + Info, + + /// An issue the player should be aware of. This should be used rarely. + Warn, + + /// A message indicating something went wrong. + Error, + + /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. + Alert + } +} \ No newline at end of file diff --git a/src/SMAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs new file mode 100644 index 00000000..24f23af7 --- /dev/null +++ b/src/SMAPI/Metadata/CoreAssets.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; +using StardewValley.Locations; +using StardewValley.Objects; +using StardewValley.Projectiles; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Metadata +{ + /// Provides metadata about core assets in the game. + internal class CoreAssets + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + /// Setters which update static or singleton texture fields indexed by normalised asset key. + private readonly IDictionary> SingletonSetters; + + + /********* + ** Public methods + *********/ + /// Initialise the core asset data. + /// Normalises an asset key to match the cache key. + public CoreAssets(Func getNormalisedPath) + { + this.GetNormalisedPath = getNormalisedPath; + this.SingletonSetters = + new Dictionary> + { + // from Game1.loadContent + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), + ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), + ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), + ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), + ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), + ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), + ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), + ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), + ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), + ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), + ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), + ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), + ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), + ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), + ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), + ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), + ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), + ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), + ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), + ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), + ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), + ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), + ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), + ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), + ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), + ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), + ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), + ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), + ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), + ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), + ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), + ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), + ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), + ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), + ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), + + // from Game1.ResetToolSpriteSheet + ["TileSheets\\tools"] = (content, key) => Game1.ResetToolSpriteSheet(), + + // from Bush + ["TileSheets\\bushes"] = (content, key) => Bush.texture = content.Load(key), + + // from Critter + ["TileSheets\\critters"] = (content, key) => Critter.critterTexture = content.Load(key), + + // from Farm + ["Buildings\\houses"] = (content, key) => + { + Farm farm = Game1.getFarm(); + if (farm != null) + farm.houseTextures = content.Load(key); + }, + + // from Farmer + ["Characters\\Farmer\\farmer_base"] = (content, key) => + { + if (Game1.player != null && Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + }, + ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + { + if (Game1.player != null && !Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + }, + + // from Flooring + ["TerrainFeatures\\Flooring"] = (content, key) => Flooring.floorsTexture = content.Load(key), + + // from FruitTree + ["TileSheets\\fruitTrees"] = (content, key) => FruitTree.texture = content.Load(key), + + // from HoeDirt + ["TerrainFeatures\\hoeDirt"] = (content, key) => HoeDirt.lightTexture = content.Load(key), + ["TerrainFeatures\\hoeDirtDark"] = (content, key) => HoeDirt.darkTexture = content.Load(key), + ["TerrainFeatures\\hoeDirtSnow"] = (content, key) => HoeDirt.snowTexture = content.Load(key), + + // from Wallpaper + ["Maps\\walls_and_floors"] = (content, key) => Wallpaper.wallpaperTexture = content.Load(key) + } + .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value); + } + + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether an asset was reloaded. + public bool ReloadForKey(SContentManager content, string key) + { + // static assets + if (this.SingletonSetters.TryGetValue(key, out Action reload)) + { + reload(content, key); + return true; + } + + // building textures + if (key.StartsWith(this.GetNormalisedPath("Buildings\\"))) + { + Building[] buildings = this.GetAllBuildings().Where(p => key == this.GetNormalisedPath($"Buildings\\{p.buildingType}")).ToArray(); + if (buildings.Any()) + { + Texture2D texture = content.Load(key); + foreach (Building building in buildings) + building.texture = texture; + return true; + } + return false; + } + + return false; + } + + + /********* + ** Private methods + *********/ + /// Get all player-constructed buildings in the world. + private IEnumerable GetAllBuildings() + { + return Game1.locations + .OfType() + .SelectMany(p => p.buildings); + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs new file mode 100644 index 00000000..3346f1ac --- /dev/null +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Rewriters; +using StardewValley; + +namespace StardewModdingAPI.Metadata +{ + /// Provides CIL instruction handlers which rewrite mods for compatibility and throw exceptions for incompatible code. + internal class InstructionMetadata + { + /********* + ** Public methods + *********/ + /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. + public IEnumerable GetHandlers() + { + return new IInstructionHandler[] + { + /**** + ** throw exception for incompatible code + ****/ + // changes in Stardew Valley 1.2 (with no rewriters) + new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible), + + // APIs removed in SMAPI 1.9 + new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Extensions", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Inheritance.SGame", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Inheritance.SObject", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.LogWriter", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Manifest", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Version", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), + + // APIs removed in SMAPI 2.0 + new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), + + /**** + ** detect code which may impact game stability + ****/ + new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch), + new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), + + /**** + ** rewrite CIL to fix incompatible code + ****/ + // crossplatform + new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), + + // Stardew Valley 1.2 + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), + new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), + new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), + + // SMAPI 1.9 + new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), + + // SMAPI 2.0 + new VirtualEntryCallRemover() // Mod.Entry changed from virtual to abstract in SMAPI 2.0, which breaks the few mods which called base.Entry() + }; + } + } +} diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs new file mode 100644 index 00000000..ee75ba54 --- /dev/null +++ b/src/SMAPI/Mod.cs @@ -0,0 +1,50 @@ +using System; + +namespace StardewModdingAPI +{ + /// The base class for a mod. + public abstract class Mod : IMod, IDisposable + { + /********* + ** Accessors + *********/ + /// Provides simplified APIs for writing mods. + public IModHelper Helper { get; internal set; } + + /// Writes messages to the console and log file. + public IMonitor Monitor { get; internal set; } + + /// The mod's manifest. + public IManifest ModManifest { get; internal set; } + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public abstract void Entry(IModHelper helper); + + /// Release or reset unmanaged resources. + public void Dispose() + { + (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it + this.Dispose(true); + GC.SuppressFinalize(this); + } + + + /********* + ** Private methods + *********/ + /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. + /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. + protected virtual void Dispose(bool disposing) { } + + /// Destruct the instance. + ~Mod() + { + this.Dispose(false); + } + } +} diff --git a/src/SMAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs new file mode 100644 index 00000000..b4286a89 --- /dev/null +++ b/src/SMAPI/PatchMode.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates how an image should be patched. + public enum PatchMode + { + /// Erase the original content within the area before drawing the new content. + Replace, + + /// Draw the new content over the original content, so the original content shows through any transparent pixels. + Overlay + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs new file mode 100644 index 00000000..7dfdc745 --- /dev/null +++ b/src/SMAPI/Program.cs @@ -0,0 +1,966 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Security; +using System.Threading; +#if SMAPI_FOR_WINDOWS +using System.Management; +using System.Windows.Forms; +#endif +using Newtonsoft.Json; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModHelpers; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Models; +using StardewValley; +using Monitor = StardewModdingAPI.Framework.Monitor; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI +{ + /// The main entry point for SMAPI, responsible for hooking into and launching the game. + internal class Program : IDisposable + { + /********* + ** Properties + *********/ + /// The log file to which to write messages. + private readonly LogFileManager LogFile; + + /// Manages console output interception. + private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + + /// The core logger and monitor for SMAPI. + private readonly Monitor Monitor; + + /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + + /// Simplifies access to private game code. + private readonly Reflector Reflection = new Reflector(); + + /// The underlying game instance. + private SGame GameInstance; + + /// The underlying content manager. + private SContentManager ContentManager => this.GameInstance.SContentManager; + + /// The SMAPI configuration settings. + /// This is initialised after the game starts. + private SConfig Settings; + + /// Tracks the installed mods. + /// This is initialised after the game starts. + private ModRegistry ModRegistry; + + /// Manages deprecation warnings. + /// This is initialised after the game starts. + private DeprecationManager DeprecationManager; + + /// Manages console commands. + /// This is initialised after the game starts. + private CommandManager CommandManager; + + /// Whether the game is currently running. + private bool IsGameRunning; + + /// Whether the program has been disposed. + private bool IsDisposed; + + + /********* + ** Public methods + *********/ + /// The main entry point which hooks into and launches the game. + /// The command-line arguments. + public static void Main(string[] args) + { + Program.AssertMinimumCompatibility(); + + // get flags from arguments + bool writeToConsole = !args.Contains("--no-terminal"); + + // get log path from arguments + string logPath = null; + { + int pathIndex = Array.LastIndexOf(args, "--log-path") + 1; + if (pathIndex >= 1 && args.Length >= pathIndex) + { + logPath = args[pathIndex]; + if (!Path.IsPathRooted(logPath)) + logPath = Path.Combine(Constants.LogDir, logPath); + } + } + if (string.IsNullOrWhiteSpace(logPath)) + logPath = Constants.DefaultLogPath; + + // load SMAPI + using (Program program = new Program(writeToConsole, logPath)) + program.RunInteractively(); + } + + /// Construct an instance. + /// Whether to output log messages to the console. + /// The full file path to which to write log messages. + public Program(bool writeToConsole, string logPath) + { + this.LogFile = new LogFileManager(logPath); + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; + } + + /// Launch SMAPI. + [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions + public void RunInteractively() + { + // initialise SMAPI + try + { + // init logging + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + this.Monitor.Log($"Mods go here: {Constants.ModPath}"); + this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); + + // validate paths + this.VerifyPath(Constants.ModPath); + this.VerifyPath(Constants.LogDir); + + // validate game version + if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) + { + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) + { + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + + // add error handlers +#if SMAPI_FOR_WINDOWS + Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); +#endif + AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + + // override game + this.GameInstance = new SGame(this.Monitor, this.Reflection); + StardewValley.Program.gamePtr = this.GameInstance; + + // add exit handler + new Thread(() => + { + this.CancellationTokenSource.Token.WaitHandle.WaitOne(); + if (this.IsGameRunning) + { + try + { + File.WriteAllText(Constants.FatalCrashMarker, string.Empty); + File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); + } + + this.GameInstance.Exit(); + } + }).Start(); + + // hook into game events +#if SMAPI_FOR_WINDOWS + ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); +#endif + this.GameInstance.Exiting += (sender, e) => this.Dispose(); + GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); + ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); + + // set window titles + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + + // show details if game crashed during last session + if (File.Exists(Constants.FatalCrashMarker)) + { + this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: http://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log($"If you ask for help, make sure to attach this file: {Constants.FatalCrashLog}", LogLevel.Error); + this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); + Console.ReadKey(); + File.Delete(Constants.FatalCrashLog); + File.Delete(Constants.FatalCrashMarker); + } + + // start game + this.Monitor.Log("Starting game...", LogLevel.Trace); + try + { + this.IsGameRunning = true; + this.GameInstance.Run(); + } + catch (Exception ex) + { + this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + } + finally + { + this.Dispose(); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Monitor.Log("Disposing...", LogLevel.Trace); + + // skip if already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; + + // dispose mod data + foreach (IModMetadata mod in this.ModRegistry.GetMods()) + { + try + { + (mod.Mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {mod.DisplayName} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } + + // dispose core components + this.IsGameRunning = false; + this.LogFile?.Dispose(); + this.ConsoleManager?.Dispose(); + this.CancellationTokenSource?.Dispose(); + this.GameInstance?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. + private static void AssertMinimumCompatibility() + { + void PrintErrorAndExit(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + Program.PressAnyKeyToExit(showMessage: true); + } + + // get game assembly name + const string gameAssemblyName = +#if SMAPI_FOR_WINDOWS + "Stardew Valley"; +#else + "StardewValley"; +#endif + + // game not present + if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null) + { + PrintErrorAndExit( + "Oops! SMAPI can't find the game. " + + (Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Windows")) || Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Mono")) + ? "It looks like you're running SMAPI from the download package, but you need to run the installed version instead. " + : "Make sure you're running StardewModdingAPI.exe in your game folder. " + ) + + "See the readme.txt file for details." + ); + } + + // Stardew Valley 1.2 types not present + if (Type.GetType($"StardewValley.LocalizedContentManager+LanguageCode, {gameAssemblyName}", throwOnError: false) == null) + { + PrintErrorAndExit(Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion) + ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." + : "Oops! SMAPI doesn't seem to be compatible with your game. Make sure you're running the latest version of Stardew Valley and SMAPI." + ); + } + } + + /// Initialise SMAPI and mods after the game starts. + private void InitialiseAfterGameStart() + { + // load settings + this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; + + // load core components + this.ModRegistry = new ModRegistry(); + this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + this.CommandManager = new CommandManager(); + + // redirect direct console output + { + Monitor monitor = this.GetSecondaryMonitor("Console.Out"); + if (monitor.WriteToConsole) + this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); + } + + // add headers + if (this.Settings.DeveloperMode) + { + this.Monitor.ShowTraceInConsole = true; + this.Monitor.ShowFullStampInConsole = true; + this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); + } + if (!this.Settings.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); + 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.VerboseLog("Verbose logging enabled."); + + // validate XNB integrity + if (!this.ValidateContentIntegrity()) + this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); + + // load mods + { + this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); + ModResolver resolver = new ModResolver(); + + // load manifests + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); + resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); + + // process dependencies + mods = resolver.ProcessDependencies(mods).ToArray(); + + // load mods + this.LoadMods(mods, new JsonHelper(), this.ContentManager); + + // check for updates + this.CheckForUpdatesAsync(mods); + } + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); + return; + } + + // update window titles + int modsLoaded = this.ModRegistry.GetMods().Count(); + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; + + // start SMAPI console + new Thread(this.RunConsoleLoop).Start(); + } + + /// Handle the game changing locale. + private void OnLocaleChanged() + { + // get locale + string locale = this.ContentManager.GetLocale(); + LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); + + // update mod translation helpers + foreach (IModMetadata mod in this.ModRegistry.GetMods()) + (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); + } + + /// Run a loop handling console input. + [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] + private void RunConsoleLoop() + { + // prepare console + this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); + this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); + this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + + // start handling command line input + Thread inputThread = new Thread(() => + { + while (true) + { + // get input + string input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + // parse input + try + { + if (!this.CommandManager.Trigger(input)) + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + } + catch (Exception ex) + { + this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + }); + inputThread.Start(); + + // keep console thread alive while the game is running + while (this.IsGameRunning && !this.Monitor.IsExiting) + Thread.Sleep(1000 / 10); + if (inputThread.ThreadState == ThreadState.Running) + inputThread.Abort(); + } + + /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. + /// Returns whether all integrity checks passed. + private bool ValidateContentIntegrity() + { + this.Monitor.Log("Detecting common issues...", LogLevel.Trace); + bool issuesFound = false; + + // object format (commonly broken by outdated files) + { + // detect issues + bool hasObjectIssues = false; + void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace); + foreach (KeyValuePair entry in Game1.objectInformation) + { + // must not be empty + if (string.IsNullOrWhiteSpace(entry.Value)) + { + LogIssue(entry.Key, "entry is empty"); + hasObjectIssues = true; + continue; + } + + // require core fields + string[] fields = entry.Value.Split('/'); + if (fields.Length < SObject.objectInfoDescriptionIndex + 1) + { + LogIssue(entry.Key, "too few fields for an object"); + hasObjectIssues = true; + continue; + } + + // check min length for specific types + switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) + { + case "Cooking": + if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) + { + LogIssue(entry.Key, "too few fields for a cooking item"); + hasObjectIssues = true; + } + break; + } + } + + // log error + if (hasObjectIssues) + { + issuesFound = true; + this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); + } + } + + return !issuesFound; + } + + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. + /// The mods to include in the update check (if eligible). + private void CheckForUpdatesAsync(IModMetadata[] mods) + { + if (!this.Settings.CheckForUpdates) + return; + + new Thread(() => + { + // create client + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + + // check SMAPI version + try + { + this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); + + ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; + if (response.Error != null) + { + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {response.Error}"); + } + else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) + this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); + else + this.VerboseLog(" OK."); + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {ex.GetLogSummary()}"); + } + + // check mod versions + try + { + // log issues + if (this.Settings.VerboseLogging) + { + this.VerboseLog("Validating mod update keys..."); + foreach (IModMetadata mod in mods) + { + if (mod.Manifest == null) + this.VerboseLog($" {mod.DisplayName}: no manifest."); + else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) + this.VerboseLog($" {mod.DisplayName}: no update keys."); + } + } + + // prepare update keys + Dictionary modsByKey = + ( + from mod in mods + where mod.Manifest?.UpdateKeys != null + from key in mod.Manifest.UpdateKeys + select new { key, mod } + ) + .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.Select(p => p.mod).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + + // fetch results + this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); + var results = + ( + from entry in client.GetModInfo(modsByKey.Keys.ToArray()) + from mod in modsByKey[entry.Key] + orderby mod.DisplayName + select new { entry.Key, Mod = mod, Info = entry.Value } + ) + .ToArray(); + + // extract latest versions + IDictionary updatesByMod = new Dictionary(); + foreach (var result in results) + { + IModMetadata mod = result.Mod; + ModInfoModel info = result.Info; + + // handle error + if (info.Error != null) + { + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); + continue; + } + + // track update + ISemanticVersion localVersion = mod.DataRecord != null + ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) + : mod.Manifest.Version; + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null + ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) + : info.Version + ); + bool isUpdate = latestVersion.IsNewerThan(localVersion); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); + if (isUpdate) + { + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) + updatesByMod[mod] = info; + } + } + + // output + if (updatesByMod.Any()) + { + this.Monitor.Newline(); + this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) + this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); + } + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); + } + }).Start(); + } + + /// Create a directory path if it doesn't exist. + /// The directory path. + private void VerifyPath(string path) + { + try + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + /// Load and hook up the given mods. + /// The mods to load. + /// The JSON helper with which to read mods' JSON files. + /// The content manager to use for mod content. + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) + { + this.Monitor.Log("Loading mods...", LogLevel.Trace); + + // load mod assemblies + IDictionary skippedMods = new Dictionary(); + { + void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + foreach (IModMetadata metadata in mods) + { + // get basic info + IManifest manifest = metadata.Manifest; + string assemblyPath = metadata.Manifest?.EntryDll != null + ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) + : null; + this.Monitor.Log(assemblyPath != null + ? $"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}..." + : $"Loading {metadata.DisplayName}...", LogLevel.Trace); + + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } + + // preprocess & load mod assembly + Assembly modAssembly; + try + { + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); + } + catch (IncompatibleInstructionException ex) + { + TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); + continue; + } + catch (SAssemblyLoadFailedException ex) + { + TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}"); + continue; + } + catch (Exception ex) + { + TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); + continue; + } + + // validate assembly + try + { + int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); + if (modEntries == 0) + { + TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); + continue; + } + if (modEntries > 1) + { + TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); + continue; + } + } + catch (Exception ex) + { + TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); + continue; + } + + // initialise mod + try + { + // get implementation + TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); + Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); + if (mod == null) + { + TrackSkip(metadata, "its entry class couldn't be instantiated."); + continue; + } + + // inject data + { + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + + mod.ModManifest = manifest; + mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); + mod.Monitor = monitor; + } + + // track mod + metadata.SetMod(mod); + this.ModRegistry.Add(metadata); + } + catch (Exception ex) + { + TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); + } + } + } + IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); + + // log skipped mods + this.Monitor.Newline(); + if (skippedMods.Any()) + { + this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); + foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) + { + IModMetadata mod = pair.Key; + string reason = pair.Value; + + if (mod.Manifest?.Version != null) + this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); + else + this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); + } + this.Monitor.Newline(); + } + + // log loaded mods + this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + + // initialise translations + this.ReloadTranslations(); + + // initialise loaded mods + foreach (IModMetadata metadata in loadedMods) + { + // add interceptors + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; + this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; + } + + // call entry method + try + { + IMod mod = metadata.Mod; + mod.Entry(mod.Helper); + } + catch (Exception ex) + { + this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) + foreach (IModMetadata metadata in loadedMods) + { + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => + { + if (e.NewItems.Count > 0) + { + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); + } + }; + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => + { + if (e.NewItems.Count > 0) + { + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); + } + }; + } + } + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(editors, loaders); + } + } + + /// Reload translations for all mods. + private void ReloadTranslations() + { + JsonHelper jsonHelper = new JsonHelper(); + foreach (IModMetadata metadata in this.ModRegistry.GetMods()) + { + // read translation files + IDictionary> translations = new Dictionary>(); + DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); + if (translationsDir.Exists) + { + foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + { + string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); + try + { + translations[locale] = jsonHelper.ReadJsonFile>(file.FullName); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); + } + } + } + + // update translation + TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; + translationHelper.SetTranslations(translations); + } + } + + /// The method called when the user submits a core SMAPI command in the console. + /// The command name. + /// The command arguments. + private void HandleCommand(string name, string[] arguments) + { + switch (name) + { + case "help": + if (arguments.Any()) + { + Command result = this.CommandManager.Get(arguments[0]); + if (result == null) + this.Monitor.Log("There's no command with that name.", LogLevel.Error); + else + this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); + } + else + { + string message = "The following commands are registered:\n"; + IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); + foreach (var group in groups) + { + string modName = group.Key; + string[] commandNames = group.ToArray(); + message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; + } + message += "For more information about a command, type 'help command_name'."; + + this.Monitor.Log(message, LogLevel.Info); + } + break; + + case "reload_i18n": + this.ReloadTranslations(); + this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); + break; + + default: + throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); + } + } + + /// Redirect messages logged directly to the console to the given monitor. + /// The monitor with which to log messages. + /// The message to log. + private void HandleConsoleMessage(IMonitor monitor, string message) + { + LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; // intercept potential exceptions + monitor.Log(message, level); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + private void PressAnyKeyToExit() + { + this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + Program.PressAnyKeyToExit(showMessage: false); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + /// Whether to print a 'press any key to exit' message to the console. + private static void PressAnyKeyToExit(bool showMessage) + { + if (showMessage) + Console.WriteLine("Game has ended. Press any key to exit."); + Thread.Sleep(100); + Console.ReadKey(); + Environment.Exit(0); + } + + /// Get a monitor instance derived from SMAPI's current settings. + /// The name of the module which will log messages with this instance. + private Monitor GetSecondaryMonitor(string name) + { + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + { + WriteToConsole = this.Monitor.WriteToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; + } + + /// Get a human-readable name for the current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + private string GetFriendlyPlatformName() + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + return Environment.OSVersion.ToString(); + } + + /// Log a message if verbose mode is enabled. + /// The message to log. + private void VerboseLog(string message) + { + if (this.Settings.VerboseLogging) + this.Monitor.Log(message, LogLevel.Trace); + } + } +} diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b0a065f5 --- /dev/null +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Stardew Modding API (SMAPI)")] +[assembly: AssemblyDescription("A modding API for Stardew Valley.")] +[assembly: Guid("5c3f7f42-fefd-43db-aaea-92ea3bcad531")] +[assembly: InternalsVisibleTo("StardewModdingAPI.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs new file mode 100644 index 00000000..1b99dae6 --- /dev/null +++ b/src/SMAPI/SemanticVersion.cs @@ -0,0 +1,237 @@ +using System; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public class SemanticVersion : ISemanticVersion + { + /********* + ** Properties + *********/ + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + private static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + public int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + public int PatchVersion { get; } + + /// An optional build tag. + public string Build { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional build tag. + [JsonConstructor] + public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) + { + this.MajorVersion = majorVersion; + this.MinorVersion = minorVersion; + this.PatchVersion = patchVersion; + this.Build = this.GetNormalisedTag(build); + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersion(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersion.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.MajorVersion = int.Parse(match.Groups["major"].Value); + this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + public int CompareTo(ISemanticVersion other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.MajorVersion != other.MajorVersion) + return this.MajorVersion.CompareTo(other.MajorVersion); + if (this.MinorVersion != other.MinorVersion) + return this.MinorVersion.CompareTo(other.MinorVersion); + if (this.PatchVersion != other.PatchVersion) + return this.PatchVersion.CompareTo(other.PatchVersion); + if (this.Build == other.Build) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Build); + bool otherIsStable = string.IsNullOrWhiteSpace(other.Build); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Build.Split('.', '-'); + string[] otherParts = other.Build.Split('.', '-'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + public bool IsOlderThan(ISemanticVersion other) + { + return this.CompareTo(other) < 0; + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsOlderThan(string other) + { + return this.IsOlderThan(new SemanticVersion(other)); + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + public bool IsNewerThan(ISemanticVersion other) + { + return this.CompareTo(other) > 0; + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsNewerThan(string other) + { + return this.IsNewerThan(new SemanticVersion(other)); + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + public bool IsBetween(ISemanticVersion min, ISemanticVersion max) + { + return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + public bool IsBetween(string min, string max) + { + return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(ISemanticVersion other) + { + return other != null && this.CompareTo(other) == 0; + } + + /// Get a string representation of the version. + public override string ToString() + { + // version + string result = this.PatchVersion != 0 + ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" + : $"{this.MajorVersion}.{this.MinorVersion}"; + + // tag + string tag = this.Build; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out ISemanticVersion parsed) + { + try + { + parsed = new SemanticVersion(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation + return null; + return tag; + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json new file mode 100644 index 00000000..ebc1235b --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -0,0 +1,2063 @@ +/* + + + +This file contains advanced configuration for SMAPI. You generally shouldn't change this file. + + + +*/ +{ + /** + * Whether to enable features intended for mod developers. Currently this only makes TRACE-level + * messages appear in the console. + */ + "DeveloperMode": true, + + /** + * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new + * versions are available, an alert will be shown in the console. This doesn't affect the load + * time even if your connection is offline or slow, because it happens in the background. + */ + "CheckForUpdates": true, + + /** + * SMAPI's GitHub project name, used to perform update checks. + */ + "GitHubProjectName": "Pathoschild/SMAPI", + + /** + * The base URL for SMAPI's web API, used to perform update checks. + * Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the + * game's bundled Mono. + */ + "WebApiBaseUrl": "https://api.smapi.io", + + /** + * Whether SMAPI should log more information about the game context. + */ + "VerboseLogging": false, + + /** + * Extra metadata about some SMAPI mods. All fields except 'ID' are optional. + * + * - 'ID' uniquely identifies the mod across all versions, even if its manifest fields changed or + * the mod doesn't have a unique ID. The format is as follows: + * - If the mod's identifier changed over time, multiple variants are separated by |. + * - Each variant can take one of two forms: a simple string matching the mod's UniqueID, + * or a JSON structure containing any of three manifest fields (ID, Name, and Author) to + * match. + * + * - 'UpdateKeys' specifies the value of the equivalent manifest field if it's not already set. + * This is used to enable update checks for older mods that haven't been updated to use it yet. + * + * - 'AlternativeUrl' specifies a URL where the player can find an unofficial update or + * alternative if the mod is no longer compatible. + * + * - 'Compatibility' overrides SMAPI's normal compatibility detection. The keys are version + * ranges in the form lower~upper, where either side can be blank for an unbounded range. (For + * example, "~1.0" means all versions up to 1.0 inclusively.) The values have two fields: + * - 'Status' specifies the compatibility. Valid values are Obsolete (SMAPI won't load it + * because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will load it even + * if it detects incompatible code). + * - 'ReasonPhrase' (optional) specifies a message to show to the player explaining why the + * mod isn't loaded. This has no effect for AssumeCompatible. + * + * - 'MapLocalVersions' and 'MapRemoteVersions' substitute versions for update checks. For + * example, if the API returns version '1.1-1078', MapRemoteVersions can map it to '1.1' when + * comparing to the mod's current version. This is only intended to support legacy mods with + * injected update keys. + */ + "ModData": [ + { + // AccessChestAnywhere + "ID": "AccessChestAnywhere", + "UpdateKeys": [ "Nexus:257" ], + "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 + }, + "MapLocalVersions": { + "1.1-1078": "1.1" + } + }, + { + // AdjustArtisanPrices + "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", + "UpdateKeys": [ "Chucklefish:3532" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 + } + }, + { + // Adjust Monster + "ID": "mmanlapat.AdjustMonster", + "UpdateKeys": [ "Nexus:1161" ] + }, + { + // Advanced Location Loader + "ID": "Entoarox.AdvancedLocationLoader", + //"UpdateKeys": [ "Chucklefish:3619" ], // Entoarox opted out of mod update checks + "Compatibility": { + "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Adventure Shop Inventory + "ID": "HammurabiAdventureShopInventory", + "UpdateKeys": [ "Chucklefish:4608" ] + }, + { + // AgingMod + "ID": "skn.AgingMod", + "UpdateKeys": [ "Nexus:1129" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // All Crops All Seasons + "ID": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 + "UpdateKeys": [ "Nexus:170" ] + }, + { + // All Professions + "ID": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 + "UpdateKeys": [ "Nexus:174" ] + }, + { + // Almighty Tool + "ID": "AlmightyTool.dll | 439", // changed in 1.2.1 + "UpdateKeys": [ "Nexus:439" ], + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapRemoteVersions": { + "1.21": "1.2.1" + } + }, + { + // Animal Mood Fix + "ID": "GPeters-AnimalMoodFix", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + } + } + }, + { + // Animal Sitter + "ID": "AnimalSitter.dll | jwdred.AnimalSitter", // changed in 1.0.9 + "UpdateKeys": [ "Nexus:581" ], + "Compatibility": { + "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // A Tapper's Dream + "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", + "UpdateKeys": [ "Nexus:260" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Auto Animal Doors + "ID": "AaronTaggart.AutoAnimalDoors", + "UpdateKeys": [ "Nexus:1019" ], + "MapRemoteVersions": { + "1.1.1": "1.1" // manifest not updated + } + }, + { + // Auto-Eat + "ID": "BALANCEMOD_AutoEat | Permamiss.AutoEat", // changed in 1.1.1 + "UpdateKeys": [ "Nexus:643" ] + }, + { + // AutoGate + "ID": "AutoGate", + "UpdateKeys": [ "Nexus:820" ] + }, + { + // Automate + "ID": "Pathoschild.Automate", + "UpdateKeys": [ "Nexus:1063" ] + }, + { + // Automated Doors + "ID": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b | azah.automated-doors", // changed in 1.4.1 + "UpdateKeys": [ "GitHub:azah/AutomatedDoors" ], + "MapLocalVersions": { + "1.4.1-1": "1.4.1" + } + }, + { + // AutoSpeed + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'} | Omegasis.AutoSpeed", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "UpdateKeys": [ "Nexus:443" ] + }, + { + // Basic Sprinkler Improved + "ID": "lrsk_sdvm_bsi.0117171308", + "UpdateKeys": [ "Nexus:833" ], + "MapRemoteVersions": { + "1.0.2": "1.0.1-release" // manifest not updated + } + }, + { + // Better Hay + "ID": "cat.betterhay", + "UpdateKeys": [ "Nexus:1430" ] + }, + { + // Better Quality More Seasons + "ID": "SB_BQMS", + "UpdateKeys": [ "Nexus:935" ] + }, + { + // Better Quarry + "ID": "BetterQuarry", + "UpdateKeys": [ "Nexus:771" ] + }, + { + // Better Ranching + "ID": "BetterRanching", + "UpdateKeys": [ "Nexus:859" ] + }, + { + // Better Shipping Box + "ID": "Kithio:BetterShippingBox", + "UpdateKeys": [ "Chucklefish:4302" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.0.1": "1.0.2" + } + }, + { + // Better Sprinklers + "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 + "UpdateKeys": [ "Nexus:41" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Billboard Anywhere + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'} | Omegasis.BillboardAnywhere", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "UpdateKeys": [ "Nexus:492" ] + }, + { + // Birthday Mail + "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d | KathrynHazuka.BirthdayMail", // changed in 1.2.3-pathoschild-update + "UpdateKeys": [ "Nexus:276" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Breed Like Rabbits + "ID": "dycedarger.breedlikerabbits", + "UpdateKeys": [ "Nexus:948" ] + }, + { + // Build Endurance + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'} | Omegasis.BuildEndurance", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "UpdateKeys": [ "Nexus:445" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Build Health + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'} | Omegasis.BuildHealth", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "UpdateKeys": [ "Nexus:446" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Buy Cooking Recipes + "ID": "Denifia.BuyRecipes", + "UpdateKeys": [ "Nexus:1126" ], // added in 1.0.1 (2017-10-04) + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Buy Back Collectables + "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 + "UpdateKeys": [ "Nexus:507" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Carry Chest + "ID": "spacechase0.CarryChest", + "UpdateKeys": [ "Nexus:1333" ] + }, + { + // Casks Anywhere + "ID": "CasksAnywhere", + "UpdateKeys": [ "Nexus:878" ], + "MapLocalVersions": { + "1.1-alpha": "1.1" + } + }, + { + // Categorize Chests + "ID": "CategorizeChests", + "UpdateKeys": [ "Nexus:1300" ] + }, + { + // ChefsCloset + "ID": "Duder.ChefsCloset", + "UpdateKeys": [ "Nexus:1030" ], + "MapLocalVersions": { + "1.3-1": "1.3" + } + }, + { + // Chest Label System + "ID": "SPDChestLabel | Speeder.ChestLabel", // changed in 1.5.1-pathoschild-update + "UpdateKeys": [ "Nexus:242" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } + }, + { + // Chest Pooling + "ID": "ChestPooling.dll | mralbobo.ChestPooling", // changed in 1.3 + "UpdateKeys": [ "GitHub:mralbobo/stardew-chest-pooling" ], + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Chests Anywhere + "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 + "UpdateKeys": [ "Nexus:518" ], + "Compatibility": { + "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Choose Baby Gender + "ID": "ChooseBabyGender.dll", + "UpdateKeys": [ "Nexus:590" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // CJB Automation + "ID": "CJBAutomation", + "UpdateKeys": [ "Nexus:211" ], + "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // CJB Cheats Menu + "ID": "CJBCheatsMenu | CJBok.CheatsMenu", // changed in 1.14 + "UpdateKeys": [ "Nexus:4" ], + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } + }, + { + // CJB Item Spawner + "ID": "CJBItemSpawner | CJBok.ItemSpawner", // changed in 1.7 + "UpdateKeys": [ "Nexus:93" ], + "Compatibility": { + "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } + }, + { + // CJB Show Item Sell Price + "ID": "CJBShowItemSellPrice | CJBok.ShowItemSellPrice", // changed in 1.7 + "UpdateKeys": [ "Nexus:5" ], + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Clean Farm + "ID": "tstaples.CleanFarm", + "UpdateKeys": [ "Nexus:794" ] + }, + { + // Climates of Ferngill + "ID": "KoihimeNakamura.ClimatesOfFerngill", + "UpdateKeys": [ "Nexus:604" ], + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Cold Weather Haley + "ID": "LordXamon.ColdWeatherHaleyPRO", + "UpdateKeys": [ "Nexus:1169" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Colored Chests + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." + } + } + }, + { + // Combat with Farm Implements + "ID": "SPDFarmingImplementsInCombat", + "UpdateKeys": [ "Nexus:313" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Community Bundle Item Tooltip + "ID": "musbah.bundleTooltip", + "UpdateKeys": [ "Nexus:1329" ] + }, + { + // Concentration on Farming + "ID": "punyo.ConcentrationOnFarming", + "UpdateKeys": [ "Nexus:1445" ] + }, + { + // Configurable Machines + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "UpdateKeys": [ "Nexus:280" ], + "MapLocalVersions": { + "1.2-beta": "1.2" + } + }, + { + // Configurable Shipping Dates + "ID": "ConfigurableShippingDates", + "UpdateKeys": [ "Nexus:675" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Cooking Skill + "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 + "UpdateKeys": [ "Nexus:522" ], + "Compatibility": { + "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // CrabNet + "ID": "CrabNet.dll | jwdred.CrabNet", // changed in 1.0.5 + "UpdateKeys": [ "Nexus:584" ], + "Compatibility": { + "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Current Location + "ID": "CurrentLocation102120161203", + "UpdateKeys": [ "Nexus:638" ] + }, + { + // Custom Critters + "ID": "spacechase0.CustomCritters", + "UpdateKeys": [ "Nexus:1255" ] + }, + { + // Custom Element Handler + "ID": "Platonymous.CustomElementHandler", + "UpdateKeys": [ "Nexus:1068" ] // added in 1.3.1 + }, + { + // Custom Farming + "ID": "Platonymous.CustomFarming", + "UpdateKeys": [ "Nexus:991" ] // added in 0.6.1 + }, + { + // Custom Farming Automate Bridge + "ID": "Platonymous.CFAutomate", + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // no longer compatible with Automate + } + }, + { + // Custom Farm Types + "ID": "spacechase0.CustomFarmTypes", + "UpdateKeys": [ "Nexus:1140" ] + }, + { + // Custom Furniture + "ID": "Platonymous.CustomFurniture", + "UpdateKeys": [ "Nexus:1254" ] // added in 0.4.1 + }, + { + // Customize Exterior + "ID": "CustomizeExterior | spacechase0.CustomizeExterior", // changed in 1.0.3 + "UpdateKeys": [ "Nexus:1099" ], + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Customizable Cart Redux + "ID": "KoihimeNakamura.CCR", + "UpdateKeys": [ "Nexus:1402" ], + "MapLocalVersions": { + "1.1-20170917": "1.1" + } + }, + { + // Customizable Traveling Cart Days + "ID": "TravelingCartYyeahdude", + "UpdateKeys": [ "Nexus:567" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Custom Linens + "ID": "Mevima.CustomLinens", + "UpdateKeys": [ "Nexus:1027" ], + "MapRemoteVersions": { + "1.1": "1.0" // manifest not updated + } + }, + { + // Custom Shops Redux + "ID": "Omegasis.CustomShopReduxGui", + "UpdateKeys": [ "Nexus:1378" ] + }, + { + // Custom TV + "ID": "Platonymous.CustomTV", + "UpdateKeys": [ "Nexus:1139" ] // added in 1.0.6 + }, + { + // Daily Luck Message + "ID": "Schematix.DailyLuckMessage", + "UpdateKeys": [ "Nexus:1327" ] + }, + { + // Daily News + "ID": "bashNinja.DailyNews", + "UpdateKeys": [ "Nexus:1141" ], + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Daily Quest Anywhere + "ID": "DailyQuest | Omegasis.DailyQuestAnywhere", // changed in 1.4 + "UpdateKeys": [ "Nexus:513" ] + }, + { + // Debug Mode + "ID": "Pathoschild.Stardew.DebugMode | Pathoschild.DebugMode", // changed in 1.4 + "UpdateKeys": [ "Nexus:679" ] + }, + { + // Dynamic Checklist + "ID": "gunnargolf.DynamicChecklist", + "UpdateKeys": [ "Nexus:1145" ], // added in 1.0.1-pathoschild-update + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Dynamic Horses + "ID": "Bpendragon-DynamicHorses", + "UpdateKeys": [ "Nexus:874" ], + "MapRemoteVersions": { + "1.2": "1.1-release" // manifest not updated + } + }, + { + // Dynamic Machines + "ID": "DynamicMachines", + "UpdateKeys": [ "Nexus:374" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.1": "1.1.1" + } + }, + { + // Dynamic NPC Sprites + "ID": "BashNinja.DynamicNPCSprites", + "UpdateKeys": [ "Nexus:1183" ] + }, + { + // Easier Farming + "ID": "cautiouswafffle.EasierFarming", + "UpdateKeys": [ "Nexus:1426" ] + }, + { + // Empty Hands + "ID": "QuicksilverFox.EmptyHands", + "UpdateKeys": [ "Nexus:1176" ], // added in 1.0.1-pathoschild-update + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Enemy Health Bars + "ID": "SPDHealthBar | Speeder.HealthBars", // changed in 1.7.1-pathoschild-update + "UpdateKeys": [ "Nexus:193" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Entoarox Framework + "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? + //"UpdateKeys": [ "Chucklefish:4228" ], // Entoarox opted out of mod update checks + "Compatibility": { + "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Expanded Fridge / Dynamic Expanded Fridge + "ID": "Uwazouri.ExpandedFridge", + "UpdateKeys": [ "Nexus:1191" ] + }, + { + // Experience Bars + "ID": "ExperienceBars | spacechase0.ExperienceBars", // changed in 1.0.2 + "UpdateKeys": [ "Nexus:509" ] + }, + { + // Extended Bus System + "ID": "ExtendedBusSystem", + "UpdateKeys": [ "Chucklefish:4373" ] + }, + { + // Extended Fridge + "ID": "Mystra007ExtendedFridge | Crystalmir.ExtendedFridge", // changed in 1.0.1 + "UpdateKeys": [ "Nexus:485" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Extended Greenhouse + "ID": "ExtendedGreenhouse", + "UpdateKeys": [ "Chucklefish:4303" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Extended Minecart + "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 + //"UpdateKeys": [ "Chucklefish:4359" ] // Entoarox opted out of mod update checks + }, + { + // Extended Reach + "ID": "spacechase0.ExtendedReach", + "UpdateKeys": [ "Nexus:1493" ] + }, + { + // Fall 28 Snow Day + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "UpdateKeys": [ "Nexus:486" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Farm Automation: Barn Door Automation + "ID": "FarmAutomation.BarnDoorAutomation.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Farm Automation: Item Collector + "ID": "FarmAutomation.ItemCollector.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Farm Automation Unofficial: Item Collector + "ID": "Maddy99.FarmAutomation.ItemCollector", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Farm Expansion + "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5 | Advize.FarmExpansion", // changed in 2.0, 2.0.5, and 3.0 + "UpdateKeys": [ "Nexus:130" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Farm Resource Generator + "ID": "FarmResourceGenerator.dll", + "UpdateKeys": [ "Nexus:647" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Fast Animations + "ID": "Pathoschild.FastAnimations", + "UpdateKeys": [ "Nexus:1089" ] + }, + { + // Faster Paths + "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths" // changed in 1.2 and 1.3; disambiguate from Shop Expander + // "UpdateKeys": [ "Chucklefish:3641" ] // Entoarox opted out of mod update checks + }, + { + // Faster Run + "ID": "FasterRun.dll | KathrynHazuka.FasterRun", // changed in 1.1.1-pathoschild-update + "UpdateKeys": [ "Nexus:733" ], // added in 1.1.1-pathoschild-update + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Fishing Adjust + "ID": "shuaiz.FishingAdjustMod", + "UpdateKeys": [ "Nexus:1350" ] + }, + { + // Fishing Tuner Redux + "ID": "HammurabiFishingTunerRedux", + "UpdateKeys": [ "Chucklefish:4578" ] + }, + { + // FlorenceMod + "ID": "FlorenceMod.dll", + "UpdateKeys": [ "Nexus:591" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.0.1": "1.1" + } + }, + { + // Flower Color Picker + "ID": "spacechase0.FlowerColorPicker", + "UpdateKeys": [ "Nexus:1229" ] + }, + { + // Forage at the Farm + "ID": "ForageAtTheFarm", + "UpdateKeys": [ "Nexus:673" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Furniture Anywhere + "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere" // changed in 1.1; disambiguate from Extended Minecart + // "UpdateKeys": [ "Chucklefish:4324" ] // Entoarox opted out of mod update checks + }, + { + // Game Reminder + "ID": "mmanlapat.GameReminder", + "UpdateKeys": [ "Nexus:1153" ] + }, + { + // Gate Opener + "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 + "UpdateKeys": [ "GitHub:mralbobo/stardew-gate-opener" ], + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // GenericShopExtender + "ID": "GenericShopExtender", + "UpdateKeys": [ "Nexus:814" ], // added in 0.1.3 + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Geode Info Menu + "ID": "cat.geodeinfomenu", + "UpdateKeys": [ "Nexus:1448" ] + }, + { + // Get Dressed + "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 + "UpdateKeys": [ "Nexus:331" ], + "Compatibility": { + "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Giant Crop Ring + "ID": "cat.giantcropring", + "UpdateKeys": [ "Nexus:1182" ] + }, + { + // Gift Taste Helper + "ID": "8008db57-fa67-4730-978e-34b37ef191d6 | tstaples.GiftTasteHelper", // changed in 2.5 + "UpdateKeys": [ "Nexus:229" ], + "Compatibility": { + "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Grandfather's Gift + "ID": "ShadowDragon.GrandfathersGift", + "UpdateKeys": [ "Nexus:985" ] + }, + { + // Happy Animals + "ID": "HappyAnimals", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Happy Birthday (Omegasis) + "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'} | Omegasis.HappyBirthday", // changed in 1.4; disambiguate from Oxyligen's fork + "UpdateKeys": [ "Nexus:520" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Happy Birthday (Oxyligen fork) + "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork + "UpdateKeys": [ "Nexus:1064" ] + }, + { + // Harp of Yoba Redux + "ID": "Platonymous.HarpOfYobaRedux", + "UpdateKeys": [ "Nexus:914" ] // added in 2.0.3 + }, + { + // Harvest Moon Witch Princess + "ID": "Sasara.WitchPrincess", + "UpdateKeys": [ "Nexus:1157" ] + }, + { + // Harvest With Scythe + "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", + "UpdateKeys": [ "Nexus:236" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Horse Whistle (icepuente) + "ID": "icepuente.HorseWhistle", + "UpdateKeys": [ "Nexus:1131" ] + }, + { + // Hunger (Yyeadude) + "ID": "HungerYyeadude", + "UpdateKeys": [ "Nexus:613" ] + }, + { + // Hunger for Food (Tigerle) + "ID": "HungerForFoodByTigerle", + "UpdateKeys": [ "Nexus:810" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Hunger Mod (skn) + "ID": "skn.HungerMod", + "UpdateKeys": [ "Nexus:1127" ], + "MapRemoteVersions": { + "1.2.1": "1.0" // manifest not updated + } + }, + { + // Idle Pause + "ID": "Veleek.IdlePause", + "UpdateKeys": [ "Nexus:1092" ], + "MapRemoteVersions": { + "1.2": "1.1" // manifest not updated + } + }, + { + // Improved Quality of Life + "ID": "Demiacle.ImprovedQualityOfLife", + "UpdateKeys": [ "Nexus:1025" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Instant Geode + "ID": "InstantGeode", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Instant Grow Trees + "ID": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 + "UpdateKeys": [ "Nexus:173" ] + }, + { + // Interaction Helper + "ID": "HammurabiInteractionHelper", + "UpdateKeys": [ "Chucklefish:4640" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Item Auto Stacker + "ID": "cat.autostacker", + "UpdateKeys": [ "Nexus:1184" ], + "MapRemoteVersions": { + "1.0.1": "1.0" // manifest not updated + } + }, + { + // Junimo Farm + "ID": "Platonymous.JunimoFarm", + "UpdateKeys": [ "Nexus:984" ], // added in 1.1.3 + "MapRemoteVersions": { + "1.1.2": "1.1.1" // manifest not updated + } + }, + { + // Less Strict Over-Exertion (AntiExhaustion) + "ID": "BALANCEMOD_AntiExhaustion", + "UpdateKeys": [ "Nexus:637" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "0.0": "1.1" + } + }, + { + // Level Extender + "ID": "Devin Lematty.Level Extender", + "UpdateKeys": [ "Nexus:1471" ], + "MapRemoteVersions": { + "1.1": "1.0" // manifest not updated + } + }, + { + // Level Up Notifications + "ID": "Level Up Notifications", + "UpdateKeys": [ "Nexus:855" ] + }, + { + // Location and Music Logging + "ID": "Brandy Lover.LMlog", + "UpdateKeys": [ "Nexus:1366" ] + }, + { + // Longevity + "ID": "RTGOAT.Longevity", + "UpdateKeys": [ "Nexus:649" ] + }, + { + // Lookup Anything + "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 + "UpdateKeys": [ "Nexus:541" ], + "Compatibility": { + "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Love Bubbles + "ID": "LoveBubbles", + "UpdateKeys": [ "Nexus:1318" ] + }, + { + // Loved Labels + "ID": "LovedLabels.dll", + "UpdateKeys": [ "Nexus:279" ], + "Compatibility": { + "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Luck Skill + "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 + "UpdateKeys": [ "Nexus:521" ], + "Compatibility": { + "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // MailOrderPigs + "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 + "UpdateKeys": [ "Nexus:632" ], + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Makeshift Multiplayer + "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 + "UpdateKeys": [ "Nexus:501" ], + "Compatibility": { + "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Map Image Exporter + "ID": "MapImageExporter | spacechase0.MapImageExporter", // changed in 1.0.2 + "UpdateKeys": [ "Nexus:1073" ] + }, + { + // Message Box [API]? (ChatMod) + "ID": "Kithio:ChatMod", + "UpdateKeys": [ "Chucklefish:4296" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Mining at the Farm + "ID": "MiningAtTheFarm", + "UpdateKeys": [ "Nexus:674" ] + }, + { + // Mining With Explosives + "ID": "MiningWithExplosives", + "UpdateKeys": [ "Nexus:770" ] + }, + { + // Modder Serialization Utility + "ID": "SerializerUtils-0-1", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "it's no longer maintained or used." + } + } + }, + { + // More Artifact Spots + "ID": "451", + "UpdateKeys": [ "Nexus:451" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // More Map Layers + "ID": "Platonymous.MoreMapLayers", + "UpdateKeys": [ "Nexus:1134" ] // added in 1.1.1 + }, + { + // More Pets + "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 + // "UpdateKeys": [ "Chucklefish:4288" ], // Entoarox opted out of mod update checks + "Compatibility": { + "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility + } + }, + { + // More Rain + "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis + "UpdateKeys": [ "Nexus:441" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // More Weapons + "ID": "Joco80.MoreWeapons", + "UpdateKeys": [ "Nexus:1168" ] + }, + { + // Move Faster + "ID": "shuaiz.MoveFasterMod", + "UpdateKeys": [ "Nexus:1351" ] + }, + { + // Multiple Sprites and Portraits On Rotation (File Loading) + "ID": "FileLoading", + "UpdateKeys": [ "Nexus:1094" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.1": "1.12" + } + }, + { + // Museum Rearranger + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'} | Omegasis.MuseumRearranger", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "UpdateKeys": [ "Nexus:428" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // New Machines + "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", + "UpdateKeys": [ "Chucklefish:3683" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Night Owl + "UpdateKeys": [ "Nexus:433" ], + "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "2.1": "1.3" // 1.3 had wrong version in manifest + } + }, + { + // No Kids Ever + "ID": "Hangy.NoKidsEver", + "UpdateKeys": [ "Nexus:1464" ] + }, + { + // No Debug Mode + "ID": "NoDebugMode", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "debug mode was removed in SMAPI 1.0." + } + } + }, + { + // No Fence Decay + "ID": "cat.nofencedecay", + "UpdateKeys": [ "Nexus:1180" ] + }, + { + // No More Pets + "ID": "NoMorePets | Omegasis.NoMorePets", // changed in 1.4 + "UpdateKeys": [ "Nexus:506" ] + }, + { + // NoSoilDecay + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "UpdateKeys": [ "Nexus:237" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location + } + }, + { + // No Soil Decay Redux + "ID": "Platonymous.NoSoilDecayRedux", + "UpdateKeys": [ "Nexus:1084" ] // added in 1.1.9 + }, + { + // NPC Map Locations + "ID": "NPCMapLocationsMod", + "UpdateKeys": [ "Nexus:239" ], + "Compatibility": { + "1.42~1.43": { + "Status": "AssumeBroken", + "ReasonPhrase": "this version has an update check error which crashes the game." + } + } + }, + { + // NPC Speak + "ID": "NpcEcho.dll", + "UpdateKeys": [ "Nexus:694" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Object Time Left + "ID": "spacechase0.ObjectTimeLeft", + "UpdateKeys": [ "Nexus:1315" ] + }, + { + // OmniFarm + "ID": "BlueMod_OmniFarm | PhthaloBlue.OmniFarm", // changed in 2.0.2-pathoschild-update + "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_OmniFarm" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Out of Season Bonuses / Seasonal Items + "ID": "midoriarmstrong.seasonalitems", + "UpdateKeys": [ "Nexus:1452" ] + }, + { + // Part of the Community + "ID": "SB_PotC", + "UpdateKeys": [ "Nexus:923" ], + "Compatibility": { + "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // PelicanFiber + "ID": "PelicanFiber.dll | jwdred.PelicanFiber", // changed in 3.0.1 + "UpdateKeys": [ "Nexus:631" ], + "Compatibility": { + "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "3.0.2": "3.0.1" // didn't change manifest version + } + }, + { + // PelicanTTS + "ID": "Platonymous.PelicanTTS", + "UpdateKeys": [ "Nexus:1079" ], // added in 1.6.1 + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Persia the Mermaid - Standalone Custom NPC + "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", + "UpdateKeys": [ "Nexus:1419" ] + }, + { + // Persival's BundleMod + "ID": "BundleMod.dll", + "UpdateKeys": [ "Nexus:438" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } + }, + { + // Plant on Grass + "ID": "Demiacle.PlantOnGrass", + "UpdateKeys": [ "Nexus:1026" ] + }, + { + // Point-and-Plant + "ID": "PointAndPlant.dll | jwdred.PointAndPlant", // changed in 1.0.3 + "UpdateKeys": [ "Nexus:572" ], + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Pony Weight Loss Program + "ID": "BadNetCode.PonyWeightLossProgram", + "UpdateKeys": [ "Nexus:1232" ] + }, + { + // Portraiture + "ID": "Platonymous.Portraiture", + "UpdateKeys": [ "Nexus:999" ] // added in 1.3.1 + }, + { + // Prairie King Made Easy + "ID": "PrairieKingMadeEasy.dll | Mucchan.PrairieKingMadeEasy", // changed in 1.0.1 + "UpdateKeys": [ "Chucklefish:3594" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Quest Delay + "ID": "BadNetCode.QuestDelay", + "UpdateKeys": [ "Nexus:1239" ] + }, + { + // Rain Randomizer + "ID": "RainRandomizer.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Recatch Legendary Fish + "ID": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 + "UpdateKeys": [ "Nexus:172" ] + }, + { + // Regeneration + "ID": "HammurabiRegeneration", + "UpdateKeys": [ "Chucklefish:4584" ] + }, + { + // Relationship Bar UI + "ID": "RelationshipBar", + "UpdateKeys": [ "Nexus:1009" ] + }, + { + // RelationshipsEnhanced + "ID": "relationshipsenhanced", + "UpdateKeys": [ "Chucklefish:4435" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Relationship Status + "ID": "relationshipstatus", + "UpdateKeys": [ "Nexus:751" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "1.0.5": "1.0.4" // not updated in manifest + } + }, + { + // Rented Tools + "ID": "JarvieK.RentedTools", + "UpdateKeys": [ "Nexus:1307" ] + }, + { + // Replanter + "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 + "UpdateKeys": [ "Nexus:589" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // ReRegeneration + "ID": "lrsk_sdvm_rerg.0925160827", + "UpdateKeys": [ "Chucklefish:4465" ], + "MapLocalVersions": { + "1.1.2-release": "1.1.2" + } + }, + { + // Reseed + "ID": "Roc.Reseed", + "UpdateKeys": [ "Nexus:887" ] + }, + { + // Reusable Wallpapers and Floors (Wallpaper Retain) + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", + "UpdateKeys": [ "Nexus:356" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Ring of Fire + "ID": "Platonymous.RingOfFire", + "UpdateKeys": [ "Nexus:1166" ] // added in 1.0.1 + }, + { + // Rope Bridge + "ID": "RopeBridge", + "UpdateKeys": [ "Nexus:824" ] + }, + { + // Rotate Toolbar + "ID": "Pathoschild.RotateToolbar", + "UpdateKeys": [ "Nexus:1100" ] + }, + { + // Rush Orders + "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 + "UpdateKeys": [ "Nexus:605" ], + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Save Anywhere + "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl + "UpdateKeys": [ "Nexus:444" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "2.6": "2.5" // not updated in manifest + } + }, + { + // Save Backup + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'} | Omegasis.SaveBackup", // changed in 1.3; disambiguate from other Alpha_Omegasis mods + "UpdateKeys": [ "Nexus:435" ], + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Scroll to Blank + "ID": "caraxian.scroll.to.blank", + "UpdateKeys": [ "Chucklefish:4405" ] + }, + { + // Scythe Harvesting + "ID": "ScytheHarvesting | mmanlapat.ScytheHarvesting", // changed in 1.6 + "UpdateKeys": [ "Nexus:1106" ] + }, + { + // Seasonal Immersion + "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + // "UpdateKeys": [ "Chucklefish:4262" ], // Entoarox opted out of mod update checks + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Seed Bag + "ID": "Platonymous.SeedBag", + "UpdateKeys": [ "Nexus:1133" ] // added in 1.1.2 + }, + { + // Self Service + "ID": "JarvieK.SelfService", + "UpdateKeys": [ "Nexus:1304" ], + "MapRemoteVersions": { + "0.2.1": "0.2" // manifest not updated + } + }, + { + // Send Items + "ID": "Denifia.SendItems", + "UpdateKeys": [ "Nexus:1087" ], // added in 1.0.3 (2017-10-04) + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Shed Notifications (BuildingsNotifications) + "ID": "TheCroak.BuildingsNotifications", + "UpdateKeys": [ "Nexus:620" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Shenandoah Project + "ID": "Shenandoah Project", + "UpdateKeys": [ "Nexus:756" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "1.1.1": "1.1" // not updated in manifest + } + }, + { + // Ship Anywhere + "ID": "spacechase0.ShipAnywhere", + "UpdateKeys": [ "Nexus:1379" ] + }, + { + // Shipment Tracker + "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", + "UpdateKeys": [ "Nexus:321" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Shop Expander + "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + // "UpdateKeys": [ "Chucklefish:4381" ], // Entoarox opted out of mod update checks + "Compatibility": { + "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Showcase Mod + "ID": "Igorious.Showcase", + "UpdateKeys": [ "Chucklefish:4487" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "0.9-500": "0.9" + } + }, + { + // Shroom Spotter + "ID": "TehPers.ShroomSpotter", + "UpdateKeys": [ "Nexus:908" ] + }, + { + // Simple Crop Label + "ID": "SimpleCropLabel", + "UpdateKeys": [ "Nexus:314" ] + }, + { + // Simple Sound Manager + "ID": "Omegasis.SimpleSoundManager", + "UpdateKeys": [ "Nexus:1410" ] + }, + { + // Simple Sprinklers + "ID": "SimpleSprinkler.dll | tZed.SimpleSprinkler", // changed in 1.5 + "UpdateKeys": [ "Nexus:76" ], + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Siv's Marriage Mod + "ID": "6266959802", + "UpdateKeys": [ "Nexus:366" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) + }, + "MapLocalVersions": { + "0.0": "1.4" + } + }, + { + // Skill Prestige + "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b | alphablackwolf.skillPrestige", // changed circa 1.2.3 + "UpdateKeys": [ "Nexus:569" ], + "Compatibility": { + "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Skill Prestige: Cooking Adapter + "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63 | Alphablackwolf.CookingSkillPrestigeAdapter", // changed circa 1.1 + "UpdateKeys": [ "Nexus:569" ], + "Compatibility": { + "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "1.2.3": "1.1" // manifest not updated + } + }, + { + // Skip Intro + "ID": "SkipIntro | Pathoschild.SkipIntro", // changed in 1.4 + "UpdateKeys": [ "Nexus:533" ] + }, + { + // Skull Cavern Elevator + "ID": "SkullCavernElevator", + "UpdateKeys": [ "Nexus:963" ] + }, + { + // Skull Cave Saver + "ID": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 + "UpdateKeys": [ "Nexus:175" ] + }, + { + // Sleepy Eye + "ID": "spacechase0.SleepyEye", + "UpdateKeys": [ "Nexus:1152" ] + }, + { + // Slower Fence Decay + "ID": "SPDSlowFenceDecay | Speeder.SlowerFenceDecay", // changed in 0.5.2-pathoschild-update + "UpdateKeys": [ "Nexus:252" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Smart Mod + "ID": "KuroBear.SmartMod", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~2.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Solar Eclipse Event + "ID": "KoihimeNakamura.SolarEclipseEvent", + "UpdateKeys": [ "Nexus:897" ], + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.3-20170917": "1.3" + } + }, + { + // SpaceCore + "ID": "spacechase0.SpaceCore", + "UpdateKeys": [ "Nexus:1348" ] + }, + { + // Speedster + "ID": "Platonymous.Speedster", + "UpdateKeys": [ "Nexus:1102" ] // added in 1.3.1 + }, + { + // Sprinkler Range + "ID": "cat.sprinklerrange", + "UpdateKeys": [ "Nexus:1179" ], + "MapRemoteVersions": { + "1.0.1": "1.0" // manifest not updated + } + }, + { + // Sprinkles + "ID": "Platonymous.Sprinkles", + "UpdateKeys": [ "Chucklefish:4592" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Sprint and Dash + "ID": "SPDSprintAndDash", + "UpdateKeys": [ "Chucklefish:3531" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Sprint and Dash Redux + "ID": "lrsk_sdvm_sndr.0921161059 | littleraskol.SprintAndDashRedux", // changed in 1.3 + "UpdateKeys": [ "Chucklefish:4201" ] + }, + { + // Sprinting Mod + "ID": "a10d3097-b073-4185-98ba-76b586cba00c", + "UpdateKeys": [ "GitHub:oliverpl/SprintingMod" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapLocalVersions": { + "1.0": "2.1" // not updated in manifest + } + }, + { + // StackSplitX + "ID": "StackSplitX.dll | tstaples.StackSplitX", // changed circa 1.3.1 + "UpdateKeys": [ "Nexus:798" ], + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // StaminaRegen + "ID": "StaminaRegen.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Stardew Config Menu + "ID": "Juice805.StardewConfigMenu", + "UpdateKeys": [ "Nexus:1312" ] + }, + { + // Stardew Content Compatibility Layer (SCCL) + "ID": "SCCL", + "UpdateKeys": [ "Nexus:889" ], + "Compatibility": { + "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Stardew Editor Game Integration + "ID": "spacechase0.StardewEditor.GameIntegration", + "UpdateKeys": [ "Nexus:1298" ] + }, + { + // Stardew Notification + "ID": "stardewnotification", + "UpdateKeys": [ "GitHub:monopandora/StardewNotification" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Stardew Symphony + "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'} | Omegasis.StardewSymphony", // changed in 1.4; disambiguate other mods by Alpha_Omegasis + "UpdateKeys": [ "Nexus:425" ], + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // StarDustCore + "ID": "StarDustCore", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + } + } + }, + { + // Starting Money + "ID": "StartingMoney | mmanlapat.StartingMoney", // changed in 1.1 + "UpdateKeys": [ "Nexus:1138" ] + }, + { + // StashItemsToChest + "ID": "BlueMod_StashItemsToChest", + "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_StashItemsToChest" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Stephan's Lots of Crops + "ID": "stephansstardewcrops", + "UpdateKeys": [ "Chucklefish:4314" ], + "MapRemoteVersions": { + "1.41": "1.1" // manifest not updated + } + }, + { + // Stone Bridge Over Pond (PondWithBridge) + "ID": "PondWithBridge.dll", + "UpdateKeys": [ "Nexus:316" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "0.0": "1.0" + } + }, + { + // Stumps to Hardwood Stumps + "ID": "StumpsToHardwoodStumps", + "UpdateKeys": [ "Nexus:691" ] + }, + { + // Super Greenhouse Warp Modifier + "ID": "SuperGreenhouse", + "UpdateKeys": [ "Chucklefish:4334" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Swim Almost Anywhere / Swim Suit + "ID": "Platonymous.SwimSuit", + "UpdateKeys": [ "Nexus:1215" ] // added in 0.5.1 + }, + { + // Tainted Cellar + "ID": "TaintedCellar.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 + } + }, + { + // Tapper Ready + "ID": "skunkkk.TapperReady", + "UpdateKeys": [ "Nexus:1219" ] + }, + { + // Teh's Fishing Overhaul + "ID": "TehPers.FishingOverhaul", + "UpdateKeys": [ "Nexus:866" ] + }, + { + // Teleporter + "ID": "Teleporter", + "UpdateKeys": [ "Chucklefish:4374" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // The Long Night + "ID": "Pathoschild.TheLongNight", + "UpdateKeys": [ "Nexus:1369" ] + }, + { + // Three-heart Dance Partner + "ID": "ThreeHeartDancePartner", + "UpdateKeys": [ "Nexus:500" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // TimeFreeze + "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe", + "UpdateKeys": [ "Nexus:973" ] + }, + { + // Time Reminder + "ID": "KoihimeNakamura.TimeReminder", + "UpdateKeys": [ "Nexus:1000" ], + "MapLocalVersions": { + "1.0-20170314": "1.0.2" + } + }, + { + // TimeSpeed + "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis + "UpdateKeys": [ "Nexus:169" ], + "Compatibility": { + "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // TractorMod + "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "UpdateKeys": [ "Nexus:1401" ] + }, + { + // Tree Transplant + "ID": "TreeTransplant", + "UpdateKeys": [ "Nexus:1342" ] + }, + { + // UI Info Suite + "ID": "Cdaragorn.UiInfoSuite", + "UpdateKeys": [ "Nexus:1150" ] + }, + { + // UiModSuite + "ID": "Demiacle.UiModSuite", + "UpdateKeys": [ "Nexus:1023" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapLocalVersions": { + "0.5": "1.0" // not updated in manifest + } + }, + { + // Variable Grass + "ID": "dantheman999.VariableGrass", + "UpdateKeys": [ "GitHub:dantheman999301/StardewMods" ] + }, + { + // Vertical Toolbar + "ID": "SB_VerticalToolMenu", + "UpdateKeys": [ "Nexus:943" ] + }, + { + // WakeUp + "ID": "WakeUp.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Wallpaper Fix + "ID": "WallpaperFix.dll", + "UpdateKeys": [ "Chucklefish:4211" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // WarpAnimals + "ID": "Symen.WarpAnimals", + "UpdateKeys": [ "Nexus:1400" ] + }, + { + // Weather Controller + "ID": "WeatherController.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // What Farm Cave / WhatAMush + "ID": "WhatAMush", + "UpdateKeys": [ "Nexus:1097" ] + }, + { + // WHats Up + "ID": "wHatsUp", + "UpdateKeys": [ "Nexus:1082" ] + }, + { + // Wonderful Farm Life + "ID": "WonderfulFarmLife.dll", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 + } + }, + { + // XmlSerializerRetool + "ID": "XmlSerializerRetool.dll", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "it's no longer maintained or used." + } + } + }, + { + // Xnb Loader + "ID": "Entoarox.XnbLoader", + // "UpdateKeys": [ "Chucklefish:4506" ], // Entoarox opted out of mod update checks + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // zDailyIncrease + "ID": "zdailyincrease", + "UpdateKeys": [ "Chucklefish:4247" ], + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapRemoteVersions": { + "1.3.5": "1.3.4" // not updated in manifest + } + }, + { + // Zoom Out Extreme + "ID": "ZoomMod | RockinMods.ZoomMod", // changed circa 1.2.1 + "UpdateKeys": [ "Nexus:1326" ], + "Compatibility": { + "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Zoryn's Better RNG + "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Zoryn's Calendar Anywhere + "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Zoryn's Durable Fences + "ID": "56d3439c-7b9b-497e-9496-0c4890e8a00e | Zoryn.DurableFences", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] + }, + { + // Zoryn's Health Bars + "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Zoryn's Fishing Mod + "ID": "fa277b1f-265e-47c3-a84f-cd320cc74949 | Zoryn.FishingMod", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] + }, + { + // Zoryn's Junimo Deposit Anywhere + "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], + "Compatibility": { + "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Zoryn's Movement Mod + "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Zoryn's Regen Mod + "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + } + ] +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj new file mode 100644 index 00000000..c6ff75d1 --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -0,0 +1,277 @@ + + + + + Debug + x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} + Exe + Properties + StardewModdingAPI + StardewModdingAPI + v4.5 + 512 + false + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + x86 + false + DEBUG;TRACE + true + false + $(SolutionDir)\..\bin\Debug\SMAPI + $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml + true + + + x86 + false + $(SolutionDir)\..\bin\Release\SMAPI + $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml + TRACE + true + true + pdbonly + true + + + icon.ico + + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + True + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + True + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + True + + + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + + + + + + + True + + + True + + + + + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + Designer + + + Always + + + Always + + + + + + Always + + + + + False + Microsoft .NET Framework 4.5 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.AssemblyRewriters + + + + + \ No newline at end of file diff --git a/src/SMAPI/Translation.cs b/src/SMAPI/Translation.cs new file mode 100644 index 00000000..ce344f81 --- /dev/null +++ b/src/SMAPI/Translation.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI +{ + /// A translation string with a fluent API to customise it. + public class Translation + { + /********* + ** Properties + *********/ + /// The placeholder text when the translation is null or empty, where {0} is the translation key. + internal const string PlaceholderText = "(no translation:{0})"; + + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The locale for which the translation was fetched. + private readonly string Locale; + + /// The underlying translation text. + private readonly string Text; + + /// The value to return if the translations is undefined. + private readonly string Placeholder; + + + /********* + ** Accessors + *********/ + /// The original translation key. + public string Key { get; } + + + /********* + ** Public methods + *********/ + /// Construct an isntance. + /// The name of the relevant mod for error messages. + /// The locale for which the translation was fetched. + /// The translation key. + /// The underlying translation text. + internal Translation(string modName, string locale, string key, string text) + : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } + + /// Construct an isntance. + /// The name of the relevant mod for error messages. + /// The locale for which the translation was fetched. + /// The translation key. + /// The underlying translation text. + /// The value to return if the translations is undefined. + internal Translation(string modName, string locale, string key, string text, string placeholder) + { + this.ModName = modName; + this.Locale = locale; + this.Key = key; + this.Text = text; + this.Placeholder = placeholder; + } + + /// Throw an exception if the translation text is null or empty. + /// There's no available translation matching the requested key and locale. + public Translation Assert() + { + if (!this.HasValue()) + throw new KeyNotFoundException($"The '{this.ModName}' mod doesn't have a translation with key '{this.Key}' for the '{this.Locale}' locale or its fallbacks."); + return this; + } + + /// Replace the text if it's null or empty. If you set a null or empty value, the translation will show the fallback "no translation" placeholder (see if you want to disable that). Returns a new instance if changed. + /// The default value. + public Translation Default(string @default) + { + return this.HasValue() + ? this + : new Translation(this.ModName, this.Locale, this.Key, @default); + } + + /// Whether to return a "no translation" placeholder if the translation is null or empty. Returns a new instance. + /// Whether to return a placeholder. + public Translation UsePlaceholder(bool use) + { + return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); + } + + /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + /// The argument is null. + public Translation Tokens(object tokens) + { + if (string.IsNullOrWhiteSpace(this.Text) || tokens == null) + return this; + + // get dictionary of tokens + IDictionary tokenLookup = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + { + // from dictionary + if (tokens is IDictionary inputLookup) + { + foreach (DictionaryEntry entry in inputLookup) + { + string key = entry.Key?.ToString().Trim(); + if (key != null) + tokenLookup[key] = entry.Value?.ToString(); + } + } + + // from object properties + else + { + Type type = tokens.GetType(); + foreach (PropertyInfo prop in type.GetProperties()) + tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString(); + foreach (FieldInfo field in type.GetFields()) + tokenLookup[field.Name] = field.GetValue(tokens)?.ToString(); + } + } + + // format translation + string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => + { + string key = match.Groups[1].Value.Trim(); + return tokenLookup.TryGetValue(key, out string value) + ? value + : match.Value; + }); + return new Translation(this.ModName, this.Locale, this.Key, text); + } + + /// Get whether the translation has a defined value. + public bool HasValue() + { + return !string.IsNullOrEmpty(this.Text); + } + + /// Get the translation text. Calling this method isn't strictly necessary, since you can assign a value directly to a string. + public override string ToString() + { + return this.Placeholder != null && !this.HasValue() + ? this.Placeholder + : this.Text; + } + + /// Get a string representation of the given translation. + /// The translation key. + public static implicit operator string(Translation translation) + { + return translation?.ToString(); + } + } +} diff --git a/src/SMAPI/Utilities/SButton.cs b/src/SMAPI/Utilities/SButton.cs new file mode 100644 index 00000000..fa5ae648 --- /dev/null +++ b/src/SMAPI/Utilities/SButton.cs @@ -0,0 +1,675 @@ +using System; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +namespace StardewModdingAPI.Utilities +{ + /// A unified button constant which includes all controller, keyboard, and mouse buttons. + /// Derived from , , and . + public enum SButton + { + /// No valid key. + None = 0, + + /********* + ** Mouse + *********/ + /// The left mouse button. + MouseLeft = 1000, + + /// The right mouse button. + MouseRight = 1001, + + /// The middle mouse button. + MouseMiddle = 1002, + + /// The first mouse XButton. + MouseX1 = 1003, + + /// The second mouse XButton. + MouseX2 = 1004, + + /********* + ** Controller + *********/ + /// The 'A' button on a controller. + ControllerA = SButtonExtensions.ControllerOffset + Buttons.A, + + /// The 'B' button on a controller. + ControllerB = SButtonExtensions.ControllerOffset + Buttons.B, + + /// The 'X' button on a controller. + ControllerX = SButtonExtensions.ControllerOffset + Buttons.X, + + /// The 'Y' button on a controller. + ControllerY = SButtonExtensions.ControllerOffset + Buttons.Y, + + /// The back button on a controller. + ControllerBack = SButtonExtensions.ControllerOffset + Buttons.Back, + + /// The start button on a controller. + ControllerStart = SButtonExtensions.ControllerOffset + Buttons.Start, + + /// The up button on the directional pad of a controller. + DPadUp = SButtonExtensions.ControllerOffset + Buttons.DPadUp, + + /// The down button on the directional pad of a controller. + DPadDown = SButtonExtensions.ControllerOffset + Buttons.DPadDown, + + /// The left button on the directional pad of a controller. + DPadLeft = SButtonExtensions.ControllerOffset + Buttons.DPadLeft, + + /// The right button on the directional pad of a controller. + DPadRight = SButtonExtensions.ControllerOffset + Buttons.DPadRight, + + /// The left bumper (shoulder) button on a controller. + LeftShoulder = SButtonExtensions.ControllerOffset + Buttons.LeftShoulder, + + /// The right bumper (shoulder) button on a controller. + RightShoulder = SButtonExtensions.ControllerOffset + Buttons.RightShoulder, + + /// The left trigger on a controller. + LeftTrigger = SButtonExtensions.ControllerOffset + Buttons.LeftTrigger, + + /// The right trigger on a controller. + RightTrigger = SButtonExtensions.ControllerOffset + Buttons.RightTrigger, + + /// The left analog stick on a controller (when pressed). + LeftStick = SButtonExtensions.ControllerOffset + Buttons.LeftStick, + + /// The right analog stick on a controller (when pressed). + RightStick = SButtonExtensions.ControllerOffset + Buttons.RightStick, + + /// The 'big button' on a controller. + BigButton = SButtonExtensions.ControllerOffset + Buttons.BigButton, + + /// The left analog stick on a controller (when pushed left). + LeftThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickLeft, + + /// The left analog stick on a controller (when pushed right). + LeftThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickRight, + + /// The left analog stick on a controller (when pushed down). + LeftThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickDown, + + /// The left analog stick on a controller (when pushed up). + LeftThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickUp, + + /// The right analog stick on a controller (when pushed left). + RightThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickLeft, + + /// The right analog stick on a controller (when pushed right). + RightThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickRight, + + /// The right analog stick on a controller (when pushed down). + RightThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickDown, + + /// The right analog stick on a controller (when pushed up). + RightThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickUp, + + /********* + ** Keyboard + *********/ + /// The A button on a keyboard. + A = Keys.A, + + /// The Add button on a keyboard. + Add = Keys.Add, + + /// The Applications button on a keyboard. + Apps = Keys.Apps, + + /// The Attn button on a keyboard. + Attn = Keys.Attn, + + /// The B button on a keyboard. + B = Keys.B, + + /// The Backspace button on a keyboard. + Back = Keys.Back, + + /// The Browser Back button on a keyboard in Windows 2000/XP. + BrowserBack = Keys.BrowserBack, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserFavorites = Keys.BrowserFavorites, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserForward = Keys.BrowserForward, + + /// The Browser Home button on a keyboard in Windows 2000/XP. + BrowserHome = Keys.BrowserHome, + + /// The Browser Refresh button on a keyboard in Windows 2000/XP. + BrowserRefresh = Keys.BrowserRefresh, + + /// The Browser Search button on a keyboard in Windows 2000/XP. + BrowserSearch = Keys.BrowserSearch, + + /// The Browser Stop button on a keyboard in Windows 2000/XP. + BrowserStop = Keys.BrowserStop, + + /// The C button on a keyboard. + C = Keys.C, + + /// The Caps Lock button on a keyboard. + CapsLock = Keys.CapsLock, + + /// The Green ChatPad button on a keyboard. + ChatPadGreen = Keys.ChatPadGreen, + + /// The Orange ChatPad button on a keyboard. + ChatPadOrange = Keys.ChatPadOrange, + + /// The CrSel button on a keyboard. + Crsel = Keys.Crsel, + + /// The D button on a keyboard. + D = Keys.D, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D0 = Keys.D0, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D1 = Keys.D1, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D2 = Keys.D2, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D3 = Keys.D3, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D4 = Keys.D4, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D5 = Keys.D5, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D6 = Keys.D6, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D7 = Keys.D7, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D8 = Keys.D8, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D9 = Keys.D9, + + /// The Decimal button on a keyboard. + Decimal = Keys.Decimal, + + /// The Delete button on a keyboard. + Delete = Keys.Delete, + + /// The Divide button on a keyboard. + Divide = Keys.Divide, + + /// The Down arrow button on a keyboard. + Down = Keys.Down, + + /// The E button on a keyboard. + E = Keys.E, + + /// The End button on a keyboard. + End = Keys.End, + + /// The Enter button on a keyboard. + Enter = Keys.Enter, + + /// The Erase EOF button on a keyboard. + EraseEof = Keys.EraseEof, + + /// The Escape button on a keyboard. + Escape = Keys.Escape, + + /// The Execute button on a keyboard. + Execute = Keys.Execute, + + /// The ExSel button on a keyboard. + Exsel = Keys.Exsel, + + /// The F button on a keyboard. + F = Keys.F, + + /// The F1 button on a keyboard. + F1 = Keys.F1, + + /// The F10 button on a keyboard. + F10 = Keys.F10, + + /// The F11 button on a keyboard. + F11 = Keys.F11, + + /// The F12 button on a keyboard. + F12 = Keys.F12, + + /// The F13 button on a keyboard. + F13 = Keys.F13, + + /// The F14 button on a keyboard. + F14 = Keys.F14, + + /// The F15 button on a keyboard. + F15 = Keys.F15, + + /// The F16 button on a keyboard. + F16 = Keys.F16, + + /// The F17 button on a keyboard. + F17 = Keys.F17, + + /// The F18 button on a keyboard. + F18 = Keys.F18, + + /// The F19 button on a keyboard. + F19 = Keys.F19, + + /// The F2 button on a keyboard. + F2 = Keys.F2, + + /// The F20 button on a keyboard. + F20 = Keys.F20, + + /// The F21 button on a keyboard. + F21 = Keys.F21, + + /// The F22 button on a keyboard. + F22 = Keys.F22, + + /// The F23 button on a keyboard. + F23 = Keys.F23, + + /// The F24 button on a keyboard. + F24 = Keys.F24, + + /// The F3 button on a keyboard. + F3 = Keys.F3, + + /// The F4 button on a keyboard. + F4 = Keys.F4, + + /// The F5 button on a keyboard. + F5 = Keys.F5, + + /// The F6 button on a keyboard. + F6 = Keys.F6, + + /// The F7 button on a keyboard. + F7 = Keys.F7, + + /// The F8 button on a keyboard. + F8 = Keys.F8, + + /// The F9 button on a keyboard. + F9 = Keys.F9, + + /// The G button on a keyboard. + G = Keys.G, + + /// The H button on a keyboard. + H = Keys.H, + + /// The Help button on a keyboard. + Help = Keys.Help, + + /// The Home button on a keyboard. + Home = Keys.Home, + + /// The I button on a keyboard. + I = Keys.I, + + /// The IME Convert button on a keyboard. + ImeConvert = Keys.ImeConvert, + + /// The IME NoConvert button on a keyboard. + ImeNoConvert = Keys.ImeNoConvert, + + /// The INS button on a keyboard. + Insert = Keys.Insert, + + /// The J button on a keyboard. + J = Keys.J, + + /// The K button on a keyboard. + K = Keys.K, + + /// The Kana button on a Japanese keyboard. + Kana = Keys.Kana, + + /// The Kanji button on a Japanese keyboard. + Kanji = Keys.Kanji, + + /// The L button on a keyboard. + L = Keys.L, + + /// The Start Applications 1 button on a keyboard in Windows 2000/XP. + LaunchApplication1 = Keys.LaunchApplication1, + + /// The Start Applications 2 button on a keyboard in Windows 2000/XP. + LaunchApplication2 = Keys.LaunchApplication2, + + /// The Start Mail button on a keyboard in Windows 2000/XP. + LaunchMail = Keys.LaunchMail, + + /// The Left arrow button on a keyboard. + Left = Keys.Left, + + /// The Left Alt button on a keyboard. + LeftAlt = Keys.LeftAlt, + + /// The Left Control button on a keyboard. + LeftControl = Keys.LeftControl, + + /// The Left Shift button on a keyboard. + LeftShift = Keys.LeftShift, + + /// The Left Windows button on a keyboard. + LeftWindows = Keys.LeftWindows, + + /// The M button on a keyboard. + M = Keys.M, + + /// The MediaNextTrack button on a keyboard in Windows 2000/XP. + MediaNextTrack = Keys.MediaNextTrack, + + /// The MediaPlayPause button on a keyboard in Windows 2000/XP. + MediaPlayPause = Keys.MediaPlayPause, + + /// The MediaPreviousTrack button on a keyboard in Windows 2000/XP. + MediaPreviousTrack = Keys.MediaPreviousTrack, + + /// The MediaStop button on a keyboard in Windows 2000/XP. + MediaStop = Keys.MediaStop, + + /// The Multiply button on a keyboard. + Multiply = Keys.Multiply, + + /// The N button on a keyboard. + N = Keys.N, + + /// The Num Lock button on a keyboard. + NumLock = Keys.NumLock, + + /// The Numeric keypad 0 button on a keyboard. + NumPad0 = Keys.NumPad0, + + /// The Numeric keypad 1 button on a keyboard. + NumPad1 = Keys.NumPad1, + + /// The Numeric keypad 2 button on a keyboard. + NumPad2 = Keys.NumPad2, + + /// The Numeric keypad 3 button on a keyboard. + NumPad3 = Keys.NumPad3, + + /// The Numeric keypad 4 button on a keyboard. + NumPad4 = Keys.NumPad4, + + /// The Numeric keypad 5 button on a keyboard. + NumPad5 = Keys.NumPad5, + + /// The Numeric keypad 6 button on a keyboard. + NumPad6 = Keys.NumPad6, + + /// The Numeric keypad 7 button on a keyboard. + NumPad7 = Keys.NumPad7, + + /// The Numeric keypad 8 button on a keyboard. + NumPad8 = Keys.NumPad8, + + /// The Numeric keypad 9 button on a keyboard. + NumPad9 = Keys.NumPad9, + + /// The O button on a keyboard. + O = Keys.O, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + Oem8 = Keys.Oem8, + + /// The OEM Auto button on a keyboard. + OemAuto = Keys.OemAuto, + + /// The OEM Angle Bracket or Backslash button on the RT 102 keyboard in Windows 2000/XP. + OemBackslash = Keys.OemBackslash, + + /// The Clear button on a keyboard. + OemClear = Keys.OemClear, + + /// The OEM Close Bracket button on a US standard keyboard in Windows 2000/XP. + OemCloseBrackets = Keys.OemCloseBrackets, + + /// The ',' button on a keyboard in any country/region in Windows 2000/XP. + OemComma = Keys.OemComma, + + /// The OEM Copy button on a keyboard. + OemCopy = Keys.OemCopy, + + /// The OEM Enlarge Window button on a keyboard. + OemEnlW = Keys.OemEnlW, + + /// The '-' button on a keyboard in any country/region in Windows 2000/XP. + OemMinus = Keys.OemMinus, + + /// The OEM Open Bracket button on a US standard keyboard in Windows 2000/XP. + OemOpenBrackets = Keys.OemOpenBrackets, + + /// The '.' button on a keyboard in any country/region. + OemPeriod = Keys.OemPeriod, + + /// The OEM Pipe button on a US standard keyboard. + OemPipe = Keys.OemPipe, + + /// The '+' button on a keyboard in Windows 2000/XP. + OemPlus = Keys.OemPlus, + + /// The OEM Question Mark button on a US standard keyboard. + OemQuestion = Keys.OemQuestion, + + /// The OEM Single/Double Quote button on a US standard keyboard. + OemQuotes = Keys.OemQuotes, + + /// The OEM Semicolon button on a US standard keyboard. + OemSemicolon = Keys.OemSemicolon, + + /// The OEM Tilde button on a US standard keyboard. + OemTilde = Keys.OemTilde, + + /// The P button on a keyboard. + P = Keys.P, + + /// The PA1 button on a keyboard. + Pa1 = Keys.Pa1, + + /// The Page Down button on a keyboard. + PageDown = Keys.PageDown, + + /// The Page Up button on a keyboard. + PageUp = Keys.PageUp, + + /// The Pause button on a keyboard. + Pause = Keys.Pause, + + /// The Play button on a keyboard. + Play = Keys.Play, + + /// The Print button on a keyboard. + Print = Keys.Print, + + /// The Print Screen button on a keyboard. + PrintScreen = Keys.PrintScreen, + + /// The IME Process button on a keyboard in Windows 95/98/ME/NT 4.0/2000/XP. + ProcessKey = Keys.ProcessKey, + + /// The Q button on a keyboard. + Q = Keys.Q, + + /// The R button on a keyboard. + R = Keys.R, + + /// The Right Arrow button on a keyboard. + Right = Keys.Right, + + /// The Right Alt button on a keyboard. + RightAlt = Keys.RightAlt, + + /// The Right Control button on a keyboard. + RightControl = Keys.RightControl, + + /// The Right Shift button on a keyboard. + RightShift = Keys.RightShift, + + /// The Right Windows button on a keyboard. + RightWindows = Keys.RightWindows, + + /// The S button on a keyboard. + S = Keys.S, + + /// The Scroll Lock button on a keyboard. + Scroll = Keys.Scroll, + + /// The Select button on a keyboard. + Select = Keys.Select, + + /// The Select Media button on a keyboard in Windows 2000/XP. + SelectMedia = Keys.SelectMedia, + + /// The Separator button on a keyboard. + Separator = Keys.Separator, + + /// The Computer Sleep button on a keyboard. + Sleep = Keys.Sleep, + + /// The Space bar on a keyboard. + Space = Keys.Space, + + /// The Subtract button on a keyboard. + Subtract = Keys.Subtract, + + /// The T button on a keyboard. + T = Keys.T, + + /// The Tab button on a keyboard. + Tab = Keys.Tab, + + /// The U button on a keyboard. + U = Keys.U, + + /// The Up Arrow button on a keyboard. + Up = Keys.Up, + + /// The V button on a keyboard. + V = Keys.V, + + /// The Volume Down button on a keyboard in Windows 2000/XP. + VolumeDown = Keys.VolumeDown, + + /// The Volume Mute button on a keyboard in Windows 2000/XP. + VolumeMute = Keys.VolumeMute, + + /// The Volume Up button on a keyboard in Windows 2000/XP. + VolumeUp = Keys.VolumeUp, + + /// The W button on a keyboard. + W = Keys.W, + + /// The X button on a keyboard. + X = Keys.X, + + /// The Y button on a keyboard. + Y = Keys.Y, + + /// The Z button on a keyboard. + Z = Keys.Z, + + /// The Zoom button on a keyboard. + Zoom = Keys.Zoom + } + + /// Provides extension methods for . + public static class SButtonExtensions + { + /********* + ** Accessors + *********/ + /// The offset added to values when converting them to to avoid collisions with values. + internal const int ControllerOffset = 2000; + + + /********* + ** Public methods + *********/ + /// Get the equivalent for the given button. + /// The keyboard button to convert. + internal static SButton ToSButton(this Keys key) + { + return (SButton)key; + } + + /// Get the equivalent for the given button. + /// The controller button to convert. + internal static SButton ToSButton(this Buttons key) + { + return (SButton)(SButtonExtensions.ControllerOffset + key); + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The keyboard equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetKeyboard(this SButton input, out Keys key) + { + if (Enum.IsDefined(typeof(Keys), (int)input)) + { + key = (Keys)input; + return true; + } + + key = Keys.None; + return false; + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The controller equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetController(this SButton input, out Buttons button) + { + if (Enum.IsDefined(typeof(Buttons), (int)input - SButtonExtensions.ControllerOffset)) + { + button = (Buttons)(input - SButtonExtensions.ControllerOffset); + return true; + } + + button = 0; + return false; + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The Stardew Valley input button equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetStardewInput(this SButton input, out InputButton button) + { + // keyboard + if (input.TryGetKeyboard(out Keys key)) + { + button = new InputButton(key); + return true; + } + + // mouse + if (input == SButton.MouseLeft || input == SButton.MouseRight) + { + button = new InputButton(mouseLeft: input == SButton.MouseLeft); + return true; + } + + // not valid + button = default(InputButton); + return false; + } + } +} diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs new file mode 100644 index 00000000..326d7fc7 --- /dev/null +++ b/src/SMAPI/Utilities/SDate.cs @@ -0,0 +1,232 @@ +using System; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Utilities +{ + /// Represents a Stardew Valley date. + public class SDate : IEquatable + { + /********* + ** Properties + *********/ + /// The internal season names in order. + private readonly string[] Seasons = { "spring", "summer", "fall", "winter" }; + + /// The number of seasons in a year. + private int SeasonsInYear => this.Seasons.Length; + + /// The number of days in a season. + private readonly int DaysInSeason = 28; + + + /********* + ** Accessors + *********/ + /// The day of month. + public int Day { get; } + + /// The season name. + public string Season { get; } + + /// The year. + public int Year { get; } + + /// The day of week. + public DayOfWeek DayOfWeek { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The day of month. + /// The season name. + /// One of the arguments has an invalid value (like day 35). + public SDate(int day, string season) + : this(day, season, Game1.year) { } + + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// One of the arguments has an invalid value (like day 35). + public SDate(int day, string season, int year) + { + // validate + if (season == null) + throw new ArgumentNullException(nameof(season)); + if (!this.Seasons.Contains(season)) + throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); + if (day < 1 || day > this.DaysInSeason) + throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); + if (year < 1) + throw new ArgumentException($"Invalid year '{year}', must be at least 1."); + + // initialise + this.Day = day; + this.Season = season; + this.Year = year; + this.DayOfWeek = this.GetDayOfWeek(); + } + + /// Get the current in-game date. + public static SDate Now() + { + return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year); + } + + /// Get a new date with the given number of days added. + /// The number of days to add. + /// Returns the resulting date. + /// The offset would result in an invalid date (like year 0). + public SDate AddDays(int offset) + { + // get new hash code + int hashCode = this.GetHashCode() + offset; + if (hashCode < 1) + throw new ArithmeticException($"Adding {offset} days to {this} would result in a date before 01 spring Y1."); + + // get day + int day = hashCode % 28; + if (day == 0) + day = 28; + + // get season index + int seasonIndex = hashCode / 28; + if (seasonIndex > 0 && hashCode % 28 == 0) + seasonIndex -= 1; + seasonIndex %= 4; + + // get year + int year = hashCode / (this.Seasons.Length * this.DaysInSeason) + 1; + + // create date + return new SDate(day, this.Seasons[seasonIndex], year); + } + + /// Get a string representation of the date. This is mainly intended for debugging or console messages. + public override string ToString() + { + return $"{this.Day:00} {this.Season} Y{this.Year}"; + } + + /**** + ** IEquatable + ****/ + /// Get whether this instance is equal to another. + /// The other value to compare. + public bool Equals(SDate other) + { + return this == other; + } + + /// Get whether this instance is equal to another. + /// The other value to compare. + public override bool Equals(object obj) + { + return obj is SDate other && this == other; + } + + /// Get a hash code which uniquely identifies a date. + public override int GetHashCode() + { + // return the number of days since 01 spring Y1 (inclusively) + int yearIndex = this.Year - 1; + return + yearIndex * this.DaysInSeason * this.SeasonsInYear + + this.GetSeasonIndex() * this.DaysInSeason + + this.Day; + } + + /**** + ** Operators + ****/ + /// Get whether one date is equal to another. + /// The base date to compare. + /// The other date to compare. + /// The equality of the dates + public static bool operator ==(SDate date, SDate other) + { + return date?.GetHashCode() == other?.GetHashCode(); + } + + /// Get whether one date is not equal to another. + /// The base date to compare. + /// The other date to compare. + public static bool operator !=(SDate date, SDate other) + { + return date?.GetHashCode() != other?.GetHashCode(); + } + + /// Get whether one date is more than another. + /// The base date to compare. + /// The other date to compare. + public static bool operator >(SDate date, SDate other) + { + return date?.GetHashCode() > other?.GetHashCode(); + } + + /// Get whether one date is more than or equal to another. + /// The base date to compare. + /// The other date to compare. + public static bool operator >=(SDate date, SDate other) + { + return date?.GetHashCode() >= other?.GetHashCode(); + } + + /// Get whether one date is less than or equal to another. + /// The base date to compare. + /// The other date to compare. + public static bool operator <=(SDate date, SDate other) + { + return date?.GetHashCode() <= other?.GetHashCode(); + } + + /// Get whether one date is less than another. + /// The base date to compare. + /// The other date to compare. + public static bool operator <(SDate date, SDate other) + { + return date?.GetHashCode() < other?.GetHashCode(); + } + + + /********* + ** Private methods + *********/ + /// Get the day of week for the current date. + private DayOfWeek GetDayOfWeek() + { + switch (this.Day % 7) + { + case 0: + return DayOfWeek.Sunday; + case 1: + return DayOfWeek.Monday; + case 2: + return DayOfWeek.Tuesday; + case 3: + return DayOfWeek.Wednesday; + case 4: + return DayOfWeek.Thursday; + case 5: + return DayOfWeek.Friday; + case 6: + return DayOfWeek.Saturday; + default: + return 0; + } + } + + /// Get the current season index. + /// The current season wasn't recognised. + private int GetSeasonIndex() + { + int index = Array.IndexOf(this.Seasons, this.Season); + if (index == -1) + throw new InvalidOperationException($"The current season '{this.Season}' wasn't recognised."); + return index; + } + } +} diff --git a/src/SMAPI/icon.ico b/src/SMAPI/icon.ico new file mode 100644 index 00000000..587a6e74 Binary files /dev/null and b/src/SMAPI/icon.ico differ diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config new file mode 100644 index 00000000..e5fa3c3a --- /dev/null +++ b/src/SMAPI/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/SMAPI/steam_appid.txt b/src/SMAPI/steam_appid.txt new file mode 100644 index 00000000..9fe92b96 --- /dev/null +++ b/src/SMAPI/steam_appid.txt @@ -0,0 +1 @@ +413150 \ No newline at end of file diff --git a/src/SMAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh new file mode 100644 index 00000000..70f1873a --- /dev/null +++ b/src/SMAPI/unix-launcher.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# MonoKickstart Shell Script +# Written by Ethan "flibitijibibo" Lee +# Modified for StardewModdingAPI by Viz and Pathoschild + +# Move to script's directory +cd "`dirname "$0"`" + +# Get the system architecture +UNAME=`uname` +ARCH=`uname -m` + +# MonoKickstart picks the right libfolder, so just execute the right binary. +if [ "$UNAME" == "Darwin" ]; then + # ... Except on OSX. + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ + + # El Capitan is a total idiot and wipes this variable out, making the + # Steam overlay disappear. This sidesteps "System Integrity Protection" + # and resets the variable with Valve's own variable (they provided this + # fix by the way, thanks Valve!). Note that you will need to update your + # launch configuration to the script location, NOT just the app location + # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). + # -flibit + if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then + export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" + fi + + # this was here before + ln -sf mcs.bin.osx mcs + + # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI + if [ -f libgdiplus.dylib ]; then + rm libgdiplus.dylib + fi + if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then + ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib + fi + + # launch SMAPI + cp StardewValley.bin.osx StardewModdingAPI.bin.osx + open -a Terminal ./StardewModdingAPI.bin.osx $@ +else + # choose launcher + LAUNCHER="" + if [ "$ARCH" == "x86_64" ]; then + ln -sf mcs.bin.x86_64 mcs + cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 + LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" + else + ln -sf mcs.bin.x86 mcs + cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 + LAUNCHER="./StardewModdingAPI.bin.x86 $@" + fi + + # get cross-distro version of POSIX command + COMMAND="" + if command -v command 2>/dev/null; then + COMMAND="command -v" + elif type type 2>/dev/null; then + COMMAND="type" + fi + + # open SMAPI in terminal + if $COMMAND x-terminal-emulator 2>/dev/null; then + x-terminal-emulator -e "$LAUNCHER" + elif $COMMAND xfce4-terminal 2>/dev/null; then + xfce4-terminal -e "$LAUNCHER" + elif $COMMAND gnome-terminal 2>/dev/null; then + gnome-terminal -e "$LAUNCHER" + elif $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" + elif $COMMAND konsole 2>/dev/null; then + konsole -e "$LAUNCHER" + elif $COMMAND terminal 2>/dev/null; then + terminal -e "$LAUNCHER" + else + $LAUNCHER + fi + + # some Linux users get error 127 (command not found) from the above block, even though + # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when + # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal + # (which can be slow if there is none). + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi +fi diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs deleted file mode 100644 index 7cc6804a..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")] -[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")] -[assembly: AssemblyProduct("StardewModdingAPI.AssemblyRewriters")] -[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")] diff --git a/src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs b/src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs deleted file mode 100644 index a7f100f2..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.AssemblyRewriters -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - public class SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } - - - /**** - ** MonoGame signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); - } - - /**** - ** XNA signatures - ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin() - { - base.Begin(); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState) - { - base.Begin(sortMode, blendState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); - } - - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); - } - } -} diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj deleted file mode 100644 index c8b03086..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - Debug - x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49} - Library - Properties - StardewModdingAPI.AssemblyRewriters - StardewModdingAPI.AssemblyRewriters - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/Enums/Platform.cs b/src/StardewModdingAPI.Installer/Enums/Platform.cs deleted file mode 100644 index 9bcaa3c3..00000000 --- a/src/StardewModdingAPI.Installer/Enums/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingApi.Installer.Enums -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux/Mac version of the game. - Mono, - - /// The Windows version of the game. - Windows - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs b/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs deleted file mode 100644 index e62b2a7c..00000000 --- a/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingApi.Installer.Enums -{ - /// The action to perform. - internal enum ScriptAction - { - /// Install SMAPI to the game directory. - Install, - - /// Remove SMAPI from the game directory. - Uninstall - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs deleted file mode 100644 index 1a132e54..00000000 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ /dev/null @@ -1,740 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using Microsoft.Win32; -using StardewModdingApi.Installer.Enums; - -namespace StardewModdingApi.Installer -{ - /// Interactively performs the install and uninstall logic. - internal class InteractiveInstaller - { - /********* - ** Properties - *********/ - /// The value that represents Windows 7. - private readonly Version Windows7Version = new Version(6, 1); - - /// The default file paths where Stardew Valley can be installed. - /// The target platform. - /// Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. - private IEnumerable GetDefaultInstallPaths(Platform platform) - { - switch (platform) - { - case Platform.Mono: - { - string home = Environment.GetEnvironmentVariable("HOME"); - - // Linux - yield return $"{home}/GOG Games/Stardew Valley/game"; - yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley") - ? $"{home}/.steam/steam/steamapps/common/Stardew Valley" - : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley"; - - // Mac - yield return "/Applications/Stardew Valley.app/Contents/MacOS"; - yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; - } - break; - - case Platform.Windows: - { - // Windows - yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley"; - yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"; - - // Windows registry - IDictionary registryKeys = new Dictionary - { - [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam - [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows - }; - foreach (var pair in registryKeys) - { - string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); - if (!string.IsNullOrWhiteSpace(path)) - yield return path; - } - } - break; - - default: - throw new InvalidOperationException($"Unknown platform '{platform}'."); - } - } - - /// Get the absolute file or folder paths to remove when uninstalling SMAPI. - /// The folder for Stardew Valley and SMAPI. - /// The folder for SMAPI mods. - private IEnumerable GetUninstallPaths(DirectoryInfo installDir, DirectoryInfo modsDir) - { - string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); - - // common - yield return GetInstallPath("Mono.Cecil.dll"); - yield return GetInstallPath("Newtonsoft.Json.dll"); - yield return GetInstallPath("StardewModdingAPI.exe"); - yield return GetInstallPath("StardewModdingAPI.config.json"); - yield return GetInstallPath("StardewModdingAPI.data.json"); - yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); - yield return GetInstallPath("System.ValueTuple.dll"); - yield return GetInstallPath("steam_appid.txt"); - - // Linux/Mac only - yield return GetInstallPath("libgdiplus.dylib"); - yield return GetInstallPath("StardewModdingAPI"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); - yield return GetInstallPath("System.Numerics.dll"); - yield return GetInstallPath("System.Runtime.Caching.dll"); - - // Windows only - yield return GetInstallPath("StardewModdingAPI.pdb"); - - // obsolete - yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 - yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 - if (modsDir.Exists) - { - foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) - yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7 - } - yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files - } - - /// Whether the current console supports color formatting. - private static readonly bool ConsoleSupportsColor = InteractiveInstaller.GetConsoleSupportsColor(); - - - /********* - ** Public methods - *********/ - /// Run the install or uninstall script. - /// The command line arguments. - /// - /// Initialisation flow: - /// 1. Collect information (mainly OS and install path) and validate it. - /// 2. Ask the user whether to install or uninstall. - /// - /// Uninstall logic: - /// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup. - /// 2. Delete all files and folders in the game directory matching one of the values returned by . - /// - /// Install flow: - /// 1. Run the uninstall flow. - /// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory. - /// 3. On Linux/Mac: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) - /// 4. Create the 'Mods' directory. - /// 5. Copy the bundled mods into the 'Mods' directory (deleting any existing versions). - /// 6. Move any mods from app data into game's mods directory. - /// - public void Run(string[] args) - { - /**** - ** read command-line arguments - ****/ - // get action from CLI - bool installArg = args.Contains("--install"); - bool uninstallArg = args.Contains("--uninstall"); - if (installArg && uninstallArg) - { - this.PrintError("You can't specify both --install and --uninstall command-line flags."); - Console.ReadLine(); - return; - } - - // get game path from CLI - string gamePathArg = null; - { - int pathIndex = Array.LastIndexOf(args, "--game-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - gamePathArg = args[pathIndex]; - } - - /**** - ** collect details - ****/ - // get platform - Platform platform = this.DetectPlatform(); - this.PrintDebug($"Platform: {(platform == Platform.Windows ? "Windows" : "Linux or Mac")}."); - - // get game path - DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg); - if (installDir == null) - { - this.PrintError("Failed finding your game path."); - Console.ReadLine(); - return; - } - - // get folders - DirectoryInfo packageDir = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.ToString())); - DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); - var paths = new - { - executable = Path.Combine(installDir.FullName, platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe"), - unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"), - unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), - unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") - }; - this.PrintDebug($"Install path: {installDir}."); - - /**** - ** validate assumptions - ****/ - if (!packageDir.Exists) - { - this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip") - ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder." - : $"The 'internal/{platform}' package folder is missing (should be at {packageDir})." - ); - Console.ReadLine(); - return; - } - if (!File.Exists(paths.executable)) - { - this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); - Console.ReadLine(); - return; - } - - /**** - ** validate Windows dependencies - ****/ - if (platform == Platform.Windows) - { - // .NET Framework 4.5+ - if (!this.HasNetFramework45(platform)) - { - this.PrintError(Environment.OSVersion.Version >= this.Windows7Version - ? "Please install the latest version of .NET Framework before installing SMAPI." // Windows 7+ - : "Please install .NET Framework 4.5 before installing SMAPI." // Windows Vista or earlier - ); - this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details."); - Console.ReadLine(); - return; - } - if (!this.HasXNA(platform)) - { - this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup."); - Console.ReadLine(); - return; - } - } - - Console.WriteLine(); - - /**** - ** ask user what to do - ****/ - ScriptAction action; - - if (installArg) - action = ScriptAction.Install; - else if (uninstallArg) - action = ScriptAction.Uninstall; - else - { - Console.WriteLine("You can...."); - Console.WriteLine("[1] Install SMAPI."); - Console.WriteLine("[2] Uninstall SMAPI."); - Console.WriteLine(); - - string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2"); - switch (choice) - { - case "1": - action = ScriptAction.Install; - break; - case "2": - action = ScriptAction.Uninstall; - break; - default: - throw new InvalidOperationException($"Unexpected action key '{choice}'."); - } - Console.WriteLine(); - } - - /**** - ** Always uninstall old files - ****/ - // restore game launcher - if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup)) - { - this.PrintDebug("Removing SMAPI launcher..."); - this.InteractivelyDelete(paths.unixLauncher); - File.Move(paths.unixLauncherBackup, paths.unixLauncher); - } - - // remove old files - string[] removePaths = this.GetUninstallPaths(installDir, modsDir) - .Where(path => Directory.Exists(path) || File.Exists(path)) - .ToArray(); - if (removePaths.Any()) - { - this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); - foreach (string path in removePaths) - this.InteractivelyDelete(path); - } - - /**** - ** Install new files - ****/ - if (action == ScriptAction.Install) - { - // copy SMAPI files to game dir - this.PrintDebug("Adding SMAPI files..."); - foreach (FileInfo sourceFile in packageDir.EnumerateFiles()) - { - string targetPath = Path.Combine(installDir.FullName, sourceFile.Name); - this.InteractivelyDelete(targetPath); - sourceFile.CopyTo(targetPath); - } - - // replace mod launcher (if possible) - if (platform == Platform.Mono) - { - this.PrintDebug("Safely replacing game launcher..."); - if (!File.Exists(paths.unixLauncherBackup)) - File.Move(paths.unixLauncher, paths.unixLauncherBackup); - else if (File.Exists(paths.unixLauncher)) - this.InteractivelyDelete(paths.unixLauncher); - - File.Move(paths.unixSmapiLauncher, paths.unixLauncher); - } - - // create mods directory (if needed) - if (!modsDir.Exists) - { - this.PrintDebug("Creating mods directory..."); - modsDir.Create(); - } - - // add or replace bundled mods - Directory.CreateDirectory(Path.Combine(installDir.FullName, "Mods")); - DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods")); - if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) - { - this.PrintDebug("Adding bundled mods..."); - foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) - { - this.PrintDebug($" adding {sourceDir.Name}..."); - - // initialise target dir - DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name)); - this.InteractivelyDelete(targetDir.FullName); - targetDir.Create(); - - // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles()) - sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); - } - } - - // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(platform, modsDir, packagedModsDir); - } - Console.WriteLine(); - - /**** - ** exit - ****/ - this.PrintColor("Done!", ConsoleColor.DarkGreen); - if (platform == Platform.Windows) - { - this.PrintColor( - action == ScriptAction.Install - ? "Don't forget to launch StardewModdingAPI.exe instead of the normal game executable. See the readme.txt for details." - : "If you manually changed shortcuts or Steam to launch SMAPI, don't forget to change those back.", - ConsoleColor.DarkGreen - ); - } - else if (action == ScriptAction.Install) - this.PrintColor("You can launch the game the same way as before to play with mods.", ConsoleColor.DarkGreen); - Console.ReadKey(); - } - - - /********* - ** Private methods - *********/ - /// Detect the game's platform. - /// The platform is not supported. - private Platform DetectPlatform() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - case PlatformID.Unix: - return Platform.Mono; - - default: - return Platform.Windows; - } - } - - /// Test whether the current console supports color formatting. - private static bool GetConsoleSupportsColor() - { - try - { - Console.ForegroundColor = Console.ForegroundColor; - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } - - /// Get the value of a key in the Windows registry. - /// The full path of the registry key relative to HKLM. - /// The name of the value. - private string GetLocalMachineRegistryValue(string key, string name) - { - RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; - RegistryKey openKey = localMachine.OpenSubKey(key); - if (openKey == null) - return null; - using (openKey) - return (string)openKey.GetValue(name); - } - - /// Print a debug message. - /// The text to print. - private void PrintDebug(string text) - { - this.PrintColor(text, ConsoleColor.DarkGray); - } - - /// Print a warning message. - /// The text to print. - private void PrintWarning(string text) - { - this.PrintColor(text, ConsoleColor.DarkYellow); - } - - /// Print a warning message. - /// The text to print. - private void PrintError(string text) - { - this.PrintColor(text, ConsoleColor.Red); - } - - /// Print a message to the console. - /// The message text. - /// The text foreground color. - private void PrintColor(string text, ConsoleColor color) - { - if (InteractiveInstaller.ConsoleSupportsColor) - { - Console.ForegroundColor = color; - Console.WriteLine(text); - Console.ResetColor(); - } - else - Console.WriteLine(text); - } - - /// Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows. - /// The current platform. - /// The current platform is not Windows. - private bool HasNetFramework45(Platform platform) - { - switch (platform) - { - case Platform.Windows: - using (RegistryKey versionKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full")) - return versionKey?.GetValue("Release") != null; // .NET Framework 4.5+ - - default: - throw new NotSupportedException("The installed .NET Framework version can only be checked on Windows."); - } - } - - /// Get whether the current system has XNA Framework installed. This only applies on Windows. - /// The current platform. - /// The current platform is not Windows. - private bool HasXNA(Platform platform) - { - switch (platform) - { - case Platform.Windows: - using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\XNA\Framework")) - return key != null; // XNA Framework 4.0+ - - default: - throw new NotSupportedException("The installed XNA Framework version can only be checked on Windows."); - } - } - - /// Interactively delete a file or folder path, and block until deletion completes. - /// The file or folder path. - private void InteractivelyDelete(string path) - { - while (true) - { - try - { - this.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); - break; - } - catch (Exception ex) - { - this.PrintError($"Oops! The installer couldn't delete {path}: [{ex.GetType().Name}] {ex.Message}."); - this.PrintError("Try rebooting your computer and then run the installer again. If that doesn't work, try deleting it yourself then press any key to retry."); - Console.ReadKey(); - } - } - } - - /// Delete a file or folder regardless of file permissions, and block until deletion completes. - /// The file or folder to reset. - private void ForceDelete(FileSystemInfo entry) - { - // ignore if already deleted - entry.Refresh(); - if (!entry.Exists) - return; - - // delete children - var folder = entry as DirectoryInfo; - if (folder != null) - { - foreach (FileSystemInfo child in folder.GetFileSystemInfos()) - this.ForceDelete(child); - } - - // reset permissions & delete - entry.Attributes = FileAttributes.Normal; - entry.Delete(); - - // wait for deletion to finish - for (int i = 0; i < 10; i++) - { - entry.Refresh(); - if (entry.Exists) - Thread.Sleep(500); - } - - // throw exception if deletion didn't happen before timeout - entry.Refresh(); - if (entry.Exists) - throw new IOException($"Timed out trying to delete {entry.FullName}"); - } - - /// Interactively ask the user to choose a value. - /// The message to print. - /// The allowed options (not case sensitive). - private string InteractivelyChoose(string message, params string[] options) - { - while (true) - { - Console.WriteLine(message); - string input = Console.ReadLine()?.Trim().ToLowerInvariant(); - if (!options.Contains(input)) - { - Console.WriteLine("That's not a valid option."); - continue; - } - return input; - } - } - - /// Interactively locate the game install path to update. - /// The current platform. - /// The path specified as a command-line argument (if any), which should override automatic path detection. - private DirectoryInfo InteractivelyGetInstallPath(Platform platform, string specifiedPath) - { - // get executable name - string executableFilename = platform == Platform.Windows - ? "Stardew Valley.exe" - : "StardewValley.exe"; - - // validate specified path - if (specifiedPath != null) - { - var dir = new DirectoryInfo(specifiedPath); - if (!dir.Exists) - { - this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't exist."); - return null; - } - if (!dir.EnumerateFiles(executableFilename).Any()) - { - this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't contain the Stardew Valley executable."); - return null; - } - return dir; - } - - // get installed paths - DirectoryInfo[] defaultPaths = - ( - from path in this.GetDefaultInstallPaths(platform).Distinct(StringComparer.InvariantCultureIgnoreCase) - let dir = new DirectoryInfo(path) - where dir.Exists && dir.EnumerateFiles(executableFilename).Any() - select dir - ) - .ToArray(); - - // choose where to install - if (defaultPaths.Any()) - { - // only one path - if (defaultPaths.Length == 1) - return defaultPaths.First(); - - // let user choose path - Console.WriteLine(); - Console.WriteLine("Found multiple copies of the game:"); - for (int i = 0; i < defaultPaths.Length; i++) - Console.WriteLine($"[{i + 1}] {defaultPaths[i].FullName}"); - Console.WriteLine(); - - string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - string choice = this.InteractivelyChoose("Where do you want to add/remove SMAPI? Type the number next to your choice, then press enter.", validOptions); - int index = int.Parse(choice, CultureInfo.InvariantCulture) - 1; - return defaultPaths[index]; - } - - // ask user - Console.WriteLine("Oops, couldn't find the game automatically."); - while (true) - { - // get path from user - Console.WriteLine($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter."); - string path = Console.ReadLine()?.Trim(); - if (string.IsNullOrWhiteSpace(path)) - { - Console.WriteLine(" You must specify a directory path to continue."); - continue; - } - - // normalise path - if (platform == Platform.Windows) - path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path - if (platform == Platform.Mono) - path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line - if (path.StartsWith("~/")) - { - string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE"); - path = Path.Combine(home, path.Substring(2)); - } - - // get directory - if (File.Exists(path)) - path = Path.GetDirectoryName(path); - DirectoryInfo directory = new DirectoryInfo(path); - - // validate path - if (!directory.Exists) - { - Console.WriteLine(" That directory doesn't seem to exist."); - continue; - } - if (!directory.EnumerateFiles(executableFilename).Any()) - { - Console.WriteLine(" That directory doesn't contain a Stardew Valley executable."); - continue; - } - - // looks OK - Console.WriteLine(" OK!"); - return directory; - } - } - - /// Interactively move mods out of the appdata directory. - /// The current platform. - /// The directory which should contain all mods. - /// The installer directory containing packaged mods. - private void InteractivelyRemoveAppDataMods(Platform platform, DirectoryInfo properModsDir, DirectoryInfo packagedModsDir) - { - // get packaged mods to delete - string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); - - // get path - string homePath = platform == Platform.Windows - ? Environment.GetEnvironmentVariable("APPDATA") - : Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".config"); - string appDataPath = Path.Combine(homePath, "StardewValley"); - DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods")); - - // check if migration needed - if (!modDir.Exists) - return; - this.PrintDebug($"Found an obsolete mod path: {modDir.FullName}"); - this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); - - // move mods if no conflicts (else warn) - foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos()) - { - // get type - bool isDir = entry is DirectoryInfo; - if (!isDir && !(entry is FileInfo)) - continue; // should never happen - - // delete packaged mods (newer version bundled into SMAPI) - if (isDir && packagedModNames.Contains(entry.Name, StringComparer.InvariantCultureIgnoreCase)) - { - this.PrintDebug($" Deleting {entry.Name} because it's bundled into SMAPI..."); - this.InteractivelyDelete(entry.FullName); - continue; - } - - // check paths - string newPath = Path.Combine(properModsDir.FullName, entry.Name); - if (isDir ? Directory.Exists(newPath) : File.Exists(newPath)) - { - this.PrintWarning($" Can't move {entry.Name} because it already exists in your game's mod directory."); - continue; - } - - // move into mods - this.PrintDebug($" Moving {entry.Name} into the game's mod directory..."); - this.Move(entry, newPath); - } - - // delete if empty - if (modDir.EnumerateFileSystemInfos().Any()) - this.PrintWarning(" You have files in this folder which couldn't be moved automatically. These will be ignored by SMAPI."); - else - { - this.PrintDebug(" Deleted empty directory."); - modDir.Delete(); - } - } - - /// Move a filesystem entry to a new parent directory. - /// The filesystem entry to move. - /// The destination path. - /// We can't use or , because those don't work across partitions. - private void Move(FileSystemInfo entry, string newPath) - { - // file - if (entry is FileInfo file) - { - file.CopyTo(newPath); - file.Delete(); - } - - // directory - else - { - Directory.CreateDirectory(newPath); - - DirectoryInfo directory = (DirectoryInfo)entry; - foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos()) - this.Move(child, Path.Combine(newPath, child.Name)); - - directory.Delete(); - } - } - } -} diff --git a/src/StardewModdingAPI.Installer/Program.cs b/src/StardewModdingAPI.Installer/Program.cs deleted file mode 100644 index 8f328ecf..00000000 --- a/src/StardewModdingAPI.Installer/Program.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace StardewModdingApi.Installer -{ - /// The entry point for SMAPI's install and uninstall console app. - internal class Program - { - /********* - ** Public methods - *********/ - /// Run the install or uninstall script. - /// The command line arguments. - public static void Main(string[] args) - { - var installer = new InteractiveInstaller(); - installer.Run(args); - } - } -} diff --git a/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs deleted file mode 100644 index 3a6a4648..00000000 --- a/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("StardewModdingAPI.Installer")] -[assembly: AssemblyProduct("StardewModdingAPI.Installer")] -[assembly: Guid("443ddf81-6aaf-420a-a610-3459f37e5575")] diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj deleted file mode 100644 index 58ce519c..00000000 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Debug - x86 - {443DDF81-6AAF-420A-A610-3459F37E5575} - Exe - Properties - StardewModdingAPI.Installer - StardewModdingAPI.Installer - v4.0 - 512 - true - - - x86 - true - full - false - $(SolutionDir)\..\bin\Debug\Installer - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - $(SolutionDir)\..\bin\Release\Installer - TRACE - prompt - 4 - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - Always - - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/readme.txt b/src/StardewModdingAPI.Installer/readme.txt deleted file mode 100644 index eb27ac52..00000000 --- a/src/StardewModdingAPI.Installer/readme.txt +++ /dev/null @@ -1,44 +0,0 @@ - ___ ___ ___ ___ - / /\ /__/\ / /\ / /\ ___ - / /:/_ | |::\ / /::\ / /::\ / /\ - / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ - / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ /__/::\ - /__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ \__\/\:\__ - \ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \ \:\/\ - \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ - \__\/ /:/ \ \:\ \ \:\ \ \:\ /__/:/ - /__/:/ \ \:\ \ \:\ \ \:\ \__\/ - \__\/ \__\/ \__\/ \__\/ - - -SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. - - -Install guide --------------------------------- -See http://stardewvalleywiki.com/Modding:Installing_SMAPI. - - -Need help? --------------------------------- -- FAQs: http://stardewvalleywiki.com/Modding:Player_FAQs -- Ask for help: https://discord.gg/kH55QXP - - -Manual install --------------------------------- -THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. -If you really want to install SMAPI manually, here's how. - -1. Download the latest version of SMAPI: https://github.com/Pathoschild/SMAPI/releases. -2. Unzip the .zip file somewhere (not in your game folder). -3. Copy the files from the "internal/Windows" folder (on Windows) or "internal/Mono" folder (on - Linux/Mac) into your game folder. The `StardewModdingAPI.exe` file should be right next to the - game's executable. -4. - - Windows only: if you use Steam, see the install guide above to enable achievements and - overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. - - - Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and - "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to - play with mods. diff --git a/src/StardewModdingAPI.ModBuildConfig/README.md b/src/StardewModdingAPI.ModBuildConfig/README.md deleted file mode 100644 index c261e705..00000000 --- a/src/StardewModdingAPI.ModBuildConfig/README.md +++ /dev/null @@ -1,121 +0,0 @@ -**Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration -for [Stardew Valley](http://stardewvalley.net/) [SMAPI](https://github.com/Pathoschild/SMAPI) mods. - -The package... - -* lets you write your mod once, and compile it on any computer. It detects the current platform - (Linux, Mac, or Windows) and game install path, and injects the right references automatically. -* configures Visual Studio so you can debug into the mod code when the game is running (_Windows - only_). -* packages the mod automatically into the game's mod folder when you build the code (_optional_). - -## Contents -* [Install](#install) -* [Simplify mod development](#simplify-mod-development) -* [Troubleshoot](#troubleshoot) -* [Versions](#versions) - -## Install -**When creating a new mod:** - -1. Create an empty library project. -2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). -3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). -4. Compile on any platform. - -**When migrating an existing mod:** - -1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, - `StardewModdingAPI`, and `xTile`. -2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). -3. Compile on any platform. - -## Simplify mod development -### Package your mod into the game folder automatically -You can copy your mod files into the `Mods` folder automatically each time you build, so you don't -need to do it manually: - -1. Edit your mod's `.csproj` file. -2. Add this block above the first `` line: - - ```xml - $(MSBuildProjectName) - ``` - -That's it! Each time you build, the files in `\Mods\` will be updated with -your `manifest.json`, build output, and any `i18n` files. - -Notes: -* To add custom files, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). -* To customise the folder name, just replace `$(MSBuildProjectName)` with the folder name you want. -* If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). - -### Debug into the mod code (Windows-only) -Stepping into your mod code when the game is running is straightforward, since this package injects -the configuration automatically. To do it: - -1. [Package your mod into the game folder automatically](#package-your-mod-into-the-game-folder-automatically). -2. Launch the project with debugging in Visual Studio or MonoDevelop. - -This will deploy your mod files into the game folder, launch SMAPI, and attach a debugger -automatically. Now you can step through your code, set breakpoints, etc. - -### Create release zips automatically (Windows-only) -You can create the mod package automatically when you build: - -1. Edit your mod's `.csproj` file. -2. Add this block above the first `` line: - - ```xml - $(SolutionDir)\_releases - ``` - -That's it! Each time you build, the mod files will be zipped into `_releases\.zip`. (You -can change the value to save the zips somewhere else.) - -## Troubleshoot -### "Failed to find the game install path" -That error means the package couldn't figure out where the game is installed. You need to specify -the game location yourself. There's two ways to do that: - -* **Option 1: set the path globally.** - _This will apply to every project that uses version 1.5+ of package._ - - 1. Get the full folder path containing the Stardew Valley executable. - 2. Create this file path: - - platform | path - --------- | ---- - Linux/Mac | `~/stardewvalley.targets` - Windows | `%USERPROFILE%\stardewvalley.targets` - - 3. Save the file with this content: - - ```xml - - - PATH_HERE - - - ``` - - 4. Replace `PATH_HERE` with your custom game install path. - -* **Option 2: set the path in the project file.** - _(You'll need to do it for every project that uses the package.)_ - 1. Get the folder path containing the Stardew Valley `.exe` file. - 2. Add this to your `.csproj` file under the ` - PATH_HERE - - ``` - - 3. Replace `PATH_HERE` with your custom game install path. - -The configuration will check your custom path first, then fall back to the default paths (so it'll -still compile on a different computer). - -## Versions -See [release notes](release-notes.md). diff --git a/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn b/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn deleted file mode 100644 index 7bd5c0c5..00000000 Binary files a/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn and /dev/null differ diff --git a/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png b/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png deleted file mode 100644 index 611cdf88..00000000 Binary files a/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png and /dev/null differ diff --git a/src/StardewModdingAPI.ModBuildConfig/build/smapi.targets b/src/StardewModdingAPI.ModBuildConfig/build/smapi.targets deleted file mode 100644 index a1b6aab3..00000000 --- a/src/StardewModdingAPI.ModBuildConfig/build/smapi.targets +++ /dev/null @@ -1,273 +0,0 @@ - - - - - - - - - - - - - - - A build task which packs mod files into a conventional release zip. - public class CreateModReleaseZip : Task, ITask - { - /********* - ** Accessors - *********/ - /// The mod files to pack. - public ITaskItem[] Files { get; set; } - - /// The name of the mod. - public string ModName { get; set; } - - /// The absolute or relative path to the folder which should contain the generated zip file. - public string OutputFolderPath { get; set; } - - - /********* - ** Public methods - *********/ - public override bool Execute() - { - try - { - // create output path if needed - Directory.CreateDirectory(OutputFolderPath); - - // get zip filename - string fileName = string.Format("{0}-{1}.zip", this.ModName, this.GetManifestVersion()); - - // clear old zip file if present - string zipPath = Path.Combine(OutputFolderPath, fileName); - if (File.Exists(zipPath)) - File.Delete(zipPath); - - // create zip file - using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) - using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) - { - foreach (ITaskItem file in Files) - { - // get file info - string filePath = file.ItemSpec; - string entryName = ModName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); - if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) - entryName = Path.Combine("i18n", entryName); - - // add to zip - using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) - { - fileStream.CopyTo(fileStreamInZip); - } - } - } - - return true; - } - catch (Exception ex) - { - Log.LogErrorFromException(ex); - return false; - } - } - - /// Get a semantic version from the mod manifest (if available). - public string GetManifestVersion() - { - // Get the file JSON string - string json = ""; - foreach(ITaskItem file in Files) - { - if(Path.GetFileName(file.ItemSpec).ToLower() != "manifest.json") - continue; - json = File.ReadAllText(file.ItemSpec); - break; - } - - // Serialize the manifest json into a data object, then get a version object from that. - IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); - IDictionary version = (IDictionary)data["Version"]; - - // Store our version numbers for ease of use - int major = (int)version["MajorVersion"]; - int minor = (int)version["MinorVersion"]; - int patch = (int)version["PatchVersion"]; - - return String.Format("{0}.{1}.{2}", major, minor, patch); - } - } - ]]> - - - - - - - - - - - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - - - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - - - - - - - - - - false - - - false - - - false - - - false - - - $(GamePath)\Stardew Valley.exe - false - - - $(GamePath)\StardewModdingAPI.exe - false - - - $(GamePath)\xTile.dll - false - False - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - - - $(GamePath)\MonoGame.Framework.dll - false - False - - - $(GamePath)\StardewValley.exe - false - - - $(GamePath)\StardewModdingAPI.exe - false - - - $(GamePath)\xTile.dll - false - - - - - - - - - - - - - - - - - - - - - - - - $(GamePath)\Mods\$(DeployModFolderName) - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/StardewModdingAPI.ModBuildConfig/package.nuspec b/src/StardewModdingAPI.ModBuildConfig/package.nuspec deleted file mode 100644 index b8e96481..00000000 --- a/src/StardewModdingAPI.ModBuildConfig/package.nuspec +++ /dev/null @@ -1,22 +0,0 @@ - - - - Pathoschild.Stardew.ModBuildConfig - 1.7.1 - MSBuild config for Stardew Valley mods - Pathoschild - Pathoschild - false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.7.1/LICENSE.txt - https://github.com/Pathoschild/Stardew.ModBuildConfig#readme - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.7.1/assets/nuget-icon.png - Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - - 1.7 added an option to create release zips on build and added a reference to XNA's XACT library for audio-related mods. - 1.7.1 fixed an issue where i18n folders were flattened, and ensures that the manifest/i18n files in the project take precedence over those in the build output if both are present. - - - - - - diff --git a/src/StardewModdingAPI.ModBuildConfig/release-notes.md b/src/StardewModdingAPI.ModBuildConfig/release-notes.md deleted file mode 100644 index ff2734f8..00000000 --- a/src/StardewModdingAPI.ModBuildConfig/release-notes.md +++ /dev/null @@ -1,28 +0,0 @@ -## Release notes -### 1.6 -* Added support for deploying mod files into `Mods` automatically. -* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. - -### 1.5 -* Added support for setting a custom game path globally. -* Added default GOG path on Mac. - -### 1.4 -* Fixed detection of non-default game paths on 32-bit Windows. -* Removed support for SilVerPLuM (discontinued). -* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). - -### 1.3 -* Added support for non-default game paths on Windows. - -### 1.2 -* Exclude game binaries from mod build output. - -### 1.1 -* Added support for overriding the target platform. - -### 1.0 -* Initial release. -* Added support for detecting the game path automatically. -* Added support for injecting XNA/MonoGame references automatically based on the OS. -* Added support for mod builders like SilVerPLuM. diff --git a/src/StardewModdingAPI.Models/ModInfoModel.cs b/src/StardewModdingAPI.Models/ModInfoModel.cs deleted file mode 100644 index 44071230..00000000 --- a/src/StardewModdingAPI.Models/ModInfoModel.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Models -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; } - - /// The mod's semantic version number. - public string Version { get; } - - /// The mod's web URL. - public string Url { get; } - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; } - - - /********* - ** Public methods - *********/ - /// Construct a valid instance. - /// The mod name. - /// The mod's semantic version number. - /// The mod's web URL. - /// The error message indicating why the mod is invalid (if applicable). - [JsonConstructor] - public ModInfoModel(string name, string version, string url, string error = null) - { - this.Name = name; - this.Version = version; - this.Url = url; - this.Error = error; // mainly initialised here for the JSON deserialiser - } - - /// Construct an valid instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) - { - this.Error = error; - } - } -} diff --git a/src/StardewModdingAPI.Models/ModSeachModel.cs b/src/StardewModdingAPI.Models/ModSeachModel.cs deleted file mode 100644 index 526fbaf3..00000000 --- a/src/StardewModdingAPI.Models/ModSeachModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Models -{ - /// Specifies mods whose update-check info to fetch. - internal class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - /// This constructed is needed for JSON deserialisation. - public ModSearchModel() { } - - /// Construct an valid instance. - /// The namespaced mod keys to search. - public ModSearchModel(IEnumerable modKeys) - { - this.ModKeys = modKeys.ToArray(); - } - } -} diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems deleted file mode 100644 index e2cb29e1..00000000 --- a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems +++ /dev/null @@ -1,15 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - - - StardewModdingAPI.Models - - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj deleted file mode 100644 index c80517af..00000000 --- a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - 14.0 - - - - - - - - diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs deleted file mode 100644 index 051ffe99..00000000 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ /dev/null @@ -1,556 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Moq; -using Newtonsoft.Json; -using NUnit.Framework; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Tests.Core -{ - /// Unit tests for . - [TestFixture] - public class ModResolverTests - { - /********* - ** Unit tests - *********/ - /**** - ** ReadManifests - ****/ - [Test(Description = "Assert that the resolver correctly returns an empty list if there are no mods installed.")] - public void ReadBasicManifest_NoMods_ReturnsEmptyList() - { - // arrange - string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(rootFolder); - - // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); - - // assert - Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); - } - - [Test(Description = "Assert that the resolver correctly returns a failed metadata if there's an empty mod folder.")] - public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest() - { - // arrange - string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(modFolder); - - // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); - IModMetadata mod = mods.FirstOrDefault(); - - // assert - Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); - Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed."); - Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); - } - - [Test(Description = "Assert that the resolver correctly reads manifest data from a randomised file.")] - public void ReadBasicManifest_CanReadFile() - { - // create manifest data - IDictionary originalDependency = new Dictionary - { - [nameof(IManifestDependency.UniqueID)] = Sample.String() - }; - IDictionary original = new Dictionary - { - [nameof(IManifest.Name)] = Sample.String(), - [nameof(IManifest.Author)] = Sample.String(), - [nameof(IManifest.Version)] = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - [nameof(IManifest.Description)] = Sample.String(), - [nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}", - [nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll", - [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}", - [nameof(IManifest.Dependencies)] = new[] { originalDependency }, - ["ExtraString"] = Sample.String(), - ["ExtraInt"] = Sample.Int() - }; - - // write to filesystem - string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); - string filename = Path.Combine(modFolder, "manifest.json"); - Directory.CreateDirectory(modFolder); - File.WriteAllText(filename, JsonConvert.SerializeObject(original)); - - // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); - IModMetadata mod = mods.FirstOrDefault(); - - // assert - Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest."); - Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); - Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); - Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); - Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); - Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); - - Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name."); - Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match."); - Assert.AreEqual(original[nameof(IManifest.Author)], mod.Manifest.Author, "The manifest's author doesn't match."); - Assert.AreEqual(original[nameof(IManifest.Description)], mod.Manifest.Description, "The manifest's description doesn't match."); - Assert.AreEqual(original[nameof(IManifest.EntryDll)], mod.Manifest.EntryDll, "The manifest's entry DLL doesn't match."); - Assert.AreEqual(original[nameof(IManifest.MinimumApiVersion)], mod.Manifest.MinimumApiVersion?.ToString(), "The manifest's minimum API version doesn't match."); - Assert.AreEqual(original[nameof(IManifest.Version)]?.ToString(), mod.Manifest.Version?.ToString(), "The manifest's version doesn't match."); - - Assert.IsNotNull(mod.Manifest.ExtraFields, "The extra fields should not be null."); - Assert.AreEqual(2, mod.Manifest.ExtraFields.Count, "The extra fields should contain two values."); - Assert.AreEqual(original["ExtraString"], mod.Manifest.ExtraFields["ExtraString"], "The manifest's extra fields should contain an 'ExtraString' value."); - Assert.AreEqual(original["ExtraInt"], mod.Manifest.ExtraFields["ExtraInt"], "The manifest's extra fields should contain an 'ExtraInt' value."); - - Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null."); - Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value."); - Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match."); - } - - /**** - ** ValidateManifests - ****/ - [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] - public void ValidateManifests_NoMods_DoesNothing() - { - new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - } - - [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] - public void ValidateManifests_Skips_Failed() - { - // arrange - Mock mock = this.GetMetadata("Mod A"); - mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); - - // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - - // assert - mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); - } - - [Test(Description = "Assert that validation fails if the mod has 'assume broken' status.")] - public void ValidateManifests_ModStatus_AssumeBroken_Fails() - { - // arrange - Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModDataRecord - { - Compatibility = new[] { new ModCompatibility("~1.0", ModStatus.AssumeBroken, null) }, - AlternativeUrl = "http://example.org" - }); - - // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - - // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); - } - - [Test(Description = "Assert that validation fails when the minimum API version is higher than the current SMAPI version.")] - public void ValidateManifests_MinimumApiVersion_Fails() - { - // arrange - Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1"))); - this.SetupMetadataForValidation(mock); - - // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - - // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); - } - - [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] - public void ValidateManifests_MissingEntryDLL_Fails() - { - // arrange - Mock mock = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.EntryDll = "Missing.dll"), allowStatusChange: true); - this.SetupMetadataForValidation(mock); - - // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - - // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); - } - - [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] - public void ValidateManifests_DuplicateUniqueID_Fails() - { - // arrange - Mock modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - Mock modB = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.Name = "Mod B"), allowStatusChange: true); - Mock modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false); - foreach (Mock mod in new[] { modA, modB, modC }) - this.SetupMetadataForValidation(mod); - - // act - new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - - // assert - modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); - } - - [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] - public void ValidateManifests_Valid_Passes() - { - // set up manifest - IManifest manifest = this.GetManifest(); - - // create DLL - string modFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(modFolder); - File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), ""); - - // arrange - Mock mock = new Mock(MockBehavior.Strict); - mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mock.Setup(p => p.DataRecord).Returns(() => null); - mock.Setup(p => p.Manifest).Returns(manifest); - mock.Setup(p => p.DirectoryPath).Returns(modFolder); - - // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); - - // assert - // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. - } - - /**** - ** ProcessDependencies - ****/ - [Test(Description = "Assert that processing dependencies doesn't fail if there are no mods installed.")] - public void ProcessDependencies_NoMods_DoesNothing() - { - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0]).ToArray(); - - // assert - Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods."); - } - - [Test(Description = "Assert that processing dependencies doesn't change the order if there are no mod dependencies.")] - public void ProcessDependencies_NoDependencies_DoesNothing() - { - // arrange - // A B C - Mock modA = this.GetMetadata("Mod A"); - Mock modB = this.GetMetadata("Mod B"); - Mock modC = this.GetMetadata("Mod C"); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object, modC.Object }).ToArray(); - - // assert - Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order unexpectedly changed with no dependencies."); - Assert.AreSame(modB.Object, mods[1], "The load order unexpectedly changed with no dependencies."); - Assert.AreSame(modC.Object, mods[2], "The load order unexpectedly changed with no dependencies."); - } - - [Test(Description = "Assert that processing dependencies skips mods that have already failed without calling any other properties.")] - public void ProcessDependencies_Skips_Failed() - { - // arrange - Mock mock = new Mock(MockBehavior.Strict); - mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); - - // act - new ModResolver().ProcessDependencies(new[] { mock.Object }); - - // assert - mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); - } - - [Test(Description = "Assert that simple dependencies are reordered correctly.")] - public void ProcessDependencies_Reorders_SimpleDependencies() - { - // arrange - // A ◀── B - // ▲ ▲ - // │ │ - // └─ C ─┘ - Mock modA = this.GetMetadata("Mod A"); - Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod A", "Mod B" }); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }).ToArray(); - - // assert - Assert.AreEqual(3, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since the other mods depend on it."); - Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); - Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs both mod A and mod B."); - } - - [Test(Description = "Assert that simple dependency chains are reordered correctly.")] - public void ProcessDependencies_Reorders_DependencyChain() - { - // arrange - // A ◀── B ◀── C ◀── D - Mock modA = this.GetMetadata("Mod A"); - Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); - Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); - - // assert - Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); - Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); - Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs mod B, and is needed by mod D."); - Assert.AreSame(modD.Object, mods[3], "The load order is incorrect: mod D should be fourth since it needs mod C."); - } - - [Test(Description = "Assert that overlapping dependency chains are reordered correctly.")] - public void ProcessDependencies_Reorders_OverlappingDependencyChain() - { - // arrange - // A ◀── B ◀── C ◀── D - // ▲ ▲ - // │ │ - // E ◀── F - Mock modA = this.GetMetadata("Mod A"); - Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); - Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); - Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod B" }); - Mock modF = this.GetMetadata("Mod F", dependencies: new[] { "Mod C", "Mod E" }); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }).ToArray(); - - // assert - Assert.AreEqual(6, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); - Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A, and is needed by mod C."); - Assert.AreSame(modC.Object, mods[2], "The load order is incorrect: mod C should be third since it needs mod B, and is needed by mod D."); - Assert.AreSame(modD.Object, mods[3], "The load order is incorrect: mod D should be fourth since it needs mod C."); - Assert.AreSame(modE.Object, mods[4], "The load order is incorrect: mod E should be fifth since it needs mod B, but is specified after C which also needs mod B."); - Assert.AreSame(modF.Object, mods[5], "The load order is incorrect: mod F should be last since it needs mods E and C."); - } - - [Test(Description = "Assert that mods with circular dependency chains are skipped, but any other mods are loaded in the correct order.")] - public void ProcessDependencies_Skips_CircularDependentMods() - { - // arrange - // A ◀── B ◀── C ──▶ D - // ▲ │ - // │ ▼ - // └──── E - Mock modA = this.GetMetadata("Mod A"); - Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B", "Mod D" }, allowStatusChange: true); - Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod E" }, allowStatusChange: true); - Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }).ToArray(); - - // assert - Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); - Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); - modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); - modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); - modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); - } - - [Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")] - public void ProcessDependencies_WithSomeFailedMods_Succeeds() - { - // arrange - // A ◀── B ◀── C D (failed) - Mock modA = this.GetMetadata("Mod A"); - Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); - Mock modD = new Mock(MockBehavior.Strict); - modD.Setup(p => p.Manifest).Returns(null); - modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); - - // assert - Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modD.Object, mods[0], "The load order is incorrect: mod D should be first since it was already failed."); - Assert.AreSame(modA.Object, mods[1], "The load order is incorrect: mod A should be second since it's needed by mod B."); - Assert.AreSame(modB.Object, mods[2], "The load order is incorrect: mod B should be third since it needs mod A, and is needed by mod C."); - Assert.AreSame(modC.Object, mods[3], "The load order is incorrect: mod C should be fourth since it needs mod B, and is needed by mod D."); - } - - [Test(Description = "Assert that dependencies are failed if they don't meet the minimum version.")] - public void ProcessDependencies_WithMinVersions_FailsIfNotMet() - { - // arrange - // A 1.0 ◀── B (need A 1.1) - Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); - - // assert - Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); - } - - [Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")] - public void ProcessDependencies_WithMinVersions_SucceedsIfMet() - { - // arrange - // A 1.0 ◀── B (need A 1.0-beta) - Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); - - // assert - Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); - Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); - } - - [Test(Description = "Assert that optional dependencies are sorted correctly if present.")] - public void ProcessDependencies_IfOptional() - { - // arrange - // A ◀── B - Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }).ToArray(); - - // assert - Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); - Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); - } - - [Test(Description = "Assert that optional dependencies are accepted if they're missing.")] - public void ProcessDependencies_IfOptional_SucceedsIfMissing() - { - // arrange - // A ◀── B where A doesn't exist - Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); - - // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }).ToArray(); - - // assert - Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); - Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod."); - } - - - /********* - ** Private methods - *********/ - /// Get a randomised basic manifest. - /// Adjust the generated manifest. - private Manifest GetManifest(Action adjust = null) - { - Manifest manifest = new Manifest - { - Name = Sample.String(), - Author = Sample.String(), - Version = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - Description = Sample.String(), - UniqueID = $"{Sample.String()}.{Sample.String()}", - EntryDll = $"{Sample.String()}.dll" - }; - adjust?.Invoke(manifest); - return manifest; - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The mod version. - /// Adjust the generated manifest. - /// The dependencies this mod requires. - private IManifest GetManifest(string uniqueID, string version, Action adjust, params IManifestDependency[] dependencies) - { - return this.GetManifest(manifest => - { - manifest.Name = uniqueID; - manifest.UniqueID = uniqueID; - manifest.Version = new SemanticVersion(version); - manifest.Dependencies = dependencies; - adjust?.Invoke(manifest); - }); - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The mod version. - /// The dependencies this mod requires. - private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies) - { - return this.GetManifest(uniqueID, version, null, dependencies); - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - private Mock GetMetadata(string uniqueID) - { - return this.GetMetadata(this.GetManifest(uniqueID, "1.0")); - } - - /// Get a randomised basic manifest. - /// The mod's name and unique ID. - /// The dependencies this mod requires. - /// Whether the code being tested is allowed to change the mod status. - private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) - { - IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); - return this.GetMetadata(manifest, allowStatusChange); - } - - /// Get a randomised basic manifest. - /// The mod manifest. - /// Whether the code being tested is allowed to change the mod status. - private Mock GetMetadata(IManifest manifest, bool allowStatusChange = false) - { - Mock mod = new Mock(MockBehavior.Strict); - mod.Setup(p => p.DataRecord).Returns(() => null); - mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); - mod.Setup(p => p.Manifest).Returns(manifest); - if (allowStatusChange) - { - mod - .Setup(p => p.SetStatus(It.IsAny(), It.IsAny())) - .Callback((status, message) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}")) - .Returns(mod.Object); - } - return mod; - } - - /// Set up a mock mod metadata for . - /// The mock mod metadata. - /// The extra metadata about the mod from SMAPI's internal data (if any). - private void SetupMetadataForValidation(Mock mod, ModDataRecord modRecord = null) - { - mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.DataRecord).Returns(() => null); - mod.Setup(p => p.Manifest).Returns(this.GetManifest()); - mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); - mod.Setup(p => p.DataRecord).Returns(modRecord); - } - } -} diff --git a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs deleted file mode 100644 index 63404a41..00000000 --- a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs +++ /dev/null @@ -1,356 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using StardewModdingAPI.Framework.ModHelpers; -using StardewValley; - -namespace StardewModdingAPI.Tests.Core -{ - /// Unit tests for and . - [TestFixture] - public class TranslationTests - { - /********* - ** Data - *********/ - /// Sample translation text for unit tests. - public static string[] Samples = { null, "", " ", "boop", " boop " }; - - - /********* - ** Unit tests - *********/ - /**** - ** Translation helper - ****/ - [Test(Description = "Assert that the translation helper correctly handles no translations.")] - public void Helper_HandlesNoTranslations() - { - // arrange - var data = new Dictionary>(); - - // act - ITranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); - Translation translation = helper.Get("key"); - Translation[] translationList = helper.GetTranslations()?.ToArray(); - - // assert - Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); - Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); - Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null."); - Assert.AreEqual(0, translationList.Length, "The full list of translations is unexpectedly not empty."); - - Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); - Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); - } - - [Test(Description = "Assert that the translation helper returns the expected translations correctly.")] - public void Helper_GetTranslations_ReturnsExpectedText() - { - // arrange - var data = this.GetSampleData(); - var expected = this.GetExpectedTranslations(); - - // act - var actual = new Dictionary(); - TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); - foreach (string locale in expected.Keys) - { - this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); - actual[locale] = helper.GetTranslations()?.ToArray(); - } - - // assert - foreach (string locale in expected.Keys) - { - Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); - Assert.That(actual[locale], Is.EquivalentTo(expected[locale]).Using(this.CompareEquality), $"The translations for {locale} don't match the expected values."); - } - } - - [Test(Description = "Assert that the translations returned by the helper has the expected text.")] - public void Helper_Get_ReturnsExpectedText() - { - // arrange - var data = this.GetSampleData(); - var expected = this.GetExpectedTranslations(); - - // act - var actual = new Dictionary(); - TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); - foreach (string locale in expected.Keys) - { - this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); - - List translations = new List(); - foreach (Translation translation in expected[locale]) - translations.Add(helper.Get(translation.Key)); - actual[locale] = translations.ToArray(); - } - - // assert - foreach (string locale in expected.Keys) - { - Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); - Assert.That(actual[locale], Is.EquivalentTo(expected[locale]).Using(this.CompareEquality), $"The translations for {locale} don't match the expected values."); - } - } - - /**** - ** Translation - ****/ - [Test(Description = "Assert that HasValue returns the expected result for various inputs.")] - [TestCase(null, ExpectedResult = false)] - [TestCase("", ExpectedResult = false)] - [TestCase(" ", ExpectedResult = true)] - [TestCase("boop", ExpectedResult = true)] - [TestCase(" boop ", ExpectedResult = true)] - public bool Translation_HasValue(string text) - { - return new Translation("ModName", "pt-BR", "key", text).HasValue(); - } - - [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] - public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) - { - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text); - - // assert - if (translation.HasValue()) - Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); - else - Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input."); - } - - [Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")] - public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) - { - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text); - - // assert - if (translation.HasValue()) - Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input."); - else - Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input."); - } - - [Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")] - public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) - { - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text).UsePlaceholder(value); - - // assert - if (translation.HasValue()) - Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); - else if (!value) - Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder disabled."); - else - Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled."); - } - - [Test(Description = "Assert that the translation's Assert method throws the expected exception.")] - public void Translation_Assert([ValueSource(nameof(TranslationTests.Samples))] string text) - { - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text); - - // assert - if (translation.HasValue()) - Assert.That(() => translation.Assert(), Throws.Nothing, "The assert unexpected threw an exception for a valid input."); - else - Assert.That(() => translation.Assert(), Throws.Exception.TypeOf(), "The assert didn't throw an exception for invalid input."); - } - - [Test(Description = "Assert that the translation returns the expected text after setting the default.")] - public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) - { - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text).Default(@default); - - // assert - if (!string.IsNullOrEmpty(text)) - Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid base text."); - else if (!string.IsNullOrEmpty(@default)) - Assert.AreEqual(@default, translation.ToString(), "The translation returned an unexpected value given a null or empty base text, but valid default."); - else - Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty base and default text."); - } - - /**** - ** Translation tokens - ****/ - [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] - public void Translation_Tokens([Values("anonymous object", "class", "IDictionary", "IDictionary")] string structure) - { - // arrange - string start = Guid.NewGuid().ToString("N"); - string middle = Guid.NewGuid().ToString("N"); - string end = Guid.NewGuid().ToString("N"); - const string input = "{{start}} tokens are properly replaced (including {{middle}} {{ MIDdlE}}) {{end}}"; - string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; - - // act - Translation translation = new Translation("ModName", "pt-BR", "key", input); - switch (structure) - { - case "anonymous object": - translation = translation.Tokens(new { start, middle, end }); - break; - - case "class": - translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end }); - break; - - case "IDictionary": - translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); - break; - - case "IDictionary": - translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); - break; - - default: - throw new NotSupportedException($"Unknown structure '{structure}'."); - } - - // assert - Assert.AreEqual(expected, translation.ToString(), "The translation returned an unexpected text."); - } - - [Test(Description = "Assert that the translation can replace tokens in all valid formats.")] - [TestCase("{{value}}", "value")] - [TestCase("{{ value }}", "value")] - [TestCase("{{value }}", "value")] - [TestCase("{{ the_value }}", "the_value")] - [TestCase("{{ the.value_here }}", "the.value_here")] - [TestCase("{{ the_value-here.... }}", "the_value-here....")] - [TestCase("{{ tHe_vALuE-HEre.... }}", "tHe_vALuE-HEre....")] - public void Translation_Tokens_ValidFormats(string text, string key) - { - // arrange - string value = Guid.NewGuid().ToString("N"); - - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); - - // assert - Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); - } - - [Test(Description = "Assert that translation tokens are case-insensitive and surrounding-whitespace-insensitive.")] - [TestCase("{{value}}", "value")] - [TestCase("{{VaLuE}}", "vAlUe")] - [TestCase("{{VaLuE }}", " vAlUe")] - public void Translation_Tokens_KeysAreNormalised(string text, string key) - { - // arrange - string value = Guid.NewGuid().ToString("N"); - - // act - Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); - - // assert - Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); - } - - - /********* - ** Private methods - *********/ - /// Set a translation helper's locale and assert that it was set correctly. - /// The translation helper to change. - /// The expected locale. - /// The expected game language code. - private void AssertSetLocale(TranslationHelper helper, string locale, LocalizedContentManager.LanguageCode localeEnum) - { - helper.SetLocale(locale, localeEnum); - Assert.AreEqual(locale, helper.Locale, "The locale doesn't match the input value."); - Assert.AreEqual(localeEnum, helper.LocaleEnum, "The locale enum doesn't match the input value."); - } - - /// Get sample raw translations to input. - private IDictionary> GetSampleData() - { - return new Dictionary> - { - ["default"] = new Dictionary - { - ["key A"] = "default A", - ["key C"] = "default C" - }, - ["en"] = new Dictionary - { - ["key A"] = "en A", - ["key B"] = "en B" - }, - ["en-US"] = new Dictionary(), - ["zzz"] = new Dictionary - { - ["key A"] = "zzz A" - } - }; - } - - /// Get the expected translation output given , based on the expected locale fallback. - private IDictionary GetExpectedTranslations() - { - var expected = new Dictionary - { - ["default"] = new[] - { - new Translation(string.Empty, "default", "key A", "default A"), - new Translation(string.Empty, "default", "key C", "default C") - }, - ["en"] = new[] - { - new Translation(string.Empty, "en", "key A", "en A"), - new Translation(string.Empty, "en", "key B", "en B"), - new Translation(string.Empty, "en", "key C", "default C") - }, - ["zzz"] = new[] - { - new Translation(string.Empty, "zzz", "key A", "zzz A"), - new Translation(string.Empty, "zzz", "key C", "default C") - } - }; - expected["en-us"] = expected["en"].ToArray(); - return expected; - } - - /// Get whether two translations have the same public values. - /// The first translation to compare. - /// The second translation to compare. - private bool CompareEquality(Translation a, Translation b) - { - return a.Key == b.Key && a.ToString() == b.ToString(); - } - - /// Get the default placeholder text when a translation is missing. - /// The translation key. - private string GetPlaceholderText(string key) - { - return string.Format(Translation.PlaceholderText, key); - } - - - /********* - ** Test models - *********/ - /// A model used to test token support. - private class TokenModel - { - /// A sample token property. - public string Start { get; set; } - - /// A sample token property. - public string Middle { get; set; } - - /// A sample token field. - public string End; - } - } -} diff --git a/src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index ee09145b..00000000 --- a/src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("StardewModdingAPI.Tests")] -[assembly: AssemblyDescription("")] -[assembly: Guid("36ccb19e-92eb-48c7-9615-98eefd45109b")] diff --git a/src/StardewModdingAPI.Tests/Sample.cs b/src/StardewModdingAPI.Tests/Sample.cs deleted file mode 100644 index 99835d92..00000000 --- a/src/StardewModdingAPI.Tests/Sample.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace StardewModdingAPI.Tests -{ - /// Provides sample values for unit testing. - internal static class Sample - { - /********* - ** Properties - *********/ - /// A random number generator. - private static readonly Random Random = new Random(); - - - /********* - ** Properties - *********/ - /// Get a sample string. - public static string String() - { - return Guid.NewGuid().ToString("N"); - } - - /// Get a sample integer. - public static int Int() - { - return Sample.Random.Next(); - } - } -} diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj deleted file mode 100644 index 41525bcb..00000000 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Debug - x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B} - Library - Properties - StardewModdingAPI.Tests - StardewModdingAPI.Tests - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll - - - ..\packages\Moq.4.7.99\lib\net45\Moq.dll - - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - - - ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} - StardewModdingAPI - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs b/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs deleted file mode 100644 index 25acbaf3..00000000 --- a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.RegularExpressions; -using NUnit.Framework; -using StardewModdingAPI.Utilities; - -namespace StardewModdingAPI.Tests.Utilities -{ - /// Unit tests for . - [TestFixture] - internal class SDateTests - { - /********* - ** Properties - *********/ - /// All valid seasons. - private static readonly string[] ValidSeasons = { "spring", "summer", "fall", "winter" }; - - /// All valid days of a month. - private static readonly int[] ValidDays = Enumerable.Range(1, 28).ToArray(); - - /// Sample relative dates for test cases. - private static class Dates - { - /// The base date to which other dates are relative. - public const string Now = "02 summer Y2"; - - /// The day before . - public const string PrevDay = "01 summer Y2"; - - /// The month before . - public const string PrevMonth = "02 spring Y2"; - - /// The year before . - public const string PrevYear = "02 summer Y1"; - - /// The day after . - public const string NextDay = "03 summer Y2"; - - /// The month after . - public const string NextMonth = "02 fall Y2"; - - /// The year after . - public const string NextYear = "02 summer Y3"; - } - - - /********* - ** Unit tests - *********/ - /**** - ** Constructor - ****/ - [Test(Description = "Assert that the constructor sets the expected values for all valid dates.")] - public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.ValidSeasons))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year) - { - // act - SDate date = new SDate(day, season, year); - - // assert - Assert.AreEqual(day, date.Day); - Assert.AreEqual(season, date.Season); - Assert.AreEqual(year, date.Year); - } - - [Test(Description = "Assert that the constructor throws an exception if the values are invalid.")] - [TestCase(01, "Spring", 1)] // seasons are case-sensitive - [TestCase(01, "springs", 1)] // invalid season name - [TestCase(-1, "spring", 1)] // day < 0 - [TestCase(29, "spring", 1)] // day > 28 - [TestCase(01, "spring", -1)] // year < 1 - [TestCase(01, "spring", 0)] // year < 1 - [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")] - public void Constructor_RejectsInvalidValues(int day, string season, int year) - { - // act & assert - Assert.Throws(() => _ = new SDate(day, season, year), "Constructing the invalid date didn't throw the expected exception."); - } - - /**** - ** ToString - ****/ - [Test(Description = "Assert that ToString returns the expected string.")] - [TestCase("14 spring Y1", ExpectedResult = "14 spring Y1")] - [TestCase("01 summer Y16", ExpectedResult = "01 summer Y16")] - [TestCase("28 fall Y10", ExpectedResult = "28 fall Y10")] - [TestCase("01 winter Y1", ExpectedResult = "01 winter Y1")] - public string ToString(string dateStr) - { - return this.GetDate(dateStr).ToString(); - } - - /**** - ** AddDays - ****/ - [Test(Description = "Assert that AddDays returns the expected date.")] - [TestCase("01 spring Y1", 15, ExpectedResult = "16 spring Y1")] // day transition - [TestCase("01 spring Y1", 28, ExpectedResult = "01 summer Y1")] // season transition - [TestCase("01 spring Y1", 28 * 4, ExpectedResult = "01 spring Y2")] // year transition - [TestCase("01 spring Y1", 28 * 7 + 17, ExpectedResult = "18 winter Y2")] // year transition - [TestCase("15 spring Y1", -14, ExpectedResult = "01 spring Y1")] // negative day transition - [TestCase("15 summer Y1", -28, ExpectedResult = "15 spring Y1")] // negative season transition - [TestCase("15 summer Y2", -28 * 4, ExpectedResult = "15 summer Y1")] // negative year transition - [TestCase("01 spring Y3", -(28 * 7 + 17), ExpectedResult = "12 spring Y1")] // negative year transition - [TestCase("06 fall Y2", 50, ExpectedResult = "28 winter Y3")] // test for zero-index errors - [TestCase("06 fall Y2", 51, ExpectedResult = "01 spring Y3")] // test for zero-index errors - public string AddDays(string dateStr, int addDays) - { - return this.GetDate(dateStr).AddDays(addDays).ToString(); - } - - /**** - ** GetHashCode - ****/ - [Test(Description = "Assert that GetHashCode returns a unique ordered value for every date.")] - public void GetHashCode_ReturnsUniqueOrderedValue() - { - IDictionary hashes = new Dictionary(); - int lastHash = int.MinValue; - for (int year = 1; year <= 4; year++) - { - foreach (string season in SDateTests.ValidSeasons) - { - foreach (int day in SDateTests.ValidDays) - { - SDate date = new SDate(day, season, year); - int hash = date.GetHashCode(); - if (hashes.TryGetValue(hash, out SDate otherDate)) - Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}."); - if (hash < lastHash) - Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash})."); - - lastHash = hash; - hashes[hash] = date; - } - } - } - } - - [Test(Description = "Assert that the == operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] - [TestCase(Dates.Now, null, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.Now, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] - public bool Operators_Equals(string now, string other) - { - return this.GetDate(now) == this.GetDate(other); - } - - [Test(Description = "Assert that the != operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] - [TestCase(Dates.Now, null, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] - public bool Operators_NotEquals(string now, string other) - { - return this.GetDate(now) != this.GetDate(other); - } - - [Test(Description = "Assert that the < operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] - [TestCase(Dates.Now, null, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] - public bool Operators_LessThan(string now, string other) - { - return this.GetDate(now) < this.GetDate(other); - } - - [Test(Description = "Assert that the <= operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] - [TestCase(Dates.Now, null, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.Now, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] - public bool Operators_LessThanOrEqual(string now, string other) - { - return this.GetDate(now) <= this.GetDate(other); - } - - [Test(Description = "Assert that the > operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] - [TestCase(Dates.Now, null, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] - public bool Operators_MoreThan(string now, string other) - { - return this.GetDate(now) > this.GetDate(other); - } - - [Test(Description = "Assert that the > operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] - [TestCase(Dates.Now, null, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevMonth, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.PrevYear, ExpectedResult = true)] - [TestCase(Dates.Now, Dates.Now, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] - [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] - public bool Operators_MoreThanOrEqual(string now, string other) - { - return this.GetDate(now) > this.GetDate(other); - } - - - /********* - ** Private methods - *********/ - /// Convert a string date into a game date, to make unit tests easier to read. - /// The date string like "dd MMMM yy". - private SDate GetDate(string dateStr) - { - if (dateStr == null) - return null; - - void Fail(string reason) => throw new AssertionException($"Couldn't parse date '{dateStr}' because {reason}."); - - // parse - Match match = Regex.Match(dateStr, @"^(?\d+) (?\w+) Y(?\d+)$"); - if (!match.Success) - Fail("it doesn't match expected pattern (should be like 28 spring Y1)"); - - // extract parts - string season = match.Groups["season"].Value; - if (!int.TryParse(match.Groups["day"].Value, out int day)) - Fail($"'{match.Groups["day"].Value}' couldn't be parsed as a day."); - if (!int.TryParse(match.Groups["year"].Value, out int year)) - Fail($"'{match.Groups["year"].Value}' couldn't be parsed as a year."); - - // build date - return new SDate(day, season, year); - } - } -} diff --git a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs b/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs deleted file mode 100644 index 03cd26c9..00000000 --- a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; -using NUnit.Framework; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Tests.Utilities -{ - /// Unit tests for . - [TestFixture] - internal class SemanticVersionTests - { - /********* - ** Unit tests - *********/ - /**** - ** Constructor - ****/ - [Test(Description = "Assert that the constructor sets the expected values for all valid versions.")] - [TestCase("1.0", ExpectedResult = "1.0")] - [TestCase("1.0.0", ExpectedResult = "1.0")] - [TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")] - [TestCase("1.2-some-tag.4", ExpectedResult = "1.2-some-tag.4")] - [TestCase("1.2.3-some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] - [TestCase("1.2.3-some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] - public string Constructor_FromString(string input) - { - return new SemanticVersion(input).ToString(); - } - - [Test(Description = "Assert that the constructor sets the expected values for all valid versions.")] - [TestCase(1, 0, 0, null, ExpectedResult = "1.0")] - [TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] - [TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] - [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] - [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] - [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] - public string Constructor_FromParts(int major, int minor, int patch, string tag) - { - // act - ISemanticVersion version = new SemanticVersion(major, minor, patch, tag); - - // assert - Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); - Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); - Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); - Assert.AreEqual(string.IsNullOrWhiteSpace(tag) ? null : tag.Trim(), version.Build, "The tag doesn't match the given value."); - return version.ToString(); - } - - [Test(Description = "Assert that the constructor throws the expected exception for invalid versions.")] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - [TestCase("1")] - [TestCase("01.0")] - [TestCase("1.05")] - [TestCase("1.5.06")] // leading zeros specifically prohibited by spec - [TestCase("1.2.3.4")] - [TestCase("1.apple")] - [TestCase("1.2.apple")] - [TestCase("1.2.3.apple")] - [TestCase("1..2..3")] - [TestCase("1.2.3-")] - [TestCase("1.2.3-some-tag...")] - [TestCase("1.2.3-some-tag...4")] - [TestCase("apple")] - [TestCase("-apple")] - [TestCase("-5")] - public void Constructor_FromString_WithInvalidValues(string input) - { - if (input == null) - this.AssertAndLogException(() => new SemanticVersion(input)); - else - this.AssertAndLogException(() => new SemanticVersion(input)); - } - - /**** - ** CompareTo - ****/ - [Test(Description = "Assert that version.CompareTo returns the expected value.")] - // equal - [TestCase("0.5.7", "0.5.7", ExpectedResult = 0)] - [TestCase("1.0", "1.0", ExpectedResult = 0)] - [TestCase("1.0-beta", "1.0-beta", ExpectedResult = 0)] - [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = 0)] - [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = 0)] - - // less than - [TestCase("0.5.7", "0.5.8", ExpectedResult = -1)] - [TestCase("1.0", "1.1", ExpectedResult = -1)] - [TestCase("1.0-beta", "1.0", ExpectedResult = -1)] - [TestCase("1.0-beta", "1.0-beta.2", ExpectedResult = -1)] - [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = -1)] - [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = -1)] - [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = -1)] - - // more than - [TestCase("0.5.8", "0.5.7", ExpectedResult = 1)] - [TestCase("1.1", "1.0", ExpectedResult = 1)] - [TestCase("1.0", "1.0-beta", ExpectedResult = 1)] - [TestCase("1.0-beta.2", "1.0-beta", ExpectedResult = 1)] - [TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = 1)] - [TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = 1)] - [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = 1)] - public int CompareTo(string versionStrA, string versionStrB) - { - ISemanticVersion versionA = new SemanticVersion(versionStrA); - ISemanticVersion versionB = new SemanticVersion(versionStrB); - return versionA.CompareTo(versionB); - } - - /**** - ** IsOlderThan - ****/ - [Test(Description = "Assert that version.IsOlderThan returns the expected value.")] - // keep test cases in sync with CompareTo for simplicity. - // equal - [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] - [TestCase("1.0", "1.0", ExpectedResult = false)] - [TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)] - [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)] - [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)] - - // less than - [TestCase("0.5.7", "0.5.8", ExpectedResult = true)] - [TestCase("1.0", "1.1", ExpectedResult = true)] - [TestCase("1.0-beta", "1.0", ExpectedResult = true)] - [TestCase("1.0-beta", "1.0-beta.2", ExpectedResult = true)] - [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = true)] - [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = true)] - [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = true)] - - // more than - [TestCase("0.5.8", "0.5.7", ExpectedResult = false)] - [TestCase("1.1", "1.0", ExpectedResult = false)] - [TestCase("1.0", "1.0-beta", ExpectedResult = false)] - [TestCase("1.0-beta.2", "1.0-beta", ExpectedResult = false)] - [TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = false)] - [TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = false)] - [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = false)] - public bool IsOlderThan(string versionStrA, string versionStrB) - { - ISemanticVersion versionA = new SemanticVersion(versionStrA); - ISemanticVersion versionB = new SemanticVersion(versionStrB); - return versionA.IsOlderThan(versionB); - } - - /**** - ** IsNewerThan - ****/ - [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] - // keep test cases in sync with CompareTo for simplicity. - // equal - [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] - [TestCase("1.0", "1.0", ExpectedResult = false)] - [TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)] - [TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)] - [TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)] - - // less than - [TestCase("0.5.7", "0.5.8", ExpectedResult = false)] - [TestCase("1.0", "1.1", ExpectedResult = false)] - [TestCase("1.0-beta", "1.0", ExpectedResult = false)] - [TestCase("1.0-beta", "1.0-beta.2", ExpectedResult = false)] - [TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = false)] - [TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = false)] - [TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = false)] - - // more than - [TestCase("0.5.8", "0.5.7", ExpectedResult = true)] - [TestCase("1.1", "1.0", ExpectedResult = true)] - [TestCase("1.0", "1.0-beta", ExpectedResult = true)] - [TestCase("1.0-beta.2", "1.0-beta", ExpectedResult = true)] - [TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = true)] - [TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = true)] - [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = true)] - public bool IsNewerThan(string versionStrA, string versionStrB) - { - ISemanticVersion versionA = new SemanticVersion(versionStrA); - ISemanticVersion versionB = new SemanticVersion(versionStrB); - return versionA.IsNewerThan(versionB); - } - - /**** - ** IsBetween - ****/ - [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] - // is between - [TestCase("0.5.7-beta.3", "0.5.7-beta.3", "0.5.7-beta.3", ExpectedResult = true)] - [TestCase("1.0", "1.0", "1.1", ExpectedResult = true)] - [TestCase("1.0", "1.0-beta", "1.1", ExpectedResult = true)] - [TestCase("1.0", "0.5", "1.1", ExpectedResult = true)] - [TestCase("1.0-beta.2", "1.0-beta.1", "1.0-beta.3", ExpectedResult = true)] - [TestCase("1.0-beta-2", "1.0-beta-1", "1.0-beta-3", ExpectedResult = true)] - - // is not between - [TestCase("1.0-beta", "1.0", "1.1", ExpectedResult = false)] - [TestCase("1.0", "1.1", "1.0", ExpectedResult = false)] - [TestCase("1.0-beta.2", "1.1", "1.0", ExpectedResult = false)] - [TestCase("1.0-beta.2", "1.0-beta.10", "1.0-beta.3", ExpectedResult = false)] - [TestCase("1.0-beta-2", "1.0-beta-10", "1.0-beta-3", ExpectedResult = false)] - public bool IsBetween(string versionStr, string lowerStr, string upperStr) - { - ISemanticVersion lower = new SemanticVersion(lowerStr); - ISemanticVersion upper = new SemanticVersion(upperStr); - ISemanticVersion version = new SemanticVersion(versionStr); - return version.IsBetween(lower, upper); - } - - /**** - ** Serialisable - ****/ - [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] - [TestCase("1.0")] - public void Serialisable(string versionStr) - { - // act - string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr)); - SemanticVersion after = JsonConvert.DeserializeObject(json); - - // assert - Assert.IsNotNull(after, "The semantic version after deserialisation is unexpectedly null."); - Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialisation doesn't match the input version."); - } - - /**** - ** GameVersion - ****/ - [Test(Description = "Assert that the GameVersion subclass correctly parses legacy game versions.")] - [TestCase("1.0")] - [TestCase("1.01")] - [TestCase("1.02")] - [TestCase("1.03")] - [TestCase("1.04")] - [TestCase("1.05")] - [TestCase("1.051")] - [TestCase("1.051b")] - [TestCase("1.06")] - [TestCase("1.07")] - [TestCase("1.07a")] - [TestCase("1.1")] - [TestCase("1.11")] - [TestCase("1.2")] - [TestCase("1.2.15")] - public void GameVersion(string versionStr) - { - // act - GameVersion version = new GameVersion(versionStr); - - // assert - Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value."); - Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); - } - - - /********* - ** Private methods - *********/ - /// Assert that the expected exception type is thrown, and log the action output and thrown exception. - /// The expected exception type. - /// The action which may throw the exception. - /// The message to log if the expected exception isn't thrown. - [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")] - private void AssertAndLogException(Func action, string message = null) - where T : Exception - { - this.AssertAndLogException(() => - { - object result = action(); - TestContext.WriteLine($"Func result: {result}"); - }); - } - - /// Assert that the expected exception type is thrown, and log the thrown exception. - /// The expected exception type. - /// The action which may throw the exception. - /// The message to log if the expected exception isn't thrown. - [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")] - private void AssertAndLogException(Action action, string message = null) - where T : Exception - { - try - { - action(); - } - catch (T ex) - { - TestContext.WriteLine($"Exception thrown:\n{ex}"); - return; - } - catch (Exception ex) when (!(ex is AssertionException)) - { - TestContext.WriteLine($"Exception thrown:\n{ex}"); - Assert.Fail(message ?? $"Didn't throw the expected exception; expected {typeof(T).FullName}, got {ex.GetType().FullName}."); - } - - // no exception thrown - Assert.Fail(message ?? "Didn't throw an exception."); - } - } -} diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/StardewModdingAPI.Tests/packages.config deleted file mode 100644 index 5fdfebdb..00000000 --- a/src/StardewModdingAPI.Tests/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs deleted file mode 100644 index 7dcfcf13..00000000 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using StardewModdingAPI.Models; -using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.ModRepositories; - -namespace StardewModdingAPI.Web.Controllers -{ - /// Provides an API to perform mod update checks. - [Produces("application/json")] - [Route("api/{version:semanticVersion}/[controller]")] - internal class ModsController : Controller - { - /********* - ** Properties - *********/ - /// The mod repositories which provide mod metadata. - private readonly IDictionary Repositories; - - /// The cache in which to store mod metadata. - private readonly IMemoryCache Cache; - - /// The number of minutes update checks should be cached before refetching them. - private readonly int CacheMinutes; - - /// A regex which matches SMAPI-style semantic version. - private readonly string VersionRegex; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The cache in which to store mod metadata. - /// The config settings for mod update checks. - public ModsController(IMemoryCache cache, IOptions configProvider) - { - ModUpdateCheckConfig config = configProvider.Value; - - this.Cache = cache; - this.CacheMinutes = config.CacheMinutes; - this.VersionRegex = config.SemanticVersionRegex; - - string version = this.GetType().Assembly.GetName().Version.ToString(3); - this.Repositories = - new IModRepository[] - { - new ChucklefishRepository( - vendorKey: config.ChucklefishKey, - userAgent: string.Format(config.ChucklefishUserAgent, version), - baseUrl: config.ChucklefishBaseUrl, - modPageUrlFormat: config.ChucklefishModPageUrlFormat - ), - new GitHubRepository( - vendorKey: config.GitHubKey, - baseUrl: config.GitHubBaseUrl, - releaseUrlFormat: config.GitHubReleaseUrlFormat, - userAgent: string.Format(config.GitHubUserAgent, version), - acceptHeader: config.GitHubAcceptHeader, - username: config.GitHubUsername, - password: config.GitHubPassword - ), - new NexusRepository( - vendorKey: config.NexusKey, - userAgent: config.NexusUserAgent, - baseUrl: config.NexusBaseUrl, - modUrlFormat: config.NexusModUrlFormat - ) - } - .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); - } - - /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search as a comma-delimited array. - [HttpGet] - public async Task> GetAsync(string modKeys) - { - string[] modKeysArray = modKeys?.Split(',').ToArray(); - if (modKeysArray == null || !modKeysArray.Any()) - return new Dictionary(); - - return await this.PostAsync(new ModSearchModel(modKeysArray)); - } - - /// Fetch version metadata for the given mods. - /// The mod search criteria. - [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) - { - // sort & filter keys - string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) - .ToArray(); - - // fetch mod info - IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) - { - // parse mod key - if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) - { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); - continue; - } - - // get matching repository - if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) - { - result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - continue; - } - - // fetch mod info - result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - - ModInfoModel info = await repository.GetModInfoAsync(modID); - if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) - info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); - - return info; - }); - } - - return result; - } - - - /********* - ** Private methods - *********/ - /// Parse a namespaced mod ID. - /// The raw mod ID to parse. - /// The parsed vendor key. - /// The parsed mod ID. - /// Returns whether the value could be parsed. - private bool TryParseModKey(string raw, out string vendorKey, out string modID) - { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - { - vendorKey = null; - modID = null; - return false; - } - - // parse - vendorKey = parts[0].Trim(); - modID = parts[1].Trim(); - return true; - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs deleted file mode 100644 index 03de639e..00000000 --- a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ConfigModels -{ - /// The config settings for mod update checks. - public class ModUpdateCheckConfig - { - /********* - ** Accessors - *********/ - /**** - ** General - ****/ - /// The number of minutes update checks should be cached before refetching them. - public int CacheMinutes { get; set; } - - /// A regex which matches SMAPI-style semantic version. - /// Derived from SMAPI's SemanticVersion implementation. - public string SemanticVersionRegex { get; set; } - - /**** - ** Chucklefish mod site - ****/ - /// The repository key for the Chucklefish mod site. - public string ChucklefishKey { get; set; } - - /// The user agent for the Chucklefish API client, where {0} is the SMAPI version. - public string ChucklefishUserAgent { get; set; } - - /// The base URL for the Chucklefish mod site. - public string ChucklefishBaseUrl { get; set; } - - /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. - public string ChucklefishModPageUrlFormat { get; set; } - - - /**** - ** GitHub - ****/ - /// The repository key for Nexus Mods. - public string GitHubKey { get; set; } - - /// The user agent for the GitHub API client, where {0} is the SMAPI version. - public string GitHubUserAgent { get; set; } - - /// The base URL for the GitHub API. - public string GitHubBaseUrl { get; set; } - - /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. - public string GitHubReleaseUrlFormat { get; set; } - - /// The Accept header value expected by the GitHub API. - public string GitHubAcceptHeader { get; set; } - - /// The username with which to authenticate to the GitHub API (if any). - public string GitHubUsername { get; set; } - - /// The password with which to authenticate to the GitHub API (if any). - public string GitHubPassword { get; set; } - - /**** - ** Nexus Mods - ****/ - /// The repository key for Nexus Mods. - public string NexusKey { get; set; } - - /// The user agent for the Nexus Mods API client. - public string NexusUserAgent { get; set; } - - /// The base URL for the Nexus Mods API. - public string NexusBaseUrl { get; set; } - - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - public string NexusModUrlFormat { get; set; } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs deleted file mode 100644 index 2c24c610..00000000 --- a/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Reflection; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; - -namespace StardewModdingAPI.Web.Framework -{ - /// Discovers controllers with support for non-public controllers. - internal class InternalControllerFeatureProvider : ControllerFeatureProvider - { - /********* - ** Public methods - *********/ - /// Determines if a given type is a controller. - /// The candidate. - /// true if the type is a controller; otherwise false. - protected override bool IsController(TypeInfo type) - { - return - type.IsClass - && !type.IsAbstract - && (/*type.IsPublic &&*/ !type.ContainsGenericParameters) - && (!type.IsDefined(typeof(NonControllerAttribute)) - && (type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || type.IsDefined(typeof(ControllerAttribute)))); - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs deleted file mode 100644 index d98acd89..00000000 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using StardewModdingAPI.Models; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - internal abstract class RepositoryBase : IModRepository - { - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - public string VendorKey { get; } - - - /********* - ** Public methods - *********/ - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public abstract void Dispose(); - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public abstract Task GetModInfoAsync(string id); - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// The unique key for this vendor. - protected RepositoryBase(string vendorKey) - { - this.VendorKey = vendorKey; - } - - /// Normalise a version string. - /// The version to normalise. - protected string NormaliseVersion(string version) - { - if (string.IsNullOrWhiteSpace(version)) - return null; - - version = version.Trim(); - if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix - version = version.Substring(1); - - return version; - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs deleted file mode 100644 index ed7bd60b..00000000 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using HtmlAgilityPack; -using Pathoschild.Http.Client; -using StardewModdingAPI.Models; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from the Chucklefish mod site. - internal class ChucklefishRepository : RepositoryBase - { - /********* - ** Properties - *********/ - /// The base URL for the Chucklefish mod site. - private readonly string BaseUrl; - - /// The URL for a mod page excluding the base URL, where {0} is the mod ID. - private readonly string ModPageUrlFormat; - - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique key for this vendor. - /// The user agent for the API client. - /// The base URL for the Chucklefish mod site. - /// The URL for a mod page excluding the , where {0} is the mod ID. - public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) - : base(vendorKey) - { - this.BaseUrl = baseUrl; - this.ModPageUrlFormat = modPageUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!uint.TryParse(id, out uint _)) - return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); - - // fetch info - try - { - // fetch HTML - string html; - try - { - html = await this.Client - .GetAsync(string.Format(this.ModPageUrlFormat, id)) - .AsString(); - } - catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) - { - return new ModInfoModel("Found no mod with this ID."); - } - - // parse HTML - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - // extract mod info - string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); - string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; - if (name.StartsWith("[SMAPI] ")) - name = name.Substring("[SMAPI] ".Length); - string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; - - // create model - return new ModInfoModel(name, this.NormaliseVersion(version), url); - } - catch (Exception ex) - { - return new ModInfoModel(ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs deleted file mode 100644 index 174fb79a..00000000 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Pathoschild.Http.Client; -using StardewModdingAPI.Models; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from GitHub project releases. - internal class GitHubRepository : RepositoryBase - { - /********* - ** Properties - *********/ - /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. - private readonly string ReleaseUrlFormat; - - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique key for this vendor. - /// The base URL for the Nexus Mods API. - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - /// The user agent for the API client. - /// The Accept header value expected by the GitHub API. - /// The username with which to authenticate to the GitHub API. - /// The password with which to authenticate to the GitHub API. - public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) - : base(vendorKey) - { - this.ReleaseUrlFormat = releaseUrlFormat; - - this.Client = new FluentClient(baseUrl) - .SetUserAgent(userAgent) - .AddDefault(req => req.WithHeader("Accept", acceptHeader)); - if (!string.IsNullOrWhiteSpace(username)) - this.Client = this.Client.SetBasicAuthentication(username, password); - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) - return new ModInfoModel($"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); - - // fetch info - try - { - GitRelease release = await this.Client - .GetAsync(string.Format(this.ReleaseUrlFormat, id)) - .As(); - return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); - } - catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) - { - return new ModInfoModel("Found no mod with this ID."); - } - catch (Exception ex) - { - return new ModInfoModel(ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - - - /********* - ** Private models - *********/ - /// Metadata about a GitHub release tag. - private class GitRelease - { - /********* - ** Accessors - *********/ - /// The display name. - [JsonProperty("name")] - public string Name { get; set; } - - /// The semantic version string. - [JsonProperty("tag_name")] - public string Tag { get; set; } - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs deleted file mode 100644 index 98e4c957..00000000 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Models; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// A repository which provides mod metadata. - internal interface IModRepository : IDisposable - { - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - string VendorKey { get; } - - - /********* - ** Public methods - *********/ - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - Task GetModInfoAsync(string id); - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs deleted file mode 100644 index 71970bec..00000000 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Pathoschild.Http.Client; -using StardewModdingAPI.Models; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from Nexus Mods. - internal class NexusRepository : RepositoryBase - { - /********* - ** Properties - *********/ - /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. - private readonly string ModUrlFormat; - - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique key for this vendor. - /// The user agent for the Nexus Mods API client. - /// The base URL for the Nexus Mods API. - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) - : base(vendorKey) - { - this.ModUrlFormat = modUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!uint.TryParse(id, out uint _)) - return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); - - // fetch info - try - { - NexusResponseModel response = await this.Client - .GetAsync(string.Format(this.ModUrlFormat, id)) - .As(); - - return response != null - ? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url) - : new ModInfoModel("Found no mod with this ID."); - } - catch (Exception ex) - { - return new ModInfoModel(ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - - - /********* - ** Private models - *********/ - /// A mod metadata response from Nexus Mods. - private class NexusResponseModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's semantic version number. - public string Version { get; set; } - - /// The mod's web URL. - [JsonProperty("mod_page_uri")] - public string Url { get; set; } - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs b/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs deleted file mode 100644 index 5a56844f..00000000 --- a/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Microsoft.AspNetCore.Rewrite; - -namespace StardewModdingAPI.Web.Framework -{ - /// Rewrite requests to prepend the subdomain portion (if any) to the path. - /// Derived from . - internal class RewriteSubdomainRule : IRule - { - /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). - /// The rewrite context. - public void ApplyRule(RewriteContext context) - { - context.Result = RuleResult.ContinueRules; - - // get host parts - string host = context.HttpContext.Request.Host.Host; - string[] parts = host.Split('.'); - - // validate - if (parts.Length < 2) - return; - if (parts.Length < 3 && !"localhost".Equals(parts[1], StringComparison.InvariantCultureIgnoreCase)) - return; - - // prepend to path - context.HttpContext.Request.Path = $"/{parts[0]}{context.HttpContext.Request.Path}"; - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs b/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs deleted file mode 100644 index be9c0918..00000000 --- a/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Routing.Constraints; - -namespace StardewModdingAPI.Web.Framework -{ - /// Constrains a route value to a valid semantic version. - internal class VersionConstraint : RegexRouteConstraint - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public VersionConstraint() - : base(@"^v(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$") { } - } -} diff --git a/src/StardewModdingAPI.Web/Program.cs b/src/StardewModdingAPI.Web/Program.cs deleted file mode 100644 index eeecb791..00000000 --- a/src/StardewModdingAPI.Web/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; - -namespace StardewModdingAPI.Web -{ - /// The main app entry point. - public class Program - { - /********* - ** Public methods - *********/ - /// The main app entry point. - /// The command-line arguments. - public static void Main(string[] args) - { - // configure web server - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .Build() - .Run(); - } - } -} diff --git a/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs deleted file mode 100644 index 63f787a4..00000000 --- a/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyTitle("StardewModdingAPI.Web")] -[assembly: AssemblyProduct("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Web/Properties/launchSettings.json b/src/StardewModdingAPI.Web/Properties/launchSettings.json deleted file mode 100644 index 3acee14d..00000000 --- a/src/StardewModdingAPI.Web/Properties/launchSettings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:59482/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "api/v1.0/mods", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Dewdrop": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "api/v1.0/mods", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:59483" - } - } -} diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj deleted file mode 100644 index 746b1a69..00000000 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - netcoreapp2.0 - false - - - - - - - - - - - - - - - - - - - - - diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs deleted file mode 100644 index eaf14983..00000000 --- a/src/StardewModdingAPI.Web/Startup.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Rewrite; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using StardewModdingAPI.Web.Framework; -using StardewModdingAPI.Web.Framework.ConfigModels; - -namespace StardewModdingAPI.Web -{ - /// The web app startup configuration. - internal class Startup - { - /********* - ** Accessors - *********/ - /// The web app configuration. - public IConfigurationRoot Configuration { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The hosting environment. - public Startup(IHostingEnvironment env) - { - this.Configuration = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddEnvironmentVariables() - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables() - .Build(); - } - - /// The method called by the runtime to add services to the container. - /// The service injection container. - public void ConfigureServices(IServiceCollection services) - { - services - .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) - .AddMemoryCache() - .AddMvc() - .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) - .AddJsonOptions(options => - { - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); - } - - /// The method called by the runtime to configure the HTTP request pipeline. - /// The application builder. - /// The hosting environment. - /// The logger factory. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - app - .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing - .UseMvc(); - } - } -} diff --git a/src/StardewModdingAPI.Web/appsettings.Development.json b/src/StardewModdingAPI.Web/appsettings.Development.json deleted file mode 100644 index fa8ce71a..00000000 --- a/src/StardewModdingAPI.Web/appsettings.Development.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json deleted file mode 100644 index 852f6f71..00000000 --- a/src/StardewModdingAPI.Web/appsettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } - }, - "ModUpdateCheck": { - "CacheMinutes": 60, - "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", - - "ChucklefishKey": "Chucklefish", - "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", - "ChucklefishBaseUrl": "https://community.playstarbound.com", - "ChucklefishModPageUrlFormat": "resources/{0}", - - "GitHubKey": "GitHub", - "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", - "GitHubBaseUrl": "https://api.github.com", - "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", - "GitHubAcceptHeader": "application/vnd.github.v3+json", - "GitHubUsername": null, /* set via environment properties */ - "GitHubPassword": null, /* set via environment properties */ - - "NexusKey": "Nexus", - "NexusUserAgent": "Nexus Client v0.63.15", - "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", - "NexusModUrlFormat": "mods/{0}" - } -} diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln deleted file mode 100644 index 5e8a2c93..00000000 --- a/src/StardewModdingAPI.sln +++ /dev/null @@ -1,128 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.16 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "StardewModdingAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - ..\.gitattributes = ..\.gitattributes - ..\.gitignore = ..\.gitignore - common.targets = common.targets - ..\CONTRIBUTING.md = ..\CONTRIBUTING.md - GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs - ..\LICENSE = ..\LICENSE - prepare-install-package.targets = prepare-install-package.targets - ..\README.md = ..\README.md - ..\release-notes.md = ..\release-notes.md - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "StardewModdingAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" - ProjectSection(ProjectDependencies) = postProject - {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "StardewModdingAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" -EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Models", "StardewModdingAPI.Models\StardewModdingAPI.Models.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" -EndProject -Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 - StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 - EndGlobalSection - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|x86.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Mixed Platforms.Build.0 = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|x86.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|x86.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Mixed Platforms.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|x86.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|x86.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|x86.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86 - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} - EndGlobalSection -EndGlobal diff --git a/src/StardewModdingAPI.sln.DotSettings b/src/StardewModdingAPI.sln.DotSettings deleted file mode 100644 index d16ef684..00000000 --- a/src/StardewModdingAPI.sln.DotSettings +++ /dev/null @@ -1,19 +0,0 @@ - - DO_NOT_SHOW - DO_NOT_SHOW - HINT - HINT - Field, Property, Event, Method - Field, Property, Event, Method - True - False - UseVarWhenEvident - UseExplicitType - ID - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> - True - True - True - \ No newline at end of file diff --git a/src/StardewModdingAPI/App.config b/src/StardewModdingAPI/App.config deleted file mode 100644 index 27cdf0f7..00000000 --- a/src/StardewModdingAPI/App.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs deleted file mode 100644 index 4d0a9ca9..00000000 --- a/src/StardewModdingAPI/Constants.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.ModLoading; -using StardewValley; - -namespace StardewModdingAPI -{ - /// Contains SMAPI's constants and assumptions. - public static class Constants - { - /********* - ** Properties - *********/ - /// The directory path containing the current save's data (if a save is loaded). - private static string RawSavePath => Context.IsSaveLoaded ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : null; - - /// Whether the directory containing the current save's data exists on disk. - private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); - - - /********* - ** Accessors - *********/ - /**** - ** Public - ****/ - /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, "beta.1"); - - /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); - - /// The maximum supported version of Stardew Valley. - public static ISemanticVersion MaximumGameVersion { get; } = null; - - /// The path to the game folder. - public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - - /// The directory path containing Stardew Valley's app data. - public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); - - /// The directory path in which error logs should be stored. - public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs"); - - /// The directory path where all saves are stored. - public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves"); - - /// The directory name containing the current save's data (if a save is loaded and the directory exists). - public static string SaveFolderName => Context.IsSaveLoaded ? Constants.GetSaveFolderName() : ""; - - /// The directory path containing the current save's data (if a save is loaded and the directory exists). - public static string CurrentSavePath => Constants.SavePathReady ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : ""; - - /**** - ** Internal - ****/ - /// The GitHub repository to check for updates. - internal const string GitHubRepository = "Pathoschild/SMAPI"; - - /// The file path for the SMAPI configuration file. - internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); - - /// The file path to the log where the latest output should be saved. - internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); - - /// A copy of the log leading up to the previous fatal crash, if any. - internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); - - /// The file path which stores a fatal crash message for the next run. - internal static string FatalCrashMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.crash.marker"); - - /// The full path to the folder containing mods. - internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); - - /// The game's current semantic version. - internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); - - /// The target game platform. - internal static Platform TargetPlatform { get; } = -#if SMAPI_FOR_WINDOWS - Platform.Windows; -#else - Platform.Mono; -#endif - - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API. - internal static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", - ["GitHub"] = "https://github.com/{0}/releases" - }; - - - /********* - ** Internal methods - *********/ - /// Get metadata for mapping assemblies to the current platform. - /// The target game platform. - internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) - { - // get assembly changes needed for platform - string[] removeAssemblyReferences; - Assembly[] targetAssemblies; - switch (targetPlatform) - { - case Platform.Mono: - removeAssemblyReferences = new[] - { - "Stardew Valley", - "Microsoft.Xna.Framework", - "Microsoft.Xna.Framework.Game", - "Microsoft.Xna.Framework.Graphics" - }; - targetAssemblies = new[] - { - typeof(StardewValley.Game1).Assembly, - typeof(Microsoft.Xna.Framework.Vector2).Assembly - }; - break; - - case Platform.Windows: - removeAssemblyReferences = new[] - { - "StardewValley", - "MonoGame.Framework" - }; - targetAssemblies = new[] - { - typeof(StardewValley.Game1).Assembly, - typeof(Microsoft.Xna.Framework.Vector2).Assembly, - typeof(Microsoft.Xna.Framework.Game).Assembly, - typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly - }; - break; - - default: - throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); - } - - return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); - } - - - /********* - ** Private methods - *********/ - /// Get the name of a save directory for the current player. - private static string GetSaveFolderName() - { - string prefix = new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray()); - return $"{prefix}_{Game1.uniqueIDForThisGame}"; - } - - /// Get the game's current version string. - private static string GetGameVersion() - { - // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time - FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); - if (field == null) - throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); - return (string)field.GetValue(null); - } - } -} diff --git a/src/StardewModdingAPI/ContentSource.cs b/src/StardewModdingAPI/ContentSource.cs deleted file mode 100644 index 35c8bc21..00000000 --- a/src/StardewModdingAPI/ContentSource.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI -{ - /// Specifies a source containing content that can be loaded. - public enum ContentSource - { - /// Assets in the game's content manager (i.e. XNBs in the game's content folder). - GameContent, - - /// XNB files in the current mod's folder. - ModFolder - } -} diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs deleted file mode 100644 index 119e14c8..00000000 --- a/src/StardewModdingAPI/Context.cs +++ /dev/null @@ -1,37 +0,0 @@ -using StardewModdingAPI.Events; -using StardewValley; -using StardewValley.Menus; - -namespace StardewModdingAPI -{ - /// Provides information about the current game state. - public static class Context - { - /********* - ** Accessors - *********/ - /**** - ** Public - ****/ - /// Whether the player has loaded a save and the world has finished initialising. - public static bool IsWorldReady { get; internal set; } - - /// Whether is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc). - public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && !Game1.eventUp; - - /// Whether is true and the player is free to move (e.g. not using a tool). - public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; - - /// Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use to draw to the screen. - public static bool IsInDrawLoop { get; internal set; } - - /**** - ** Internal - ****/ - /// Whether a player save has been loaded. - internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); - - /// Whether the game is currently writing to the save file. - internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something - } -} diff --git a/src/StardewModdingAPI/Events/ChangeType.cs b/src/StardewModdingAPI/Events/ChangeType.cs deleted file mode 100644 index 4b207f08..00000000 --- a/src/StardewModdingAPI/Events/ChangeType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Events -{ - /// Indicates how an inventory item changed. - public enum ChangeType - { - /// The entire stack was removed. - Removed, - - /// The entire stack was added. - Added, - - /// The stack size changed. - StackChange - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs deleted file mode 100644 index 4b4e2ad0..00000000 --- a/src/StardewModdingAPI/Events/ContentEvents.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the game loads content. - public static class ContentEvents - { - - /********* - ** Events - *********/ - /// Raised after the content language changes. - public static event EventHandler> AfterLocaleChanged; - - - /********* - ** Internal methods - *********/ - /// Raise an event. - /// Encapsulates monitoring and logging. - /// The previous locale. - /// The current locale. - internal static void InvokeAfterLocaleChanged(IMonitor monitor, string oldLocale, string newLocale) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterLocaleChanged)}", ContentEvents.AfterLocaleChanged?.GetInvocationList(), null, new EventArgsValueChanged(oldLocale, newLocale)); - } - } -} diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/StardewModdingAPI/Events/ControlEvents.cs deleted file mode 100644 index 80d0f547..00000000 --- a/src/StardewModdingAPI/Events/ControlEvents.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player uses a controller, keyboard, or mouse. - public static class ControlEvents - { - /********* - ** Events - *********/ - /// Raised when the changes. That happens when the player presses or releases a key. - public static event EventHandler KeyboardChanged; - - /// Raised when the player presses a keyboard key. - public static event EventHandler KeyPressed; - - /// Raised when the player releases a keyboard key. - public static event EventHandler KeyReleased; - - /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public static event EventHandler MouseChanged; - - /// The player pressed a controller button. This event isn't raised for trigger buttons. - public static event EventHandler ControllerButtonPressed; - - /// The player released a controller button. This event isn't raised for trigger buttons. - public static event EventHandler ControllerButtonReleased; - - /// The player pressed a controller trigger button. - public static event EventHandler ControllerTriggerPressed; - - /// The player released a controller trigger button. - public static event EventHandler ControllerTriggerReleased; - - - /********* - ** Internal methods - *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous keyboard state. - /// The current keyboard state. - internal static void InvokeKeyboardChanged(IMonitor monitor, KeyboardState priorState, KeyboardState newState) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyboardChanged)}", ControlEvents.KeyboardChanged?.GetInvocationList(), null, new EventArgsKeyboardStateChanged(priorState, newState)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous mouse state. - /// The current mouse state. - /// The previous mouse position on the screen adjusted for the zoom level. - /// The current mouse position on the screen adjusted for the zoom level. - internal static void InvokeMouseChanged(IMonitor monitor, MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.MouseChanged)}", ControlEvents.MouseChanged?.GetInvocationList(), null, new EventArgsMouseStateChanged(priorState, newState, priorPosition, newPosition)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The keyboard button that was pressed. - internal static void InvokeKeyPressed(IMonitor monitor, Keys key) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyPressed)}", ControlEvents.KeyPressed?.GetInvocationList(), null, new EventArgsKeyPressed(key)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The keyboard button that was released. - internal static void InvokeKeyReleased(IMonitor monitor, Keys key) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyReleased)}", ControlEvents.KeyReleased?.GetInvocationList(), null, new EventArgsKeyPressed(key)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The controller button that was pressed. - internal static void InvokeButtonPressed(IMonitor monitor, Buttons button) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(PlayerIndex.One, button)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The controller button that was released. - internal static void InvokeButtonReleased(IMonitor monitor, Buttons button) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(PlayerIndex.One, button)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The trigger button that was pressed. - /// The current trigger value. - internal static void InvokeTriggerPressed(IMonitor monitor, Buttons button, float value) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(PlayerIndex.One, button, value)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The trigger button that was pressed. - /// The current trigger value. - internal static void InvokeTriggerReleased(IMonitor monitor, Buttons button, float value) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(PlayerIndex.One, button, value)); - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs b/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs deleted file mode 100644 index 2a2aa163..00000000 --- a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using StardewValley.Menus; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsClickableMenuChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous menu. - public IClickableMenu NewMenu { get; } - - /// The current menu. - public IClickableMenu PriorMenu { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous menu. - /// The current menu. - public EventArgsClickableMenuChanged(IClickableMenu priorMenu, IClickableMenu newMenu) - { - this.NewMenu = newMenu; - this.PriorMenu = priorMenu; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs b/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs deleted file mode 100644 index 5e6585f0..00000000 --- a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using StardewValley.Menus; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsClickableMenuClosed : EventArgs - { - /********* - ** Accessors - *********/ - /// The menu that was closed. - public IClickableMenu PriorMenu { get; } - - - /********* - ** Accessors - *********/ - /// Construct an instance. - /// The menu that was closed. - public EventArgsClickableMenuClosed(IClickableMenu priorMenu) - { - this.PriorMenu = priorMenu; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs b/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs deleted file mode 100644 index 3243b80b..00000000 --- a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerButtonPressed : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was pressed. - public Buttons ButtonPressed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the button. - /// The controller button that was pressed. - public EventArgsControllerButtonPressed(PlayerIndex playerIndex, Buttons button) - { - this.PlayerIndex = playerIndex; - this.ButtonPressed = button; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs b/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs deleted file mode 100644 index e05a080b..00000000 --- a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerButtonReleased : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was pressed. - public Buttons ButtonReleased { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the button. - /// The controller button that was released. - public EventArgsControllerButtonReleased(PlayerIndex playerIndex, Buttons button) - { - this.PlayerIndex = playerIndex; - this.ButtonReleased = button; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs b/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs deleted file mode 100644 index a2087733..00000000 --- a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerTriggerPressed : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was pressed. - public Buttons ButtonPressed { get; } - - /// The current trigger value. - public float Value { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the trigger button. - /// The trigger button that was pressed. - /// The current trigger value. - public EventArgsControllerTriggerPressed(PlayerIndex playerIndex, Buttons button, float value) - { - this.PlayerIndex = playerIndex; - this.ButtonPressed = button; - this.Value = value; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs b/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs deleted file mode 100644 index d2eecbec..00000000 --- a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerTriggerReleased : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was released. - public Buttons ButtonReleased { get; } - - /// The current trigger value. - public float Value { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the trigger button. - /// The trigger button that was released. - /// The current trigger value. - public EventArgsControllerTriggerReleased(PlayerIndex playerIndex, Buttons button, float value) - { - this.PlayerIndex = playerIndex; - this.ButtonReleased = button; - this.Value = value; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs b/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs deleted file mode 100644 index 25d3ebf3..00000000 --- a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsCurrentLocationChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's current location. - public GameLocation NewLocation { get; } - - /// The player's previous location. - public GameLocation PriorLocation { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's previous location. - /// The player's current location. - public EventArgsCurrentLocationChanged(GameLocation priorLocation, GameLocation newLocation) - { - this.NewLocation = newLocation; - this.PriorLocation = priorLocation; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs b/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs deleted file mode 100644 index fb8c821e..00000000 --- a/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsGameLocationsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The current list of game locations. - public List NewLocations { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current list of game locations. - public EventArgsGameLocationsChanged(List newLocations) - { - this.NewLocations = newLocations; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/StardewModdingAPI/Events/EventArgsInput.cs deleted file mode 100644 index 66cb19f2..00000000 --- a/src/StardewModdingAPI/Events/EventArgsInput.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Utilities; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments when a button is pressed or released. - public class EventArgsInput : EventArgs - { - /********* - ** Accessors - *********/ - /// The button on the controller, keyboard, or mouse. - public SButton Button { get; } - - /// The current cursor position. - public ICursorPosition Cursor { get; set; } - - /// Whether the input is considered a 'click' by the game for enabling action. - public bool IsClick { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) - { - this.Button = button; - this.Cursor = cursor; - this.IsClick = isClick; - } - - /// Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event. - public void SuppressButton() - { - this.SuppressButton(this.Button); - } - - /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. - /// The button to suppress. - public void SuppressButton(SButton button) - { - // keyboard - if (this.Button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Except(new[] { key }).ToArray()); - - // controller - else if (this.Button.TryGetController(out Buttons controllerButton)) - { - var newState = GamePad.GetState(PlayerIndex.One); - var thumbsticks = Game1.oldPadState.ThumbSticks; - var triggers = Game1.oldPadState.Triggers; - var buttons = Game1.oldPadState.Buttons; - var dpad = Game1.oldPadState.DPad; - - switch (controllerButton) - { - // d-pad - case Buttons.DPadDown: - dpad = new GamePadDPad(dpad.Up, newState.DPad.Down, dpad.Left, dpad.Right); - break; - case Buttons.DPadLeft: - dpad = new GamePadDPad(dpad.Up, dpad.Down, newState.DPad.Left, dpad.Right); - break; - case Buttons.DPadRight: - dpad = new GamePadDPad(dpad.Up, dpad.Down, dpad.Left, newState.DPad.Right); - break; - case Buttons.DPadUp: - dpad = new GamePadDPad(newState.DPad.Up, dpad.Down, dpad.Left, dpad.Right); - break; - - // trigger - case Buttons.LeftTrigger: - triggers = new GamePadTriggers(newState.Triggers.Left, triggers.Right); - break; - case Buttons.RightTrigger: - triggers = new GamePadTriggers(triggers.Left, newState.Triggers.Right); - break; - - // thumbstick - case Buttons.LeftThumbstickDown: - case Buttons.LeftThumbstickLeft: - case Buttons.LeftThumbstickRight: - case Buttons.LeftThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Left, thumbsticks.Right); - break; - case Buttons.RightThumbstickDown: - case Buttons.RightThumbstickLeft: - case Buttons.RightThumbstickRight: - case Buttons.RightThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Right, thumbsticks.Left); - break; - - // buttons - default: - var mask = - (buttons.A == ButtonState.Pressed ? Buttons.A : 0) - | (buttons.B == ButtonState.Pressed ? Buttons.B : 0) - | (buttons.Back == ButtonState.Pressed ? Buttons.Back : 0) - | (buttons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0) - | (buttons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0) - | (buttons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0) - | (buttons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0) - | (buttons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0) - | (buttons.Start == ButtonState.Pressed ? Buttons.Start : 0) - | (buttons.X == ButtonState.Pressed ? Buttons.X : 0) - | (buttons.Y == ButtonState.Pressed ? Buttons.Y : 0); - mask = mask ^ controllerButton; - buttons = new GamePadButtons(mask); - break; - } - - Game1.oldPadState = new GamePadState(thumbsticks, triggers, buttons, dpad); - } - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs b/src/StardewModdingAPI/Events/EventArgsIntChanged.cs deleted file mode 100644 index 0c742d12..00000000 --- a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an integer field that changed value. - public class EventArgsIntChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous value. - public int PriorInt { get; } - - /// The current value. - public int NewInt { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous value. - /// The current value. - public EventArgsIntChanged(int priorInt, int newInt) - { - this.PriorInt = priorInt; - this.NewInt = newInt; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs b/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs deleted file mode 100644 index 1ee02842..00000000 --- a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsInventoryChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's inventory. - public List Inventory { get; } - - /// The added items. - public List Added { get; } - - /// The removed items. - public List Removed { get; } - - /// The items whose stack sizes changed. - public List QuantityChanged { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's inventory. - /// The inventory changes. - public EventArgsInventoryChanged(List inventory, List changedItems) - { - this.Inventory = inventory; - this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); - this.Removed = changedItems.Where(n => n.ChangeType == ChangeType.Removed).ToList(); - this.QuantityChanged = changedItems.Where(n => n.ChangeType == ChangeType.StackChange).ToList(); - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs b/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs deleted file mode 100644 index d9d81e10..00000000 --- a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsKeyPressed : EventArgs - { - /********* - ** Accessors - *********/ - /// The keyboard button that was pressed. - public Keys KeyPressed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The keyboard button that was pressed. - public EventArgsKeyPressed(Keys key) - { - this.KeyPressed = key; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs b/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs deleted file mode 100644 index 14e397ce..00000000 --- a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsKeyboardStateChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous keyboard state. - public KeyboardState NewState { get; } - - /// The current keyboard state. - public KeyboardState PriorState { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous keyboard state. - /// The current keyboard state. - public EventArgsKeyboardStateChanged(KeyboardState priorState, KeyboardState newState) - { - this.PriorState = priorState; - this.NewState = newState; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs b/src/StardewModdingAPI/Events/EventArgsLevelUp.cs deleted file mode 100644 index fe6696d4..00000000 --- a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLevelUp : EventArgs - { - /********* - ** Accessors - *********/ - /// The player skill that leveled up. - public LevelType Type { get; } - - /// The new skill level. - public int NewLevel { get; } - - /// The player skill types. - public enum LevelType - { - /// The combat skill. - Combat, - - /// The farming skill. - Farming, - - /// The fishing skill. - Fishing, - - /// The foraging skill. - Foraging, - - /// The mining skill. - Mining, - - /// The luck skill. - Luck - } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player skill that leveled up. - /// The new skill level. - public EventArgsLevelUp(LevelType type, int newLevel) - { - this.Type = type; - this.NewLevel = newLevel; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs b/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs deleted file mode 100644 index 058999e9..00000000 --- a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using StardewValley; -using Object = StardewValley.Object; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLocationObjectsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The current list of objects in the current location. - public SerializableDictionary NewObjects { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged(SerializableDictionary newObjects) - { - this.NewObjects = newObjects; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs b/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs deleted file mode 100644 index c82fed35..00000000 --- a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsMineLevelChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous mine level. - public int PreviousMineLevel { get; } - - /// The current mine level. - public int CurrentMineLevel { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous mine level. - /// The current mine level. - public EventArgsMineLevelChanged(int previousMineLevel, int currentMineLevel) - { - this.PreviousMineLevel = previousMineLevel; - this.CurrentMineLevel = currentMineLevel; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs b/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs deleted file mode 100644 index 57298164..00000000 --- a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsMouseStateChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous mouse state. - public MouseState PriorState { get; } - - /// The current mouse state. - public MouseState NewState { get; } - - /// The previous mouse position on the screen adjusted for the zoom level. - public Point PriorPosition { get; } - - /// The current mouse position on the screen adjusted for the zoom level. - public Point NewPosition { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous mouse state. - /// The current mouse state. - /// The previous mouse position on the screen adjusted for the zoom level. - /// The current mouse position on the screen adjusted for the zoom level. - public EventArgsMouseStateChanged(MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) - { - this.PriorState = priorState; - this.NewState = newState; - this.PriorPosition = priorPosition; - this.NewPosition = newPosition; - } - } -} diff --git a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs b/src/StardewModdingAPI/Events/EventArgsValueChanged.cs deleted file mode 100644 index 1d25af49..00000000 --- a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a field that changed value. - /// The value type. - public class EventArgsValueChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous value. - public T PriorValue { get; } - - /// The current value. - public T NewValue { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous value. - /// The current value. - public EventArgsValueChanged(T priorValue, T newValue) - { - this.PriorValue = priorValue; - this.NewValue = newValue; - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs deleted file mode 100644 index b477376e..00000000 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the game changes state. - public static class GameEvents - { - /********* - ** Events - *********/ - /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . - internal static event EventHandler InitializeInternal; - - /// Raised when the game updates its state (≈60 times per second). - public static event EventHandler UpdateTick; - - /// Raised every other tick (≈30 times per second). - public static event EventHandler SecondUpdateTick; - - /// Raised every fourth tick (≈15 times per second). - public static event EventHandler FourthUpdateTick; - - /// Raised every eighth tick (≈8 times per second). - public static event EventHandler EighthUpdateTick; - - /// Raised every 15th tick (≈4 times per second). - public static event EventHandler QuarterSecondTick; - - /// Raised every 30th tick (≈twice per second). - public static event EventHandler HalfSecondTick; - - /// Raised every 60th tick (≈once per second). - public static event EventHandler OneSecondTick; - - - /********* - ** Internal methods - *********/ - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeInitialize(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); - } - - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeSecondUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFourthUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeEighthUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeQuarterSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeHalfSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeOneSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); - } - } -} diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/StardewModdingAPI/Events/GraphicsEvents.cs deleted file mode 100644 index fff51bed..00000000 --- a/src/StardewModdingAPI/Events/GraphicsEvents.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised during the game's draw loop, when the game is rendering content to the window. - public static class GraphicsEvents - { - /********* - ** Events - *********/ - /**** - ** Generic events - ****/ - /// Raised after the game window is resized. - public static event EventHandler Resize; - - /**** - ** Main render events - ****/ - /// Raised before drawing the world to the screen. - public static event EventHandler OnPreRenderEvent; - - /// Raised after drawing the world to the screen. - public static event EventHandler OnPostRenderEvent; - - /**** - ** HUD events - ****/ - /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public static event EventHandler OnPreRenderHudEvent; - - /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public static event EventHandler OnPostRenderHudEvent; - - /**** - ** GUI events - ****/ - /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public static event EventHandler OnPreRenderGuiEvent; - - /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public static event EventHandler OnPostRenderGuiEvent; - - - /********* - ** Internal methods - *********/ - /**** - ** Generic events - ****/ - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeResize(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); - } - - /**** - ** Main render events - ****/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPreRenderEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderEvent)}", GraphicsEvents.OnPreRenderEvent?.GetInvocationList()); - } - - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPostRenderEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderEvent)}", GraphicsEvents.OnPostRenderEvent?.GetInvocationList()); - } - - /// Get whether there are any post-render event listeners. - internal static bool HasPostRenderListeners() - { - return GraphicsEvents.OnPostRenderEvent != null; - } - - /**** - ** GUI events - ****/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPreRenderGuiEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderGuiEvent)}", GraphicsEvents.OnPreRenderGuiEvent?.GetInvocationList()); - } - - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPostRenderGuiEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderGuiEvent)}", GraphicsEvents.OnPostRenderGuiEvent?.GetInvocationList()); - } - - /**** - ** HUD events - ****/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPreRenderHudEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderHudEvent)}", GraphicsEvents.OnPreRenderHudEvent?.GetInvocationList()); - } - - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeOnPostRenderHudEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderHudEvent)}", GraphicsEvents.OnPostRenderHudEvent?.GetInvocationList()); - } - } -} diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/StardewModdingAPI/Events/InputEvents.cs deleted file mode 100644 index c31eb698..00000000 --- a/src/StardewModdingAPI/Events/InputEvents.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Utilities; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player uses a controller, keyboard, or mouse button. - public static class InputEvents - { - /********* - ** Events - *********/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. - public static event EventHandler ButtonPressed; - - /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. - public static event EventHandler ButtonReleased; - - - /********* - ** Internal methods - *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) - { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) - { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); - } - } -} diff --git a/src/StardewModdingAPI/Events/ItemStackChange.cs b/src/StardewModdingAPI/Events/ItemStackChange.cs deleted file mode 100644 index f9ae6df6..00000000 --- a/src/StardewModdingAPI/Events/ItemStackChange.cs +++ /dev/null @@ -1,20 +0,0 @@ -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Represents an inventory slot that changed. - public class ItemStackChange - { - /********* - ** Accessors - *********/ - /// The item in the slot. - public Item Item { get; set; } - - /// The amount by which the item's stack size changed. - public int StackChange { get; set; } - - /// How the inventory slot changed. - public ChangeType ChangeType { get; set; } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/LocationEvents.cs b/src/StardewModdingAPI/Events/LocationEvents.cs deleted file mode 100644 index b834bc1c..00000000 --- a/src/StardewModdingAPI/Events/LocationEvents.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework; -using StardewModdingAPI.Framework; -using StardewValley; -using Object = StardewValley.Object; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player transitions between game locations, a location is added or removed, or the objects in the current location change. - public static class LocationEvents - { - /********* - ** Events - *********/ - /// Raised after the player warps to a new location. - public static event EventHandler CurrentLocationChanged; - - /// Raised after a game location is added or removed. - public static event EventHandler LocationsChanged; - - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - public static event EventHandler LocationObjectsChanged; - - - /********* - ** Internal methods - *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The player's previous location. - /// The player's current location. - internal static void InvokeCurrentLocationChanged(IMonitor monitor, GameLocation priorLocation, GameLocation newLocation) - { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.CurrentLocationChanged)}", LocationEvents.CurrentLocationChanged?.GetInvocationList(), null, new EventArgsCurrentLocationChanged(priorLocation, newLocation)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The current list of game locations. - internal static void InvokeLocationsChanged(IMonitor monitor, List newLocations) - { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationsChanged)}", LocationEvents.LocationsChanged?.GetInvocationList(), null, new EventArgsGameLocationsChanged(newLocations)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The current list of objects in the current location. - internal static void InvokeOnNewLocationObject(IMonitor monitor, SerializableDictionary newObjects) - { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationObjectsChanged)}", LocationEvents.LocationObjectsChanged?.GetInvocationList(), null, new EventArgsLocationObjectsChanged(newObjects)); - } - } -} diff --git a/src/StardewModdingAPI/Events/MenuEvents.cs b/src/StardewModdingAPI/Events/MenuEvents.cs deleted file mode 100644 index bd8d897e..00000000 --- a/src/StardewModdingAPI/Events/MenuEvents.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using StardewModdingAPI.Framework; -using StardewValley.Menus; - -namespace StardewModdingAPI.Events -{ - /// Events raised when a game menu is opened or closed (including internal menus like the title screen). - public static class MenuEvents - { - /********* - ** Events - *********/ - /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. - public static event EventHandler MenuChanged; - - /// Raised after a game menu is closed. - public static event EventHandler MenuClosed; - - - /********* - ** Internal methods - *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous menu. - /// The current menu. - internal static void InvokeMenuChanged(IMonitor monitor, IClickableMenu priorMenu, IClickableMenu newMenu) - { - monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuChanged)}", MenuEvents.MenuChanged?.GetInvocationList(), null, new EventArgsClickableMenuChanged(priorMenu, newMenu)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The menu that was closed. - internal static void InvokeMenuClosed(IMonitor monitor, IClickableMenu priorMenu) - { - monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuClosed)}", MenuEvents.MenuClosed?.GetInvocationList(), null, new EventArgsClickableMenuClosed(priorMenu)); - } - } -} diff --git a/src/StardewModdingAPI/Events/MineEvents.cs b/src/StardewModdingAPI/Events/MineEvents.cs deleted file mode 100644 index 9cf7edac..00000000 --- a/src/StardewModdingAPI/Events/MineEvents.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised when something happens in the mines. - public static class MineEvents - { - /********* - ** Events - *********/ - /// Raised after the player warps to a new level of the mine. - public static event EventHandler MineLevelChanged; - - - /********* - ** Internal methods - *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous mine level. - /// The current mine level. - internal static void InvokeMineLevelChanged(IMonitor monitor, int previousMineLevel, int currentMineLevel) - { - monitor.SafelyRaiseGenericEvent($"{nameof(MineEvents)}.{nameof(MineEvents.MineLevelChanged)}", MineEvents.MineLevelChanged?.GetInvocationList(), null, new EventArgsMineLevelChanged(previousMineLevel, currentMineLevel)); - } - } -} diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs deleted file mode 100644 index 5a9a9d5f..00000000 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI.Framework; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player data changes. - public static class PlayerEvents - { - /********* - ** Events - *********/ - /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public static event EventHandler InventoryChanged; - - /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. - public static event EventHandler LeveledUp; - - - /********* - ** Internal methods - *********/ - /// Raise an event. - /// Encapsulates monitoring and logging. - /// The player's inventory. - /// The inventory changes. - internal static void InvokeInventoryChanged(IMonitor monitor, List inventory, IEnumerable changedItems) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); - } - - /// Rase a event. - /// Encapsulates monitoring and logging. - /// The player skill that leveled up. - /// The new skill level. - internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel)); - } - } -} diff --git a/src/StardewModdingAPI/Events/SaveEvents.cs b/src/StardewModdingAPI/Events/SaveEvents.cs deleted file mode 100644 index 50e6d729..00000000 --- a/src/StardewModdingAPI/Events/SaveEvents.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised before and after the player saves/loads the game. - public static class SaveEvents - { - /********* - ** Events - *********/ - /// Raised before the game begins writes data to the save file. - public static event EventHandler BeforeSave; - - /// Raised after the game finishes writing data to the save file. - public static event EventHandler AfterSave; - - /// Raised after the player loads a save slot. - public static event EventHandler AfterLoad; - - /// Raised after the game returns to the title screen. - public static event EventHandler AfterReturnToTitle; - - - /********* - ** Internal methods - *********/ - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeBeforeSave(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeSave)}", SaveEvents.BeforeSave?.GetInvocationList(), null, EventArgs.Empty); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterSave(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterSave)}", SaveEvents.AfterSave?.GetInvocationList(), null, EventArgs.Empty); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterLoad(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterLoad)}", SaveEvents.AfterLoad?.GetInvocationList(), null, EventArgs.Empty); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterReturnToTitle(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterReturnToTitle)}", SaveEvents.AfterReturnToTitle?.GetInvocationList(), null, EventArgs.Empty); - } - } -} diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs deleted file mode 100644 index 9aea5e04..00000000 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the in-game date or time changes. - public static class TimeEvents - { - /********* - ** Events - *********/ - /// Raised after the game begins a new day, including when loading a save. - public static event EventHandler AfterDayStarted; - - /// Raised after the in-game clock changes. - public static event EventHandler TimeOfDayChanged; - - /********* - ** Internal methods - *********/ - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterDayStarted(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous time in military time format (e.g. 6:00pm is 1800). - /// The current time in military time format (e.g. 6:10pm is 1810). - internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Command.cs b/src/StardewModdingAPI/Framework/Command.cs deleted file mode 100644 index 943e018d..00000000 --- a/src/StardewModdingAPI/Framework/Command.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework -{ - /// A command that can be submitted through the SMAPI console to interact with SMAPI. - internal class Command - { - /********* - ** Accessor - *********/ - /// The friendly name for the mod that registered the command. - public string ModName { get; } - - /// The command name, which the user must type to trigger it. - public string Name { get; } - - /// The human-readable documentation shown when the player runs the built-in 'help' command. - public string Documentation { get; } - - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - public Action Callback { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The friendly name for the mod that registered the command. - /// The command name, which the user must type to trigger it. - /// The human-readable documentation shown when the player runs the built-in 'help' command. - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - public Command(string modName, string name, string documentation, Action callback) - { - this.ModName = modName; - this.Name = name; - this.Documentation = documentation; - this.Callback = callback; - } - } -} diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/StardewModdingAPI/Framework/CommandManager.cs deleted file mode 100644 index 79a23d03..00000000 --- a/src/StardewModdingAPI/Framework/CommandManager.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework -{ - /// Manages console commands. - internal class CommandManager - { - /********* - ** Properties - *********/ - /// The commands registered with SMAPI. - private readonly IDictionary Commands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - - /********* - ** Public methods - *********/ - /// Add a console command. - /// The friendly mod name for this instance. - /// The command name, which the user must type to trigger it. - /// The human-readable documentation shown when the player runs the built-in 'help' command. - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - /// Whether to allow a null argument; this should only used for backwards compatibility. - /// The or is null or empty. - /// The is not a valid format. - /// There's already a command with that name. - public void Add(string modName, string name, string documentation, Action callback, bool allowNullCallback = false) - { - name = this.GetNormalisedName(name); - - // validate format - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(nameof(name), "Can't register a command with no name."); - if (name.Any(char.IsWhiteSpace)) - throw new FormatException($"Can't register the '{name}' command because the name can't contain whitespace."); - if (callback == null && !allowNullCallback) - throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback."); - - // ensure uniqueness - if (this.Commands.ContainsKey(name)) - throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name."); - - // add command - this.Commands.Add(name, new Command(modName, name, documentation, callback)); - } - - /// Get a command by its unique name. - /// The command name. - /// Returns the matching command, or null if not found. - public Command Get(string name) - { - name = this.GetNormalisedName(name); - this.Commands.TryGetValue(name, out Command command); - return command; - } - - /// Get all registered commands. - public IEnumerable GetAll() - { - return this.Commands - .Values - .OrderBy(p => p.Name); - } - - /// Trigger a command. - /// The raw command input. - /// Returns whether a matching command was triggered. - public bool Trigger(string input) - { - if (string.IsNullOrWhiteSpace(input)) - return false; - - string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - string name = args[0]; - args = args.Skip(1).ToArray(); - - return this.Trigger(name, args); - } - - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. - public bool Trigger(string name, string[] arguments) - { - // get normalised name - name = this.GetNormalisedName(name); - if (name == null) - return false; - - // get command - if (this.Commands.TryGetValue(name, out Command command)) - { - command.Callback.Invoke(name, arguments); - return true; - } - return false; - } - - - /********* - ** Private methods - *********/ - /// Get a normalised command name. - /// The command name. - private string GetNormalisedName(string name) - { - name = name?.Trim().ToLower(); - return !string.IsNullOrWhiteSpace(name) - ? name - : null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/AssetData.cs b/src/StardewModdingAPI/Framework/Content/AssetData.cs deleted file mode 100644 index 1ab9eebd..00000000 --- a/src/StardewModdingAPI/Framework/Content/AssetData.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Content -{ - /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. - /// The interface value type. - internal class AssetData : AssetInfo, IAssetData - { - /********* - ** Accessors - *********/ - /// The content data being read. - public TValue Data { get; protected set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetData(string locale, string assetName, TValue data, Func getNormalisedPath) - : base(locale, assetName, data.GetType(), getNormalisedPath) - { - this.Data = data; - } - - /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. - /// The new content value. - /// The is null. - /// The 's type is not compatible with the loaded asset's type. - public void ReplaceWith(TValue value) - { - if (value == null) - throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); - if (!this.DataType.IsInstanceOfType(value)) - throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); - - this.Data = value; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs deleted file mode 100644 index e9b29b12..00000000 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - internal class AssetDataForDictionary : AssetData>, IAssetDataForDictionary - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - public void Set(TKey key, TValue value) - { - this.Data[key] = value; - } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - public void Set(TKey key, Func value) - { - this.Data[key] = value(this.Data[key]); - } - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - public void Set(Func replacer) - { - foreach (var pair in this.Data.ToArray()) - this.Data[pair.Key] = replacer(pair.Key, pair.Value); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs deleted file mode 100644 index 45c5588b..00000000 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - internal class AssetDataForImage : AssetData, IAssetDataForImage - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Overwrite part of the image. - /// The image to patch into the content. - /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. - /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. - /// Indicates how an image should be patched. - /// One of the arguments is null. - /// The is outside the bounds of the spritesheet. - public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) - { - // get texture - Texture2D target = this.Data; - - // get areas - sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); - targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); - - // validate - if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); - if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) - throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) - throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) - throw new InvalidOperationException("The source and target areas must be the same size."); - - // get source data - int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; - Color[] sourceData = new Color[pixelCount]; - source.GetData(0, sourceArea, sourceData, 0, pixelCount); - - // merge data in overlay mode - if (patchMode == PatchMode.Overlay) - { - Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; - target.GetData(0, targetArea, newData, 0, newData.Length); - for (int i = 0; i < sourceData.Length; i++) - { - Color pixel = sourceData[i]; - if (pixel.A != 0) // not transparent - newData[i] = pixel; - } - sourceData = newData; - } - - // patch target texture - target.SetData(0, targetArea, sourceData, 0, pixelCount); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs deleted file mode 100644 index f30003e4..00000000 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to content being read from a data file. - internal class AssetDataForObject : AssetData, IAssetData - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Construct an instance. - /// The asset metadata. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetDataForObject(IAssetInfo info, object data, Func getNormalisedPath) - : this(info.Locale, info.AssetName, data, getNormalisedPath) { } - - /// Get a helper to manipulate the data as a dictionary. - /// The expected dictionary key. - /// The expected dictionary balue. - /// The content being read isn't a dictionary. - public IAssetDataForDictionary AsDictionary() - { - return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath); - } - - /// Get a helper to manipulate the data as an image. - /// The content being read isn't an image. - public IAssetDataForImage AsImage() - { - return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath); - } - - /// Get the data as a given type. - /// The expected data type. - /// The data can't be converted to . - public TData GetData() - { - if (!(this.Data is TData)) - throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); - return (TData)this.Data; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs deleted file mode 100644 index d580dc06..00000000 --- a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - internal class AssetInfo : IAssetInfo - { - /********* - ** Properties - *********/ - /// Normalises an asset key to match the cache key. - protected readonly Func GetNormalisedPath; - - - /********* - ** Accessors - *********/ - /// The content's locale code, if the content is localised. - public string Locale { get; } - - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. - public string AssetName { get; } - - /// The content data type. - public Type DataType { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content type being read. - /// Normalises an asset key to match the cache key. - public AssetInfo(string locale, string assetName, Type type, Func getNormalisedPath) - { - this.Locale = locale; - this.AssetName = assetName; - this.DataType = type; - this.GetNormalisedPath = getNormalisedPath; - } - - /// Get whether the asset name being loaded matches a given name after normalisation. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - public bool AssetNameEquals(string path) - { - path = this.GetNormalisedPath(path); - return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); - } - - - /********* - ** Protected methods - *********/ - /// Get a human-readable type name. - /// The type to name. - protected string GetFriendlyTypeName(Type type) - { - // dictionary - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - { - Type[] genericArgs = type.GetGenericArguments(); - return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; - } - - // texture - if (type == typeof(Texture2D)) - return type.Name; - - // native type - if (type == typeof(int)) - return "int"; - if (type == typeof(string)) - return "string"; - - // default - return type.FullName; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ContentManagerShim.cs b/src/StardewModdingAPI/Framework/ContentManagerShim.cs deleted file mode 100644 index d46f23a3..00000000 --- a/src/StardewModdingAPI/Framework/ContentManagerShim.cs +++ /dev/null @@ -1,50 +0,0 @@ -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// A minimal content manager which defers to SMAPI's main content manager. - internal class ContentManagerShim : LocalizedContentManager - { - /********* - ** Properties - *********/ - /// SMAPI's underlying content manager. - private readonly SContentManager ContentManager; - - - /********* - ** Accessors - *********/ - /// The content manager's name for logs (if any). - public string Name { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's underlying content manager. - /// The content manager's name for logs (if any). - public ContentManagerShim(SContentManager contentManager, string name) - : base(contentManager.ServiceProvider, contentManager.RootDirectory, contentManager.CurrentCulture, contentManager.LanguageCodeOverride) - { - this.ContentManager = contentManager; - this.Name = name; - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T Load(string assetName) - { - return this.ContentManager.LoadFor(assetName, this); - } - - /// Dispose held resources. - /// Whether the content manager is disposing (rather than finalising). - protected override void Dispose(bool disposing) - { - this.ContentManager.DisposeFor(this); - } - } -} diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/StardewModdingAPI/Framework/CursorPosition.cs deleted file mode 100644 index db02b3d1..00000000 --- a/src/StardewModdingAPI/Framework/CursorPosition.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace StardewModdingAPI.Framework -{ - /// Defines a position on a given map at different reference points. - internal class CursorPosition : ICursorPosition - { - /********* - ** Accessors - *********/ - /// The pixel position relative to the top-left corner of the visible screen. - public Vector2 ScreenPixels { get; } - - /// The tile position under the cursor relative to the top-left corner of the map. - public Vector2 Tile { get; } - - /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. - public Vector2 GrabTile { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The pixel position relative to the top-left corner of the visible screen. - /// The tile position relative to the top-left corner of the map. - /// The tile position that the game considers under the cursor for purposes of clicking actions. - public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) - { - this.ScreenPixels = screenPixels; - this.Tile = tile; - this.GrabTile = grabTile; - } - } -} diff --git a/src/StardewModdingAPI/Framework/DeprecationLevel.cs b/src/StardewModdingAPI/Framework/DeprecationLevel.cs deleted file mode 100644 index c0044053..00000000 --- a/src/StardewModdingAPI/Framework/DeprecationLevel.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Framework -{ - /// Indicates how deprecated something is. - internal enum DeprecationLevel - { - /// It's deprecated but won't be removed soon. Mod authors have some time to update their mods. Deprecation warnings should be logged, but not written to the console. - Notice, - - /// Mods should no longer be using it. Deprecation messages should be debug entries in the console. - Info, - - /// The code will be removed soon. Deprecation messages should be warnings in the console. - PendingRemoval - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs deleted file mode 100644 index b07c6c7d..00000000 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework -{ - /// Manages deprecation warnings. - internal class DeprecationManager - { - /********* - ** Properties - *********/ - /// The deprecations which have already been logged (as 'mod name::noun phrase::version'). - private readonly HashSet LoggedDeprecations = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - /// Encapsulates monitoring and logging for a given module. - private readonly IMonitor Monitor; - - /// Tracks the installed mods. - private readonly ModRegistry ModRegistry; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Encapsulates monitoring and logging for a given module. - /// Tracks the installed mods. - public DeprecationManager(IMonitor monitor, ModRegistry modRegistry) - { - this.Monitor = monitor; - this.ModRegistry = modRegistry; - } - - /// Log a deprecation warning. - /// A noun phrase describing what is deprecated. - /// The SMAPI version which deprecated it. - /// How deprecated the code is. - public void Warn(string nounPhrase, string version, DeprecationLevel severity) - { - this.Warn(this.ModRegistry.GetModFromStack(), nounPhrase, version, severity); - } - - /// Log a deprecation warning. - /// The friendly mod name which used the deprecated code. - /// A noun phrase describing what is deprecated. - /// The SMAPI version which deprecated it. - /// How deprecated the code is. - public void Warn(string source, string nounPhrase, string version, DeprecationLevel severity) - { - // ignore if already warned - if (!this.MarkWarned(source ?? "", nounPhrase, version)) - return; - - // build message - string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; - if (source == null) - message += $"{Environment.NewLine}{Environment.StackTrace}"; - - // log message - switch (severity) - { - case DeprecationLevel.Notice: - this.Monitor.Log(message, LogLevel.Trace); - break; - - case DeprecationLevel.Info: - this.Monitor.Log(message, LogLevel.Debug); - break; - - case DeprecationLevel.PendingRemoval: - this.Monitor.Log(message, LogLevel.Warn); - break; - - default: - throw new NotSupportedException($"Unknown deprecation level '{severity}'"); - } - } - - /// Mark a deprecation warning as already logged. - /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). - /// The SMAPI version which deprecated it. - /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. - public bool MarkWarned(string nounPhrase, string version) - { - return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version); - } - - /// Mark a deprecation warning as already logged. - /// The friendly name of the assembly which used the deprecated code. - /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). - /// The SMAPI version which deprecated it. - /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. - public bool MarkWarned(string source, string nounPhrase, string version) - { - if (string.IsNullOrWhiteSpace(source)) - throw new InvalidOperationException("The deprecation source cannot be empty."); - - string key = $"{source}::{nounPhrase}::{version}"; - if (this.LoggedDeprecations.Contains(key)) - return false; - this.LoggedDeprecations.Add(key); - return true; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs deleted file mode 100644 index ec9279f1..00000000 --- a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Exceptions -{ - /// An exception thrown when an assembly can't be loaded by SMAPI, with all the relevant details in the message. - internal class SAssemblyLoadFailedException : Exception - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The error message. - public SAssemblyLoadFailedException(string message) - : base(message) { } - } -} diff --git a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs b/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs deleted file mode 100644 index 85d85e3d..00000000 --- a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Content; - -namespace StardewModdingAPI.Framework.Exceptions -{ - /// An implementation of used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework. - internal class SContentLoadException : ContentLoadException - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The error message. - /// The underlying exception, if any. - public SContentLoadException(string message, Exception ex = null) - : base(message, ex) { } - } -} diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs deleted file mode 100644 index f7133ee7..00000000 --- a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Exceptions -{ - /// A format exception which provides a user-facing error message. - internal class SParseException : FormatException - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The error message. - /// The underlying exception, if any. - public SParseException(string message, Exception ex = null) - : base(message, ex) { } - } -} diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/StardewModdingAPI/Framework/GameVersion.cs deleted file mode 100644 index 48159f61..00000000 --- a/src/StardewModdingAPI/Framework/GameVersion.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework -{ - /// An implementation of that correctly handles the non-semantic versions used by older Stardew Valley releases. - internal class GameVersion : SemanticVersion - { - /********* - ** Private methods - *********/ - /// A mapping of game to semantic versions. - private static readonly IDictionary VersionMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - ["1.01"] = "1.0.1", - ["1.02"] = "1.0.2", - ["1.03"] = "1.0.3", - ["1.04"] = "1.0.4", - ["1.05"] = "1.0.5", - ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. - ["1.051b"] = "1.0.6-prelease2", - ["1.06"] = "1.0.6", - ["1.07"] = "1.0.7", - ["1.07a"] = "1.0.8-prerelease1", - ["1.11"] = "1.1.1" - }; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The game version string. - public GameVersion(string version) - : base(GameVersion.GetSemanticVersionString(version)) { } - - /// Get a string representation of the version. - public override string ToString() - { - return GameVersion.GetGameVersionString(base.ToString()); - } - - - /********* - ** Private methods - *********/ - /// Convert a game version string to a semantic version string. - /// The game version string. - private static string GetSemanticVersionString(string gameVersion) - { - return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) - ? semanticVersion - : gameVersion; - } - - /// Convert a game version string to a semantic version string. - /// The game version string. - private static string GetGameVersionString(string gameVersion) - { - foreach (var mapping in GameVersion.VersionMap) - { - if (mapping.Value.Equals(gameVersion, StringComparison.InvariantCultureIgnoreCase)) - return mapping.Key; - } - return gameVersion; - } - } -} diff --git a/src/StardewModdingAPI/Framework/IModMetadata.cs b/src/StardewModdingAPI/Framework/IModMetadata.cs deleted file mode 100644 index c21734a7..00000000 --- a/src/StardewModdingAPI/Framework/IModMetadata.cs +++ /dev/null @@ -1,47 +0,0 @@ -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.ModLoading; - -namespace StardewModdingAPI.Framework -{ - /// Metadata for a mod. - internal interface IModMetadata - { - /********* - ** Accessors - *********/ - /// The mod's display name. - string DisplayName { get; } - - /// The mod's full directory path. - string DirectoryPath { get; } - - /// The mod manifest. - IManifest Manifest { get; } - - /// >Metadata about the mod from SMAPI's internal data (if any). - ModDataRecord DataRecord { get; } - - /// The metadata resolution status. - ModMetadataStatus Status { get; } - - /// The reason the metadata is invalid, if any. - string Error { get; } - - /// The mod instance (if it was loaded). - IMod Mod { get; } - - - /********* - ** Public methods - *********/ - /// Set the mod status. - /// The metadata resolution status. - /// The reason the metadata is invalid, if any. - /// Return the instance for chaining. - IModMetadata SetStatus(ModMetadataStatus status, string error = null); - - /// Set the mod instance. - /// The mod instance to set. - IModMetadata SetMod(IMod mod); - } -} diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs deleted file mode 100644 index 3709e05d..00000000 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Reflection; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// Provides extension methods for SMAPI's internal use. - internal static class InternalExtensions - { - /**** - ** IMonitor - ****/ - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments (or null to pass ). - public static void SafelyRaisePlainEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender = null, EventArgs args = null) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast()) - { - // handle SMAPI exiting - if (monitor.IsExiting) - { - monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn); - return; - } - - // raise event - try - { - handler.Invoke(sender, args ?? EventArgs.Empty); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// The event argument object type. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments. - public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast>()) - { - try - { - handler.Invoke(sender, args); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - - /// Log a message for the player or developer the first time it occurs. - /// The monitor through which to log the message. - /// The hash of logged messages. - /// The message to log. - /// The log severity level. - public static void LogOnce(this IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) - { - if (!hash.Contains(message)) - { - monitor.Log(message, level); - hash.Add(message); - } - } - - /**** - ** Exceptions - ****/ - /// Get a string representation of an exception suitable for writing to the error log. - /// The error to summarise. - public static string GetLogSummary(this Exception exception) - { - switch (exception) - { - case TypeLoadException ex: - return $"Failed loading type '{ex.TypeName}': {exception}"; - - case ReflectionTypeLoadException ex: - string summary = exception.ToString(); - foreach (Exception childEx in ex.LoaderExceptions) - summary += $"\n\n{childEx.GetLogSummary()}"; - return summary; - - default: - return exception.ToString(); - } - } - - /**** - ** Sprite batch - ****/ - /// Get whether the sprite batch is between a begin and end pair. - /// The sprite batch to check. - /// The reflection helper with which to access private fields. - public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection) - { - // get field name - const string fieldName = -#if SMAPI_FOR_WINDOWS - "inBeginEndPair"; -#else - "_beginCalled"; -#endif - - // get result - return reflection.GetPrivateField(Game1.spriteBatch, fieldName).GetValue(); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs deleted file mode 100644 index b8f2c34e..00000000 --- a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Logging -{ - /// Manages console output interception. - internal class ConsoleInterceptionManager : IDisposable - { - /********* - ** Properties - *********/ - /// The intercepting console writer. - private readonly InterceptingTextWriter Output; - - - /********* - ** Accessors - *********/ - /// Whether the current console supports color formatting. - public bool SupportsColor { get; } - - /// The event raised when a message is written to the console directly. - public event Action OnMessageIntercepted; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ConsoleInterceptionManager() - { - // redirect output through interceptor - this.Output = new InterceptingTextWriter(Console.Out); - this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); - Console.SetOut(this.Output); - - // test color support - this.SupportsColor = this.TestColorSupport(); - } - - /// Get an exclusive lock and write to the console output without interception. - /// The action to perform within the exclusive write block. - public void ExclusiveWriteWithoutInterception(Action action) - { - lock (Console.Out) - { - try - { - this.Output.ShouldIntercept = false; - action(); - } - finally - { - this.Output.ShouldIntercept = true; - } - } - } - - /// Release all resources. - public void Dispose() - { - Console.SetOut(this.Output.Out); - this.Output.Dispose(); - } - - - /********* - ** private methods - *********/ - /// Test whether the current console supports color formatting. - private bool TestColorSupport() - { - try - { - this.ExclusiveWriteWithoutInterception(() => - { - Console.ForegroundColor = Console.ForegroundColor; - }); - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs deleted file mode 100644 index 9ca61b59..00000000 --- a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace StardewModdingAPI.Framework.Logging -{ - /// A text writer which allows intercepting output. - internal class InterceptingTextWriter : TextWriter - { - /********* - ** Accessors - *********/ - /// The underlying console output. - public TextWriter Out { get; } - - /// The character encoding in which the output is written. - public override Encoding Encoding => this.Out.Encoding; - - /// Whether to intercept console output. - public bool ShouldIntercept { get; set; } - - /// The event raised when a message is written to the console directly. - public event Action OnMessageIntercepted; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying output writer. - public InterceptingTextWriter(TextWriter output) - { - this.Out = output; - } - - /// Writes a subarray of characters to the text string or stream. - /// The character array to write data from. - /// The character position in the buffer at which to start retrieving data. - /// The number of characters to write. - public override void Write(char[] buffer, int index, int count) - { - if (this.ShouldIntercept) - this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); - else - this.Out.Write(buffer, index, count); - } - - /// Writes a character to the text string or stream. - /// The character to write to the text stream. - /// Console log messages from the game should be caught by . This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone. - public override void Write(char ch) - { - this.Out.Write(ch); - } - - /// Releases the unmanaged resources used by the and optionally releases the managed resources. - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected override void Dispose(bool disposing) - { - this.OnMessageIntercepted = null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs deleted file mode 100644 index 8cfe0527..00000000 --- a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.IO; - -namespace StardewModdingAPI.Framework.Logging -{ - /// Manages reading and writing to log file. - internal class LogFileManager : IDisposable - { - /********* - ** Properties - *********/ - /// The underlying stream writer. - private readonly StreamWriter Stream; - - - /********* - ** Accessors - *********/ - /// The full path to the log file being written. - public string Path { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The log file to write. - public LogFileManager(string path) - { - this.Path = path; - - // create log directory if needed - string logDir = System.IO.Path.GetDirectoryName(path); - if (logDir == null) - throw new ArgumentException($"The log path '{path}' is not valid."); - Directory.CreateDirectory(logDir); - - // open log file stream - this.Stream = new StreamWriter(path, append: false) { AutoFlush = true }; - } - - /// Write a message to the log. - /// The message to log. - public void WriteLine(string message) - { - // always use Windows-style line endings for convenience - // (Linux/Mac editors are fine with them, Windows editors often require them) - this.Stream.Write(message + "\r\n"); - } - - /// Release all resources. - public void Dispose() - { - this.Stream.Dispose(); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs deleted file mode 100644 index 16032da1..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// The common base class for mod helpers. - internal abstract class BaseHelper : IModLinked - { - /********* - ** Accessors - *********/ - /// The unique ID of the mod for which the helper was created. - public string ModID { get; } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// The unique ID of the relevant mod. - protected BaseHelper(string modID) - { - this.ModID = modID; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs deleted file mode 100644 index bdedb07c..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides an API for managing console commands. - internal class CommandHelper : BaseHelper, ICommandHelper - { - /********* - ** Accessors - *********/ - /// The friendly mod name for this instance. - private readonly string ModName; - - /// Manages console commands. - private readonly CommandManager CommandManager; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique ID of the relevant mod. - /// The friendly mod name for this instance. - /// Manages console commands. - public CommandHelper(string modID, string modName, CommandManager commandManager) - : base(modID) - { - this.ModName = modName; - this.CommandManager = commandManager; - } - - /// Add a console command. - /// The command name, which the user must type to trigger it. - /// The human-readable documentation shown when the player runs the built-in 'help' command. - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - /// The or is null or empty. - /// The is not a valid format. - /// There's already a command with that name. - public ICommandHelper Add(string name, string documentation, Action callback) - { - this.CommandManager.Add(this.ModName, name, documentation, callback); - return this; - } - - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. - public bool Trigger(string name, string[] arguments) - { - return this.CommandManager.Trigger(name, arguments); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs deleted file mode 100644 index 4440ae40..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ /dev/null @@ -1,476 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Exceptions; -using StardewValley; -using xTile; -using xTile.Format; -using xTile.Tiles; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides an API for loading content assets. - internal class ContentHelper : BaseHelper, IContentHelper - { - /********* - ** Properties - *********/ - /// SMAPI's underlying content manager. - private readonly SContentManager ContentManager; - - /// The absolute path to the mod folder. - private readonly string ModFolderPath; - - /// The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName"). - private readonly string ModFolderPathFromContent; - - /// The friendly mod name for use in errors. - private readonly string ModName; - - /// Encapsulates monitoring and logging for a given module. - private readonly IMonitor Monitor; - - - /********* - ** Accessors - *********/ - /// The game's current locale code (like pt-BR). - public string CurrentLocale => this.ContentManager.GetLocale(); - - /// The game's current locale as an enum value. - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); - - /// The observable implementation of . - internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); - - /// The observable implementation of . - internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); - - /// Interceptors which provide the initial versions of matching content assets. - public IList AssetLoaders => this.ObservableAssetLoaders; - - /// Interceptors which edit matching content assets after they're loaded. - public IList AssetEditors => this.ObservableAssetEditors; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's underlying content manager. - /// The absolute path to the mod folder. - /// The unique ID of the relevant mod. - /// The friendly mod name for use in errors. - /// Encapsulates monitoring and logging. - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) - : base(modID) - { - this.ContentManager = contentManager; - this.ModFolderPath = modFolderPath; - this.ModName = modName; - this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); - this.Monitor = monitor; - } - - /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - public T Load(string key, ContentSource source = ContentSource.ModFolder) - { - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); - - this.AssertValidAssetKeyFormat(key); - try - { - switch (source) - { - case ContentSource.GameContent: - return this.ContentManager.Load(key); - - case ContentSource.ModFolder: - // get file - FileInfo file = this.GetModFile(key); - if (!file.Exists) - throw GetContentError($"there's no matching file at path '{file.FullName}'."); - - // get asset path - string assetPath = this.GetModAssetPath(key, file.FullName); - - // try cache - if (this.ContentManager.IsLoaded(assetPath)) - return this.ContentManager.Load(assetPath); - - // load content - switch (file.Extension.ToLower()) - { - // XNB file - case ".xnb": - { - T asset = this.ContentManager.Load(assetPath); - if (asset is Map) - this.FixLocalMapTilesheets(asset as Map, key); - return asset; - } - - // unpacked map - case ".tbin": - { - // validate - if (typeof(T) != typeof(Map)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); - - // fetch & cache - FormatManager formatManager = FormatManager.Instance; - Map map = formatManager.LoadMap(file.FullName); - this.FixLocalMapTilesheets(map, key); - - // inject map - this.ContentManager.Inject(assetPath, map); - return (T)(object)map; - } - - // unpacked image - case ".png": - // validate - if (typeof(T) != typeof(Texture2D)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - - // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - this.ContentManager.Inject(assetPath, texture); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); - } - - default: - throw GetContentError($"unknown content source '{source}'."); - } - } - catch (Exception ex) when (!(ex is SContentLoadException)) - { - throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); - } - } - - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) - { - switch (source) - { - case ContentSource.GameContent: - return this.ContentManager.NormaliseAssetName(key); - - case ContentSource.ModFolder: - FileInfo file = this.GetModFile(key); - return this.ContentManager.NormaliseAssetName(this.GetModAssetPath(key, file.FullName)); - - default: - throw new NotSupportedException($"Unknown content source '{source}'."); - } - } - - /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to invalidate in the content folder. - /// The is empty or contains invalid characters. - /// Returns whether the given asset key was cached. - public bool InvalidateCache(string key) - { - this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace); - string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); - return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); - } - - /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. - /// The asset type to remove from the cache. - /// Returns whether any assets were invalidated. - public bool InvalidateCache() - { - this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); - return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); - } - - /********* - ** Private methods - *********/ - /// Fix the tilesheets for a map loaded from the mod folder. - /// The map whose tilesheets to fix. - /// The map asset key within the mod folder. - /// The map tilesheets could not be loaded. - /// - /// The game's logic for tilesheets in is a bit specialised. It boils down to this: - /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the Content folder. - /// * Else it's loaded from Content\Maps with a seasonal prefix. - /// - /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. - /// Instead we use a more heuristic approach: check relative to the map file first, then relative to - /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, we try - /// for a seasonal variation and then an exact match. - /// - /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. - /// - private void FixLocalMapTilesheets(Map map, string mapKey) - { - // check map info - if (!map.TileSheets.Any()) - return; - mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators - string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder - - // fix tilesheets - foreach (TileSheet tilesheet in map.TileSheets) - { - string imageSource = tilesheet.ImageSource; - - // get seasonal name (if applicable) - string seasonalImageSource = null; - if (Game1.currentSeason != null) - { - string filename = Path.GetFileName(imageSource); - bool hasSeasonalPrefix = - filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) - || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) - || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase) - || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase); - if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_")) - { - string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase)); - seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}"; - } - } - - // load best match - try - { - string key = - this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource) - ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource); - if (key != null) - { - tilesheet.ImageSource = key; - continue; - } - } - catch (Exception ex) - { - throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); - } - - // none found - throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder."); - } - } - - /// Load a tilesheet image source if the file exists. - /// The folder path containing the map, relative to the mod folder. - /// The tilesheet image source to load. - /// Returns the loaded asset key (if it was loaded successfully). - /// See remarks on . - private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource) - { - if (imageSource == null) - return null; - - // check relative to map file - { - string localKey = Path.Combine(relativeMapFolder, imageSource); - FileInfo localFile = this.GetModFile(localKey); - if (localFile.Exists) - { - try - { - this.Load(localKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex); - } - - return this.GetActualAssetKey(localKey); - } - } - - // check relative to content folder - { - foreach (string candidateKey in new[] { imageSource, $@"Maps\{imageSource}" }) - { - string contentKey = candidateKey.EndsWith(".png") - ? candidateKey.Substring(0, imageSource.Length - 4) - : candidateKey; - - try - { - this.Load(contentKey, ContentSource.GameContent); - return contentKey; - } - catch - { - // ignore file-not-found errors - // TODO: while it's useful to suppress a asset-not-found error here to avoid - // confusion, this is a pretty naive approach. Even if the file doesn't exist, - // the file may have been loaded through an IAssetLoader which failed. So even - // if the content file doesn't exist, that doesn't mean the error here is a - // content-not-found error. Unfortunately XNA doesn't provide a good way to - // detect the error type. - if (this.GetContentFolderFile(contentKey).Exists) - throw; - } - } - } - - // not found - return null; - } - - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertValidAssetKeyFormat(string key) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new ArgumentException("The asset key or local path contains invalid characters."); - } - - /// Get a file from the mod folder. - /// The asset path relative to the mod folder. - private FileInfo GetModFile(string path) - { - // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); - FileInfo file = new FileInfo(path); - - // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") - { - FileInfo result = new FileInfo(path + ".xnb"); - if (result.Exists) - file = result; - } - - return file; - } - - /// Get a file from the game's content folder. - /// The asset key. - private FileInfo GetContentFolderFile(string key) - { - // get file path - string path = Path.Combine(this.ContentManager.FullRootDirectory, key); - if (!path.EndsWith(".xnb")) - path += ".xnb"; - - // get file - return new FileInfo(path); - } - - /// Get the asset path which loads a mod folder through a content manager. - /// The file path relative to the mod's folder. - /// The absolute file path. - private string GetModAssetPath(string localPath, string absolutePath) - { -#if SMAPI_FOR_WINDOWS - // XNA doesn't allow absolute asset paths, so get a path relative to the content folder - return Path.Combine(this.ModFolderPathFromContent, localPath); -#else - // MonoGame is weird about relative paths on Mac, but allows absolute paths - return absolutePath; -#endif - } - - /// Get a directory path relative to a given root. - /// The root path from which the path should be relative. - /// The target file path. - private string GetRelativePath(string rootPath, string targetPath) - { - // convert to URIs - Uri from = new Uri(rootPath + "/"); - Uri to = new Uri(targetPath + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); - - // get relative path - return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) - .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs deleted file mode 100644 index 665b9cf4..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.IO; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides simplified APIs for writing mods. - internal class ModHelper : BaseHelper, IModHelper, IDisposable - { - /********* - ** Properties - *********/ - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper; - - - /********* - ** Accessors - *********/ - /// The full path to the mod's folder. - public string DirectoryPath { get; } - - /// An API for loading content assets. - public IContentHelper Content { get; } - - /// An API for accessing private game code. - public IReflectionHelper Reflection { get; } - - /// an API for fetching metadata about loaded mods. - public IModRegistry ModRegistry { get; } - - /// An API for managing console commands. - public ICommandHelper ConsoleCommands { get; } - - /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - public ITranslationHelper Translation { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's unique ID. - /// The full path to the mod's folder. - /// Encapsulate SMAPI's JSON parsing. - /// An API for loading content assets. - /// An API for managing console commands. - /// an API for fetching metadata about loaded mods. - /// An API for accessing private game code. - /// An API for reading translations stored in the mod's i18n folder. - /// An argument is null or empty. - /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) - : base(modID) - { - // validate directory - if (string.IsNullOrWhiteSpace(modDirectory)) - throw new ArgumentNullException(nameof(modDirectory)); - if (!Directory.Exists(modDirectory)) - throw new InvalidOperationException("The specified mod directory does not exist."); - - // initialise - this.DirectoryPath = modDirectory; - this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); - this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); - this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); - this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); - this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); - this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); - } - - /**** - ** Mod config file - ****/ - /// Read the mod's configuration file (and create it if needed). - /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. - public TConfig ReadConfig() - where TConfig : class, new() - { - TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); - this.WriteConfig(config); // create file or fill in missing fields - return config; - } - - /// Save to the mod's configuration file. - /// The config class type. - /// The config settings to save. - public void WriteConfig(TConfig config) - where TConfig : class, new() - { - this.WriteJsonFile("config.json", config); - } - - /**** - ** Generic JSON files - ****/ - /// Read a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - public TModel ReadJsonFile(string path) - where TModel : class - { - path = Path.Combine(this.DirectoryPath, path); - return this.JsonHelper.ReadJsonFile(path); - } - - /// Save to a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// The model to save. - public void WriteJsonFile(string path, TModel model) - where TModel : class - { - path = Path.Combine(this.DirectoryPath, path); - this.JsonHelper.WriteJsonFile(path, model); - } - - - /**** - ** Disposal - ****/ - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - // nothing to dispose yet - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs deleted file mode 100644 index 9e824694..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides metadata about installed mods. - internal class ModRegistryHelper : BaseHelper, IModRegistry - { - /********* - ** Properties - *********/ - /// The underlying mod registry. - private readonly ModRegistry Registry; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique ID of the relevant mod. - /// The underlying mod registry. - public ModRegistryHelper(string modID, ModRegistry registry) - : base(modID) - { - this.Registry = registry; - } - - /// Get metadata for all loaded mods. - public IEnumerable GetAll() - { - return this.Registry.GetAll(); - } - - /// Get metadata for a loaded mod. - /// The mod's unique ID. - /// Returns the matching mod's metadata, or null if not found. - public IManifest Get(string uniqueID) - { - return this.Registry.Get(uniqueID); - } - - /// Get whether a mod has been loaded. - /// The mod's unique ID. - public bool IsLoaded(string uniqueID) - { - return this.Registry.IsLoaded(uniqueID); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs deleted file mode 100644 index 8d435416..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using StardewModdingAPI.Framework.Reflection; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides helper methods for accessing private game code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). - internal class ReflectionHelper : BaseHelper, IReflectionHelper - { - /********* - ** Properties - *********/ - /// The underlying reflection helper. - private readonly Reflector Reflector; - - /// The mod name for error messages. - private readonly string ModName; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique ID of the relevant mod. - /// The mod name for error messages. - /// The underlying reflection helper. - public ReflectionHelper(string modID, string modName, Reflector reflector) - : base(modID) - { - this.ModName = modName; - this.Reflector = reflector; - } - - /**** - ** Fields - ****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field wrapper, or null if the field doesn't exist and is false. - public IPrivateField GetPrivateField(object obj, string name, bool required = true) - { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateField(obj, name, required); - } - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) - { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateField(type, name, required); - } - - /**** - ** Properties - ****/ - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) - { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateProperty(obj, name, required); - } - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) - { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateProperty(type, name, required); - } - - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(object obj, string name, bool required = true) - { - this.AssertAccessAllowed(obj); - IPrivateField field = this.GetPrivateField(obj, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(Type type, string name, bool required = true) - { - this.AssertAccessAllowed(type); - IPrivateField field = this.GetPrivateField(type, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /**** - ** Methods - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) - { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, required); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) - { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, required); - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); - } - - - /********* - ** Private methods - *********/ - /// Assert that mods can use the reflection helper to access the given type. - /// The type being accessed. - private void AssertAccessAllowed(Type type) - { - // validate type namespace - if (type.Namespace != null) - { - string rootSmapiNamespace = typeof(Program).Namespace; - if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) - throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); - } - } - - /// Assert that mods can use the reflection helper to access the given type. - /// The object being accessed. - private void AssertAccessAllowed(object obj) - { - if (obj != null) - this.AssertAccessAllowed(obj.GetType()); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs deleted file mode 100644 index bbe3a81a..00000000 --- a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - internal class TranslationHelper : BaseHelper, ITranslationHelper - { - /********* - ** Properties - *********/ - /// The name of the relevant mod for error messages. - private readonly string ModName; - - /// The translations for each locale. - private readonly IDictionary> All = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - - /// The translations for the current locale, with locale fallback taken into account. - private IDictionary ForLocale; - - - /********* - ** Accessors - *********/ - /// The current locale. - public string Locale { get; private set; } - - /// The game's current language code. - public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique ID of the relevant mod. - /// The name of the relevant mod for error messages. - /// The initial locale. - /// The game's current language code. - public TranslationHelper(string modID, string modName, string locale, LocalizedContentManager.LanguageCode languageCode) - : base(modID) - { - // save data - this.ModName = modName; - - // set locale - this.SetLocale(locale, languageCode); - } - - /// Get all translations for the current locale. - public IEnumerable GetTranslations() - { - return this.ForLocale.Values.ToArray(); - } - - /// Get a translation for the current locale. - /// The translation key. - public Translation Get(string key) - { - this.ForLocale.TryGetValue(key, out Translation translation); - return translation ?? new Translation(this.ModName, this.Locale, key, null); - } - - /// Get a translation for the current locale. - /// The translation key. - /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. - public Translation Get(string key, object tokens) - { - return this.Get(key).Tokens(tokens); - } - - /// Set the translations to use. - /// The translations to use. - internal TranslationHelper SetTranslations(IDictionary> translations) - { - // reset translations - this.All.Clear(); - foreach (var pair in translations) - this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); - - // rebuild cache - this.SetLocale(this.Locale, this.LocaleEnum); - - return this; - } - - /// Set the current locale and precache translations. - /// The current locale. - /// The game's current language code. - internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) - { - this.Locale = locale.ToLower().Trim(); - this.LocaleEnum = localeEnum; - - this.ForLocale = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (string next in this.GetRelevantLocales(this.Locale)) - { - // skip if locale not defined - if (!this.All.TryGetValue(next, out IDictionary translations)) - continue; - - // add missing translations - foreach (var pair in translations) - { - if (!this.ForLocale.ContainsKey(pair.Key)) - this.ForLocale.Add(pair.Key, new Translation(this.ModName, this.Locale, pair.Key, pair.Value)); - } - } - } - - - /********* - ** Private methods - *********/ - /// Get the locales which can provide translations for the given locale, in precedence order. - /// The locale for which to find valid locales. - private IEnumerable GetRelevantLocales(string locale) - { - // given locale - yield return locale; - - // broader locales (like pt-BR => pt) - while (true) - { - int dashIndex = locale.LastIndexOf('-'); - if (dashIndex <= 0) - break; - - locale = locale.Substring(0, dashIndex); - yield return locale; - } - - // default - if (locale != "default") - yield return "default"; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs deleted file mode 100644 index 4378798c..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// A minimal assembly definition resolver which resolves references to known assemblies. - internal class AssemblyDefinitionResolver : DefaultAssemblyResolver - { - /********* - ** Properties - *********/ - /// The known assemblies. - private readonly IDictionary Loaded = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Add known assemblies to the resolver. - /// The known assemblies. - public void Add(params AssemblyDefinition[] assemblies) - { - foreach (AssemblyDefinition assembly in assemblies) - { - this.Loaded[assembly.Name.Name] = assembly; - this.Loaded[assembly.Name.FullName] = assembly; - } - } - - /// Resolve an assembly reference. - /// The assembly name. - public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); - - /// Resolve an assembly reference. - /// The assembly name. - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); - - - /********* - ** Private methods - *********/ - /// Resolve a known assembly definition based on its short or full name. - /// The assembly's short or full name. - private AssemblyDefinition ResolveName(string name) - { - return this.Loaded.ContainsKey(name) - ? this.Loaded[name] - : null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs deleted file mode 100644 index 11be19fc..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Indicates the result of an assembly load. - internal enum AssemblyLoadStatus - { - /// The assembly was loaded successfully. - Okay = 1, - - /// The assembly could not be loaded. - Failed = 2, - - /// The assembly is already loaded. - AlreadyLoaded = 3 - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs deleted file mode 100644 index 1e3c4a05..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Metadata; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Preprocesses and loads mod assemblies. - internal class AssemblyLoader - { - /********* - ** Properties - *********/ - /// Metadata for mapping assemblies to the current platform. - private readonly PlatformAssemblyMap AssemblyMap; - - /// A type => assembly lookup for types which should be rewritten. - private readonly IDictionary TypeAssemblies; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// Whether to enable developer mode logging. - private readonly bool IsDeveloperMode; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current game platform. - /// Encapsulates monitoring and logging. - /// Whether to enable developer mode logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) - { - this.Monitor = monitor; - this.IsDeveloperMode = isDeveloperMode; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - - // generate type => assembly lookup for types which should be rewritten - this.TypeAssemblies = new Dictionary(); - foreach (Assembly assembly in this.AssemblyMap.Targets) - { - ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; - foreach (TypeDefinition type in module.GetTypes()) - { - if (!type.IsPublic) - continue; // no need to rewrite - if (type.Namespace.Contains("<")) - continue; // ignore assembly metadata - this.TypeAssemblies[type.FullName] = assembly; - } - } - } - - /// Preprocess and load an assembly. - /// The mod for which the assembly is being loaded. - /// The assembly file path. - /// Assume the mod is compatible, even if incompatible code is detected. - /// Returns the rewrite metadata for the preprocessed assembly. - /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) - { - // get referenced local assemblies - AssemblyParseResult[] assemblies; - { - AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); - HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded - assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); - } - - // validate load - if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) - { - throw new SAssemblyLoadFailedException(!File.Exists(assemblyPath) - ? $"Could not load '{assemblyPath}' because it doesn't exist." - : $"Could not load '{assemblyPath}'." - ); - } - if (assemblies.Last().Status == AssemblyLoadStatus.AlreadyLoaded) // mod assembly is last in dependency order - throw new SAssemblyLoadFailedException($"Could not load '{assemblyPath}' because it was already loaded. Do you have two copies of this mod?"); - - // rewrite & load assemblies in leaf-to-root order - bool oneAssembly = assemblies.Length == 1; - Assembly lastAssembly = null; - HashSet loggedMessages = new HashSet(); - foreach (AssemblyParseResult assembly in assemblies) - { - if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) - continue; - - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); - if (changed) - { - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } - } - else - { - if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); - lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); - } - } - - // last assembly loaded is the root - return lastAssembly; - } - - /// Resolve an assembly by its name. - /// The assembly name. - /// - /// This implementation returns the first loaded assembly which matches the short form of - /// the assembly name, to resolve assembly resolution issues when rewriting - /// assemblies (especially with Mono). Since this is meant to be called on , - /// the implicit assumption is that loading the exact assembly failed. - /// - public Assembly ResolveAssembly(string name) - { - string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) - return AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(p => p.GetName().Name == shortName); - } - - - /********* - ** Private methods - *********/ - /**** - ** Assembly parsing - ****/ - /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. - /// The assembly file to load. - /// The assembly names that should be skipped. - /// A resolver which resolves references to known assemblies. - /// Returns the rewrite metadata for the preprocessed assembly. - private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) - { - // validate - if (file.Directory == null) - throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); - if (!file.Exists) - yield break; // not a local assembly - - // read assembly - byte[] assemblyBytes = File.ReadAllBytes(file.FullName); - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); - - // skip if already visited - if (visitedAssemblyNames.Contains(assembly.Name.Name)) - yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); - visitedAssemblyNames.Add(assembly.Name.Name); - - // yield referenced assemblies - foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) - { - FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); - foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) - yield return result; - } - - // yield assembly - yield return new AssemblyParseResult(file, assembly, AssemblyLoadStatus.Okay); - } - - /**** - ** Assembly rewriting - ****/ - /// Rewrite the types referenced by an assembly. - /// The mod for which the assembly is being loaded. - /// The assembly to rewrite. - /// Assume the mod is compatible, even if incompatible code is detected. - /// The messages that have already been logged for this mod. - /// A string to prefix to log messages. - /// Returns whether the assembly was modified. - /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) - { - ModuleDefinition module = assembly.MainModule; - string filename = $"{assembly.Name.Name}.dll"; - - // swap assembly references if needed (e.g. XNA => MonoGame) - bool platformChanged = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) - { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) - { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; - } - } - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); - } - - // find (and optionally rewrite) incompatible instructions - bool anyRewritten = false; - IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) - { - // check method definition - foreach (IInstructionHandler handler in handlers) - { - InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; - } - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - foreach (Instruction instruction in cil.Body.Instructions.ToArray()) - { - foreach (IInstructionHandler handler in handlers) - { - InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; - } - } - } - - return platformChanged || anyRewritten; - } - - /// Process the result from an instruction handler. - /// The mod being analysed. - /// The instruction handler. - /// The result returned by the handler. - /// The messages already logged for the current mod. - /// Assume the mod is compatible, even if incompatible code is detected. - /// A string to prefix to log messages. - /// The assembly filename for log messages. - private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) - { - switch (result) - { - case InstructionHandleResult.Rewritten: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); - break; - - case InstructionHandleResult.NotCompatible: - if (!assumeCompatible) - throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - break; - - case InstructionHandleResult.DetectedGamePatch: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); - break; - - case InstructionHandleResult.DetectedSaveSerialiser: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); - break; - - case InstructionHandleResult.DetectedDynamic: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", -#if SMAPI_FOR_WINDOWS - this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug -#else - LogLevel.Warn -#endif - ); - break; - - case InstructionHandleResult.None: - break; - - default: - throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); - } - } - - /// Get the correct reference to use for compatibility with the current platform. - /// The type reference to rewrite. - private void ChangeTypeScope(TypeReference type) - { - // check skip conditions - if (type == null || type.FullName.StartsWith("System.")) - return; - - // get assembly - if (!this.TypeAssemblies.TryGetValue(type.FullName, out Assembly assembly)) - return; - - // replace scope - AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; - type.Scope = assemblyRef; - } - - /// Get all methods in a module. - /// The module to search. - private IEnumerable GetMethods(ModuleDefinition module) - { - return ( - from type in module.GetTypes() - where type.HasMethods - from method in type.Methods - where method.HasBody - select method - ); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs deleted file mode 100644 index b56a776c..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.IO; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Metadata about a parsed assembly definition. - internal class AssemblyParseResult - { - /********* - ** Accessors - *********/ - /// The original assembly file. - public readonly FileInfo File; - - /// The assembly definition. - public readonly AssemblyDefinition Definition; - - /// The result of the assembly load. - public AssemblyLoadStatus Status; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The original assembly file. - /// The assembly definition. - /// The result of the assembly load. - public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status) - { - this.File = file; - this.Definition = assembly; - this.Status = status; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs deleted file mode 100644 index e4beb7a9..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds incompatible CIL instructions that reference a given event. - internal class EventFinder : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; - - /// The event name for which to find references. - private readonly string EventName; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name for which to find references. - /// The event name for which to find references. - /// The result to return for matching instructions. - public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) - { - this.FullTypeName = fullTypeName; - this.EventName = eventName; - this.Result = result; - this.NounPhrase = $"{fullTypeName}.{eventName} event"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.FullTypeName - && (methodRef.Name == "add_" + this.EventName || methodRef.Name == "remove_" + this.EventName); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs deleted file mode 100644 index 00805815..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds incompatible CIL instructions that reference a given field. - internal class FieldFinder : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; - - /// The field name for which to find references. - private readonly string FieldName; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name for which to find references. - /// The field name for which to find references. - /// The result to return for matching instructions. - public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) - { - this.FullTypeName = fullTypeName; - this.FieldName = fieldName; - this.Result = result; - this.NounPhrase = $"{fullTypeName}.{fieldName} field"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - return - fieldRef != null - && fieldRef.DeclaringType.FullName == this.FullTypeName - && fieldRef.Name == this.FieldName; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs deleted file mode 100644 index 5358f181..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds incompatible CIL instructions that reference a given method. - internal class MethodFinder : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; - - /// The method name for which to find references. - private readonly string MethodName; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name for which to find references. - /// The method name for which to find references. - /// The result to return for matching instructions. - public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) - { - this.FullTypeName = fullTypeName; - this.MethodName = methodName; - this.Result = result; - this.NounPhrase = $"{fullTypeName}.{methodName} method"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.FullTypeName - && methodRef.Name == this.MethodName; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs deleted file mode 100644 index e54c86cf..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds incompatible CIL instructions that reference a given property. - internal class PropertyFinder : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; - - /// The property name for which to find references. - private readonly string PropertyName; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name for which to find references. - /// The property name for which to find references. - /// The result to return for matching instructions. - public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) - { - this.FullTypeName = fullTypeName; - this.PropertyName = propertyName; - this.Result = result; - this.NounPhrase = $"{fullTypeName}.{propertyName} property"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.FullTypeName - && (methodRef.Name == "get_" + this.PropertyName || methodRef.Name == "set_" + this.PropertyName); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs deleted file mode 100644 index 45349def..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Linq; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds incompatible CIL instructions that reference a given type. - internal class TypeFinder : IInstructionHandler - { - /********* - ** Accessors - *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name to match. - /// The result to return for matching instructions. - public TypeFinder(string fullTypeName, InstructionHandleResult result) - { - this.FullTypeName = fullTypeName; - this.Result = result; - this.NounPhrase = $"{fullTypeName} type"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(method) - ? this.Result - : InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The method deifnition. - protected bool IsMatch(MethodDefinition method) - { - if (this.IsMatch(method.ReturnType)) - return true; - - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.IsMatch(variable.VariableType)) - return true; - } - - return false; - } - - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - // field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null) - { - return - this.IsMatch(fieldRef.DeclaringType) // field on target class - || this.IsMatch(fieldRef.FieldType); // field value is target class - } - - // method reference - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null) - { - return - this.IsMatch(methodRef.DeclaringType) // method on target class - || this.IsMatch(methodRef.ReturnType) // method returns target class - || methodRef.Parameters.Any(p => this.IsMatch(p.ParameterType)); // method parameters - } - - return false; - } - - /// Get whether a type reference matches the expected type. - /// The type to check. - protected bool IsMatch(TypeReference type) - { - // root type - if (type.FullName == this.FullTypeName) - return true; - - // generic arguments - if (type is GenericInstanceType genericType) - { - if (genericType.GenericArguments.Any(this.IsMatch)) - return true; - } - - // generic parameters (e.g. constraints) - if (type.GenericParameters.Any(this.IsMatch)) - return true; - - return false; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs deleted file mode 100644 index 8830cc74..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Performs predefined logic for detected CIL instructions. - internal interface IInstructionHandler - { - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the handler matches. - string NounPhrase { get; } - - - /********* - ** Methods - *********/ - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs b/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs deleted file mode 100644 index 17ec24b1..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// An exception raised when an incompatible instruction is found while loading a mod assembly. - internal class IncompatibleInstructionException : Exception - { - /********* - ** Accessors - *********/ - /// A brief noun phrase which describes the incompatible instruction that was found. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A brief noun phrase which describes the incompatible instruction that was found. - public IncompatibleInstructionException(string nounPhrase) - : base($"Found an incompatible CIL instruction ({nounPhrase}).") - { - this.NounPhrase = nounPhrase; - } - - /// Construct an instance. - /// A brief noun phrase which describes the incompatible instruction that was found. - /// A message which describes the error. - public IncompatibleInstructionException(string nounPhrase, string message) - : base(message) - { - this.NounPhrase = nounPhrase; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs deleted file mode 100644 index 0ae598fc..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Indicates how an instruction was handled. - internal enum InstructionHandleResult - { - /// No special handling is needed. - None, - - /// The instruction was successfully rewritten for compatibility. - Rewritten, - - /// The instruction is not compatible and can't be rewritten for compatibility. - NotCompatible, - - /// The instruction is compatible, but patches the game in a way that may impact stability. - DetectedGamePatch, - - /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. - DetectedSaveSerialiser, - - /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. - DetectedDynamic - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs deleted file mode 100644 index 075e237a..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright. - internal class InvalidModStateException : Exception - { - /// Construct an instance. - /// The error message. - /// The underlying exception, if any. - public InvalidModStateException(string message, Exception ex = null) - : base(message, ex) { } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs deleted file mode 100644 index 0774b487..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// The status of a given mod in the dependency-sorting algorithm. - internal enum ModDependencyStatus - { - /// The mod hasn't been visited yet. - Queued, - - /// The mod is currently being analysed as part of a dependency chain. - Checking, - - /// The mod has already been sorted. - Sorted, - - /// The mod couldn't be sorted due to a metadata issue (e.g. missing dependencies). - Failed - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs deleted file mode 100644 index 5055da75..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ /dev/null @@ -1,68 +0,0 @@ -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Metadata for a mod. - internal class ModMetadata : IModMetadata - { - /********* - ** Accessors - *********/ - /// The mod's display name. - public string DisplayName { get; } - - /// The mod's full directory path. - public string DirectoryPath { get; } - - /// The mod manifest. - public IManifest Manifest { get; } - - /// Metadata about the mod from SMAPI's internal data (if any). - public ModDataRecord DataRecord { get; } - - /// The metadata resolution status. - public ModMetadataStatus Status { get; private set; } - - /// The reason the metadata is invalid, if any. - public string Error { get; private set; } - - /// The mod instance (if it was loaded). - public IMod Mod { get; private set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's display name. - /// The mod's full directory path. - /// The mod manifest. - /// Metadata about the mod from SMAPI's internal data (if any). - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) - { - this.DisplayName = displayName; - this.DirectoryPath = directoryPath; - this.Manifest = manifest; - this.DataRecord = dataRecord; - } - - /// Set the mod status. - /// The metadata resolution status. - /// The reason the metadata is invalid, if any. - /// Return the instance for chaining. - public IModMetadata SetStatus(ModMetadataStatus status, string error = null) - { - this.Status = status; - this.Error = error; - return this; - } - - /// Set the mod instance. - /// The mod instance to set. - public IModMetadata SetMod(IMod mod) - { - this.Mod = mod; - return this; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs deleted file mode 100644 index ab65f7b4..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Indicates the status of a mod's metadata resolution. - internal enum ModMetadataStatus - { - /// The mod has been found, but hasn't been processed yet. - Found, - - /// The mod cannot be loaded. - Failed - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs deleted file mode 100644 index d0ef1b08..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ /dev/null @@ -1,366 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Finds and processes mod metadata. - internal class ModResolver - { - /********* - ** Public methods - *********/ - /// Get manifest metadata for each folder in the given root path. - /// The root path to search for mods. - /// The JSON helper with which to read manifests. - /// Metadata about mods from SMAPI's internal data. - /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable dataRecords) - { - dataRecords = dataRecords.ToArray(); - - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) - { - // read file - Manifest manifest = null; - string path = Path.Combine(modDir.FullName, "manifest.json"); - string error = null; - try - { - // read manifest - manifest = jsonHelper.ReadJsonFile(path); - - // validate - if (manifest == null) - { - error = File.Exists(path) - ? "its manifest is invalid." - : "it doesn't have a manifest."; - } - else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - error = "its manifest doesn't set an entry DLL."; - } - catch (SParseException ex) - { - error = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; - } - - // get internal data record (if any) - ModDataRecord dataRecord = null; - if (manifest != null) - { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); - } - - // add default update keys - if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) - manifest.UpdateKeys = dataRecord.UpdateKeys; - - // build metadata - string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) - ? manifest.Name - : modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); - ModMetadataStatus status = error == null - ? ModMetadataStatus.Found - : ModMetadataStatus.Failed; - - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); - } - } - - /// Validate manifest metadata. - /// The mod manifests to validate. - /// The current SMAPI version. - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). - public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, IDictionary vendorModUrls) - { - mods = mods.ToArray(); - - // validate each manifest - foreach (IModMetadata mod in mods) - { - // skip if already failed - if (mod.Status == ModMetadataStatus.Failed) - continue; - - // validate compatibility - ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); - switch (compatibility?.Status) - { - case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); - continue; - - case ModStatus.AssumeBroken: - { - // get reason - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; - - // get update URLs - List updateUrls = new List(); - foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) - { - string[] parts = key.Split(new[] { ':' }, 2); - if (parts.Length != 2) - continue; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - updateUrls.Add(string.Format(urlTemplate, modID)); - } - if (mod.DataRecord.AlternativeUrl != null) - updateUrls.Add(mod.DataRecord.AlternativeUrl); - - // build error - string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) - error += "newer version"; - else - error += $"version newer than {compatibility.UpperVersion}"; - error += " at " + string.Join(" or ", updateUrls); - - mod.SetStatus(ModMetadataStatus.Failed, error); - } - continue; - } - - // validate SMAPI version - if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); - continue; - } - - // validate DLL path - string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; - } - - // validate required fields - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); - } - } - - // validate IDs are unique - { - var duplicatesByID = mods - .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) - .Where(p => p.Count() > 1); - foreach (var group in duplicatesByID) - { - foreach (IModMetadata mod in group) - { - if (mod.Status == ModMetadataStatus.Failed) - continue; // don't replace metadata error - mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); - } - } - } - } - - /// Sort the given mods by the order they should be loaded. - /// The mods to process. - public IEnumerable ProcessDependencies(IEnumerable mods) - { - // initialise metadata - mods = mods.ToArray(); - var sortedMods = new Stack(); - var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); - - // handle failed mods - foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed)) - { - states[mod] = ModDependencyStatus.Failed; - sortedMods.Push(mod); - } - - // sort mods - foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); - - return sortedMods.Reverse(); - } - - - /********* - ** Private methods - *********/ - /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. - /// The full list of mods being validated. - /// The mod whose dependencies to process. - /// The dependency state for each mod. - /// The list in which to save mods sorted by dependency order. - /// The current change of mod dependencies. - /// Returns the mod dependency status. - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) - { - // check if already visited - switch (states[mod]) - { - // already sorted or failed - case ModDependencyStatus.Sorted: - case ModDependencyStatus.Failed: - return states[mod]; - - // dependency loop - case ModDependencyStatus.Checking: - // This should never happen. The higher-level mod checks if the dependency is - // already being checked, so it can fail without visiting a mod twice. If this - // case is hit, that logic didn't catch the dependency loop for some reason. - throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName}))."); - - // not visited yet, start processing - case ModDependencyStatus.Queued: - break; - - // sanity check - default: - throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); - } - - // no dependencies, mark sorted - if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) - { - sortedMods.Push(mod); - return states[mod] = ModDependencyStatus.Sorted; - } - - // get dependencies - var dependencies = - ( - from entry in mod.Manifest.Dependencies - let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - orderby entry.UniqueID - select new - { - ID = entry.UniqueID, - MinVersion = entry.MinimumVersion, - Mod = dependencyMod, - IsRequired = entry.IsRequired - } - ) - .ToArray(); - - // missing required dependencies, mark failed - { - string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); - if (failedIDs.Any()) - { - sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); - return states[mod] = ModDependencyStatus.Failed; - } - } - - // dependency min version not met, mark failed - { - string[] failedLabels = - ( - from entry in dependencies - where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) - select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" - ) - .ToArray(); - if (failedLabels.Any()) - { - sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); - return states[mod] = ModDependencyStatus.Failed; - } - } - - // process dependencies - { - states[mod] = ModDependencyStatus.Checking; - - // recursively sort dependencies - foreach (var dependency in dependencies) - { - IModMetadata requiredMod = dependency.Mod; - var subchain = new List(currentChain) { mod }; - - // ignore missing optional dependency - if (!dependency.IsRequired && requiredMod == null) - continue; - - // detect dependency loop - if (states[requiredMod] == ModDependencyStatus.Checking) - { - sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); - return states[mod] = ModDependencyStatus.Failed; - } - - // recursively process each dependency - var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); - switch (substatus) - { - // sorted successfully - case ModDependencyStatus.Sorted: - break; - - // failed, which means this mod can't be loaded either - case ModDependencyStatus.Failed: - sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); - return states[mod] = ModDependencyStatus.Failed; - - // unexpected status - case ModDependencyStatus.Queued: - case ModDependencyStatus.Checking: - throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status."); - - // sanity check - default: - throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); - } - } - - // all requirements sorted successfully - sortedMods.Push(mod); - return states[mod] = ModDependencyStatus.Sorted; - } - } - - /// Get all mod folders in a root folder, passing through empty folders as needed. - /// The root folder path to search. - private IEnumerable GetModFolders(string rootPath) - { - foreach (string modRootPath in Directory.GetDirectories(rootPath)) - { - DirectoryInfo directory = new DirectoryInfo(modRootPath); - - // if a folder only contains another folder, check the inner folder instead - while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) - directory = directory.GetDirectories().First(); - - yield return directory; - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Platform.cs b/src/StardewModdingAPI/Framework/ModLoading/Platform.cs deleted file mode 100644 index 45e881c4..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Platform.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Framework.ModLoading -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux/Mac version of the game. - Mono, - - /// The Windows version of the game. - Windows - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs deleted file mode 100644 index 463f45e8..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Metadata for mapping assemblies to the current . - internal class PlatformAssemblyMap - { - /********* - ** Accessors - *********/ - /**** - ** Data - ****/ - /// The target game platform. - public readonly Platform TargetPlatform; - - /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). - public readonly string[] RemoveNames; - - /**** - ** Metadata - ****/ - /// The assemblies to target. Equivalent types should be rewritten to use these assemblies. - public readonly Assembly[] Targets; - - /// An assembly => reference cache. - public readonly IDictionary TargetReferences; - - /// An assembly => module cache. - public readonly IDictionary TargetModules; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The target game platform. - /// The assembly short names to remove (like Stardew Valley). - /// The assemblies to target. - public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) - { - // save data - this.TargetPlatform = targetPlatform; - this.RemoveNames = removeAssemblyNames; - - // cache assembly metadata - this.Targets = targetAssemblies; - this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs b/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs deleted file mode 100644 index 56a60a72..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Provides helper methods for field rewriters. - internal static class RewriteHelper - { - /********* - ** Public methods - *********/ - /// Get the field reference from an instruction if it matches. - /// The IL instruction. - public static FieldReference AsFieldReference(Instruction instruction) - { - return instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld - ? (FieldReference)instruction.Operand - : null; - } - - /// Get the method reference from an instruction if it matches. - /// The IL instruction. - public static MethodReference AsMethodReference(Instruction instruction) - { - return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt - ? (MethodReference)instruction.Operand - : null; - } - - /// Get whether a type matches a type reference. - /// The defined type. - /// The type reference. - public static bool IsSameType(Type type, TypeReference reference) - { - // same namespace & name - if (type.Namespace != reference.Namespace || type.Name != reference.Name) - return false; - - // same generic parameters - if (type.IsGenericType) - { - if (!reference.IsGenericInstance) - return false; - - Type[] defGenerics = type.GetGenericArguments(); - TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); - if (defGenerics.Length != refGenerics.Length) - return false; - for (int i = 0; i < defGenerics.Length; i++) - { - if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) - return false; - } - } - - return true; - } - - /// Get whether a method definition matches the signature expected by a method reference. - /// The method definition. - /// The method reference. - public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) - { - // same name - if (definition.Name != reference.Name) - return false; - - // same arguments - ParameterInfo[] definitionParameters = definition.GetParameters(); - ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); - if (referenceParameters.Length != definitionParameters.Length) - return false; - for (int i = 0; i < referenceParameters.Length; i++) - { - if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) - return false; - } - return true; - } - - /// Get whether a type has a method whose signature matches the one expected by a method reference. - /// The type to check. - /// The method reference. - public static bool HasMatchingSignature(Type type, MethodReference reference) - { - return type - .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) - .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs deleted file mode 100644 index 63358b39..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites references to one field with another. - internal class FieldReplaceRewriter : FieldFinder - { - /********* - ** Properties - *********/ - /// The new field to reference. - private readonly FieldInfo ToField; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - /// The new field name to reference. - public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) - : base(type.FullName, fromFieldName, InstructionHandleResult.None) - { - this.ToField = type.GetField(toFieldName); - if (this.ToField == null) - throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - FieldReference newRef = module.Import(this.ToField); - cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); - return InstructionHandleResult.Rewritten; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs deleted file mode 100644 index a20b8bee..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites field references into property references. - internal class FieldToPropertyRewriter : FieldFinder - { - /********* - ** Properties - *********/ - /// The type whose field to which references should be rewritten. - private readonly Type Type; - - /// The field name to rewrite. - private readonly string FieldName; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - public FieldToPropertyRewriter(Type type, string fieldName) - : base(type.FullName, fieldName, InstructionHandleResult.None) - { - this.Type = type; - this.FieldName = fieldName; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.FieldName}")); - cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); - return InstructionHandleResult.Rewritten; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs deleted file mode 100644 index 974fcf4c..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites method references from one parent type to another if the signatures match. - internal class MethodParentRewriter : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The type whose methods to remap. - private readonly Type FromType; - - /// The type with methods to map to. - private readonly Type ToType; - - /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - private readonly bool OnlyIfPlatformChanged; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type whose methods to remap. - /// The type with methods to map to. - /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) - { - this.FromType = fromType; - this.ToType = toType; - this.NounPhrase = $"{fromType.Name} methods"; - this.OnlyIfPlatformChanged = onlyIfPlatformChanged; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction, platformChanged)) - return InstructionHandleResult.None; - - MethodReference methodRef = (MethodReference)instruction.Operand; - methodRef.DeclaringType = module.Import(this.ToType); - return InstructionHandleResult.Rewritten; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - /// Whether the mod was compiled on a different platform. - protected bool IsMatch(Instruction instruction, bool platformChanged) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && (platformChanged || !this.OnlyIfPlatformChanged) - && methodRef.DeclaringType.FullName == this.FromType.FullName - && RewriteHelper.HasMatchingSignature(this.ToType, methodRef); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs deleted file mode 100644 index 74f2fcdd..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites all references to a type. - internal class TypeReferenceRewriter : TypeFinder - { - /********* - ** Properties - *********/ - /// The full type name to which to find references. - private readonly string FromTypeName; - - /// The new type to reference. - private readonly Type ToType; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name to which to find references. - /// The new type to reference. - public TypeReferenceRewriter(string fromTypeFullName, Type toType) - : base(fromTypeFullName, InstructionHandleResult.None) - { - this.FromTypeName = fromTypeFullName; - this.ToType = toType; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - bool rewritten = false; - - // return type - if (this.IsMatch(method.ReturnType)) - { - method.ReturnType = this.RewriteIfNeeded(module, method.ReturnType); - rewritten = true; - } - - // parameters - foreach (ParameterDefinition parameter in method.Parameters) - { - if (this.IsMatch(parameter.ParameterType)) - { - parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType); - rewritten = true; - } - } - - // generic parameters - for (int i = 0; i < method.GenericParameters.Count; i++) - { - var parameter = method.GenericParameters[i]; - if (this.IsMatch(parameter)) - { - TypeReference newType = this.RewriteIfNeeded(module, parameter); - if (newType != parameter) - method.GenericParameters[i] = new GenericParameter(parameter.Name, newType); - rewritten = true; - } - } - - // local variables - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.IsMatch(variable.VariableType)) - { - variable.VariableType = this.RewriteIfNeeded(module, variable.VariableType); - rewritten = true; - } - } - - return rewritten - ? InstructionHandleResult.Rewritten - : InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) - return InstructionHandleResult.None; - - // field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null) - { - fieldRef.DeclaringType = this.RewriteIfNeeded(module, fieldRef.DeclaringType); - fieldRef.FieldType = this.RewriteIfNeeded(module, fieldRef.FieldType); - } - - // method reference - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null) - { - methodRef.DeclaringType = this.RewriteIfNeeded(module, methodRef.DeclaringType); - methodRef.ReturnType = this.RewriteIfNeeded(module, methodRef.ReturnType); - foreach (var parameter in methodRef.Parameters) - parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType); - } - - // type reference - if (instruction.Operand is TypeReference typeRef) - { - TypeReference newRef = this.RewriteIfNeeded(module, typeRef); - if (typeRef != newRef) - cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); - } - - return InstructionHandleResult.Rewritten; - } - - /********* - ** Private methods - *********/ - /// Get the adjusted type reference if it matches, else the same value. - /// The assembly module containing the instruction. - /// The type to replace if it matches. - private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type) - { - // root type - if (type.FullName == this.FromTypeName) - return module.Import(this.ToType); - - // generic arguments - if (type is GenericInstanceType genericType) - { - for (int i = 0; i < genericType.GenericArguments.Count; i++) - genericType.GenericArguments[i] = this.RewriteIfNeeded(module, genericType.GenericArguments[i]); - } - - // generic parameters (e.g. constraints) - for (int i = 0; i < type.GenericParameters.Count; i++) - type.GenericParameters[i] = new GenericParameter(this.RewriteIfNeeded(module, type.GenericParameters[i])); - - return type; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs deleted file mode 100644 index 322a7df1..00000000 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites virtual calls to the method. - internal class VirtualEntryCallRemover : IInstructionHandler - { - /********* - ** Properties - *********/ - /// The type containing the method. - private readonly Type ToType; - - /// The name of the method. - private readonly string MethodName; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public VirtualEntryCallRemover() - { - this.ToType = typeof(Mod); - this.MethodName = nameof(Mod.Entry); - this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - // get instructions comprising method call - int index = cil.Body.Instructions.IndexOf(instruction); - Instruction loadArg0 = cil.Body.Instructions[index - 2]; - Instruction loadArg1 = cil.Body.Instructions[index - 1]; - if (loadArg0.OpCode != OpCodes.Ldarg_0) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); - if (loadArg1.OpCode != OpCodes.Ldarg_1) - throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); - - // remove method call - cil.Remove(loadArg0); - cil.Remove(loadArg1); - cil.Remove(instruction); - return InstructionHandleResult.Rewritten; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - protected bool IsMatch(Instruction instruction) - { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - return - methodRef != null - && methodRef.DeclaringType.FullName == this.ToType.FullName - && methodRef.Name == this.MethodName; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs deleted file mode 100644 index 9dde7a20..00000000 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; - -namespace StardewModdingAPI.Framework -{ - /// Tracks the installed mods. - internal class ModRegistry - { - /********* - ** Properties - *********/ - /// The registered mod data. - private readonly List Mods = new List(); - - /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). - private readonly IDictionary ModNamesByAssembly = new Dictionary(); - - - /********* - ** Public methods - *********/ - /**** - ** Basic metadata - ****/ - /// Get metadata for all loaded mods. - public IEnumerable GetAll() - { - return this.Mods.Select(p => p.Manifest); - } - - /// Get metadata for a loaded mod. - /// The mod's unique ID. - /// Returns the matching mod's metadata, or null if not found. - public IManifest Get(string uniqueID) - { - // normalise search ID - if (string.IsNullOrWhiteSpace(uniqueID)) - return null; - uniqueID = uniqueID.Trim(); - - // find match - return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); - } - - /// Get whether a mod has been loaded. - /// The mod's unique ID. - public bool IsLoaded(string uniqueID) - { - return this.Get(uniqueID) != null; - } - - /**** - ** Mod data - ****/ - /// Register a mod as a possible source of deprecation warnings. - /// The mod metadata. - public void Add(IModMetadata metadata) - { - this.Mods.Add(metadata); - this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName; - } - - /// Get all enabled mods. - public IEnumerable GetMods() - { - return (from mod in this.Mods select mod); - } - - /// Get the friendly mod name which defines a type. - /// The type to check. - /// Returns the mod name, or null if the type isn't part of a known mod. - public string GetModFrom(Type type) - { - // null - if (type == null) - return null; - - // known type - string assemblyName = type.Assembly.FullName; - if (this.ModNamesByAssembly.ContainsKey(assemblyName)) - return this.ModNamesByAssembly[assemblyName]; - - // not found - return null; - } - - /// Get the friendly name for the closest assembly registered as a source of deprecation warnings. - /// Returns the source name, or null if no registered assemblies were found. - public string GetModFromStack() - { - // get stack frames - StackTrace stack = new StackTrace(); - StackFrame[] frames = stack.GetFrames(); - if (frames == null) - return null; - - // search stack for a source assembly - foreach (StackFrame frame in frames) - { - MethodBase method = frame.GetMethod(); - string name = this.GetModFrom(method.ReflectedType); - if (name != null) - return name; - } - - // no known assembly found - return null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs deleted file mode 100644 index b85787e5..00000000 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework.Models -{ - /// A manifest which describes a mod for SMAPI. - internal class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// A brief description of the mod. - public string Description { get; set; } - - /// The mod author's name. - public string Author { get; set; } - - /// The mod version. - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion Version { get; set; } - - /// The minimum SMAPI version required by this mod, if any. - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion MinimumApiVersion { get; set; } - - /// The name of the DLL in the directory that has the method. - public string EntryDll { get; set; } - - /// The other mods that must be loaded before this mod. - [JsonConverter(typeof(SFieldConverter))] - public IManifestDependency[] Dependencies { get; set; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - public string[] UpdateKeys { get; set; } - - /// The unique mod ID. - public string UniqueID { get; set; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs deleted file mode 100644 index 5646b335..00000000 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// A mod dependency listed in a mod manifest. - internal class ManifestDependency : IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - public string UniqueID { get; set; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } - - /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; set; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique mod ID to require. - /// The minimum required version (if any). - /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) - { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; - this.IsRequired = required; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs deleted file mode 100644 index 54737e6c..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Models -{ - /// Specifies the compatibility of a given mod version range. - internal class ModCompatibility - { - /********* - ** Accessors - *********/ - /// The lowest version in the range, or null for all past versions. - public ISemanticVersion LowerVersion { get; } - - /// The highest version in the range, or null for all future versions. - public ISemanticVersion UpperVersion { get; } - - /// The mod compatibility. - public ModStatus Status { get; } - - /// The reason phrase to show in log output, or null to use the default value. - /// For example, "this version is incompatible with the latest version of the game". - public string ReasonPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range. - /// The mod compatibility. - /// The reason phrase to show in log output, or null to use the default value. - public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) - { - // extract version strings - string[] versions = versionRange.Split('~'); - if (versions.Length != 2) - throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); - - // initialise - this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; - this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; - this.Status = status; - this.ReasonPhrase = reasonPhrase; - } - - /// Get whether a given version is contained within this compatibility range. - /// The version to check. - public bool MatchesVersion(ISemanticVersion version) - { - return - (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModDataID.cs b/src/StardewModdingAPI/Framework/Models/ModDataID.cs deleted file mode 100644 index d19434fa..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModDataID.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// Uniquely identifies a mod in SMAPI's internal data. - /// - /// This represents a custom format which uniquely identifies a mod across all versions, even - /// if its field values change or it doesn't specify a unique ID. This is mapped to a string - /// with the following format: - /// - /// 1. If the mod's identifier changed over time, multiple variants can be separated by the | - /// character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. - /// - internal class ModDataID - { - /********* - ** Properties - *********/ - /// The unique sets of field values which identify this mod. - private readonly FieldSnapshot[] Snapshots; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ModDataID() { } - - /// Construct an instance. - /// The mod identifier string (see remarks on ). - public ModDataID(string data) - { - this.Snapshots = - ( - from string part in data.Split('|') - let str = part.Trim() - select str.StartsWith("{") - ? JsonConvert.DeserializeObject(str) - : new FieldSnapshot { ID = str } - ) - .ToArray(); - } - - /// Get whether this ID matches a given mod manifest. - /// The mod's unique ID, or a substitute ID if it isn't set in the manifest. - /// The manifest to check. - public bool Matches(string id, IManifest manifest) - { - return this.Snapshots.Any(snapshot => - snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) - && ( - snapshot.Author == null - || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) - ); - } - - - /********* - ** Private models - *********/ - /// A unique set of fields which identifies the mod. - private class FieldSnapshot - { - /********* - ** Accessors - *********/ - /// The unique mod ID. - public string ID { get; set; } - - /// The mod name, or null to ignore the mod name. - public string Name { get; set; } - - /// The author name, or null to ignore the author. - public string Author { get; set; } - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs deleted file mode 100644 index c6a12188..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework.Models -{ - /// Metadata about a mod from SMAPI's internal data. - internal class ModDataRecord - { - /********* - ** Accessors - *********/ - /// The unique mod identifier. - [JsonConverter(typeof(SFieldConverter))] - public ModDataID ID { get; set; } - - /// A value to inject into field if it's not already set. - public string[] UpdateKeys { get; set; } - - /// The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible. - public string AlternativeUrl { get; set; } - - /// The compatibility of given mod versions (if any). - [JsonConverter(typeof(SFieldConverter))] - public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; - - /// Map local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Map remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Get the compatibility record for a given version, if any. - /// The mod version to check. - public ModCompatibility GetCompatibility(ISemanticVersion version) - { - return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); - } - - /// Get a semantic local version for update checks. - /// The local version to normalise. - public string GetLocalVersionForUpdateChecks(string version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - public string GetRemoteVersionForUpdateChecks(string version) - { - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModStatus.cs b/src/StardewModdingAPI/Framework/Models/ModStatus.cs deleted file mode 100644 index 343ccb7e..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Indicates how SMAPI should treat a mod. - internal enum ModStatus - { - /// Don't override the status. - None, - - /// The mod is obsolete and shouldn't be used, regardless of version. - Obsolete, - - /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken, - - /// Assume the mod is compatible, even if SMAPI detects incompatible code. - AssumeCompatible - } -} diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs deleted file mode 100644 index 401e1a3a..00000000 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// The SMAPI configuration settings. - internal class SConfig - { - /******** - ** Accessors - ********/ - /// Whether to enable development features. - public bool DeveloperMode { get; set; } - - /// Whether to check for newer versions of SMAPI and mods on startup. - public bool CheckForUpdates { get; set; } - - /// SMAPI's GitHub project name, used to perform update checks. - public string GitHubProjectName { get; set; } - - /// The base URL for SMAPI's web API, used to perform update checks. - public string WebApiBaseUrl { get; set; } - - /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } - - /// Extra metadata about mods. - public ModDataRecord[] ModData { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs deleted file mode 100644 index bf338386..00000000 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using StardewModdingAPI.Framework.Logging; - -namespace StardewModdingAPI.Framework -{ - /// Encapsulates monitoring and logic for a given module. - internal class Monitor : IMonitor - { - /********* - ** Properties - *********/ - /// The name of the module which logs messages using this instance. - private readonly string Source; - - /// Manages access to the console output. - private readonly ConsoleInterceptionManager ConsoleManager; - - /// The log file to which to write messages. - private readonly LogFileManager LogFile; - - /// The maximum length of the values. - private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); - - /// The console text color for each log level. - private static readonly IDictionary Colors = Monitor.GetConsoleColorScheme(); - - /// Propagates notification that SMAPI should exit. - private readonly CancellationTokenSource ExitTokenSource; - - - /********* - ** Accessors - *********/ - /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. - public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; - - /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. - internal bool ShowFullStampInConsole { get; set; } - - /// Whether to show trace messages in the console. - internal bool ShowTraceInConsole { get; set; } - - /// Whether to write anything to the console. This should be disabled if no console is available. - internal bool WriteToConsole { get; set; } = true; - - /// Whether to write anything to the log file. This should almost always be enabled. - internal bool WriteToFile { get; set; } = true; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The name of the module which logs messages using this instance. - /// Manages access to the console output. - /// The log file to which to write messages. - /// Propagates notification that SMAPI should exit. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource) - { - // validate - if (string.IsNullOrWhiteSpace(source)) - throw new ArgumentException("The log source cannot be empty."); - - // initialise - this.Source = source; - this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); - this.ConsoleManager = consoleManager; - this.ExitTokenSource = exitTokenSource; - } - - /// Log a message for the player or developer. - /// The message to log. - /// The log severity level. - public void Log(string message, LogLevel level = LogLevel.Debug) - { - this.LogImpl(this.Source, message, level, Monitor.Colors[level]); - } - - /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. - /// The reason for the shutdown. - public void ExitGameImmediately(string reason) - { - this.LogFatal($"{this.Source} requested an immediate game shutdown: {reason}"); - this.ExitTokenSource.Cancel(); - } - - /// Write a newline to the console and log file. - internal void Newline() - { - if (this.WriteToConsole) - this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); - if (this.WriteToFile) - this.LogFile.WriteLine(""); - } - - - /********* - ** Private methods - *********/ - /// Log a fatal error message. - /// The message to log. - private void LogFatal(string message) - { - this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); - } - - /// Write a message line to the log. - /// The name of the mod logging the message. - /// The message to log. - /// The log level. - /// The console foreground color. - /// The console background color (or null to leave it as-is). - private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) - { - // generate message - string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); - - string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; - string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; - - // write to console - if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) - { - this.ConsoleManager.ExclusiveWriteWithoutInterception(() => - { - if (this.ConsoleManager.SupportsColor) - { - if (background.HasValue) - Console.BackgroundColor = background.Value; - Console.ForegroundColor = color; - Console.WriteLine(consoleMessage); - Console.ResetColor(); - } - else - Console.WriteLine(consoleMessage); - }); - } - - // write to log file - if (this.WriteToFile) - this.LogFile.WriteLine(fullMessage); - } - - /// Get the color scheme to use for the current console. - private static IDictionary GetConsoleColorScheme() - { - // scheme for dark console background - if (Monitor.IsDark(Console.BackgroundColor)) - { - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; - } - - // scheme for light console background - return new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.Black, - [LogLevel.Warn] = ConsoleColor.DarkYellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.DarkMagenta - }; - } - - /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. - /// The color to check. - private static bool IsDark(ConsoleColor color) - { - switch (color) - { - case ConsoleColor.Black: - case ConsoleColor.Blue: - case ConsoleColor.DarkBlue: - case ConsoleColor.DarkRed: - case ConsoleColor.Red: - return true; - - default: - return false; - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs b/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs deleted file mode 100644 index 30faca37..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// A cached member reflection result. - internal struct CacheEntry - { - /********* - ** Accessors - *********/ - /// Whether the lookup found a valid match. - public bool IsValid; - - /// The reflection data for this member (or null if invalid). - public MemberInfo MemberInfo; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Whether the lookup found a valid match. - /// The reflection data for this member (or null if invalid). - public CacheEntry(bool isValid, MemberInfo memberInfo) - { - this.IsValid = isValid; - this.MemberInfo = memberInfo; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs deleted file mode 100644 index 0bf45969..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Reflection; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// A private field obtained through reflection. - /// The field value type. - internal class PrivateField : IPrivateField - { - /********* - ** Properties - *********/ - /// The type that has the field. - private readonly Type ParentType; - - /// The object that has the instance field (if applicable). - private readonly object Parent; - - /// The display name shown in error messages. - private string DisplayName => $"{this.ParentType.FullName}::{this.FieldInfo.Name}"; - - - /********* - ** Accessors - *********/ - /// The reflection metadata. - public FieldInfo FieldInfo { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type that has the field. - /// The object that has the instance field (if applicable). - /// The reflection metadata. - /// Whether the field is static. - /// The or is null. - /// The is null for a non-static field, or not null for a static field. - public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic) - { - // validate - if (parentType == null) - throw new ArgumentNullException(nameof(parentType)); - if (field == null) - throw new ArgumentNullException(nameof(field)); - if (isStatic && obj != null) - throw new ArgumentException("A static field cannot have an object instance."); - if (!isStatic && obj == null) - throw new ArgumentException("A non-static field must have an object instance."); - - // save - this.ParentType = parentType; - this.Parent = obj; - this.FieldInfo = field; - } - - /// Get the field value. - public TValue GetValue() - { - try - { - return (TValue)this.FieldInfo.GetValue(this.Parent); - } - catch (InvalidCastException) - { - throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); - } - catch (Exception ex) - { - throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex); - } - } - - /// Set the field value. - //// The value to set. - public void SetValue(TValue value) - { - try - { - this.FieldInfo.SetValue(this.Parent, value); - } - catch (InvalidCastException) - { - throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); - } - catch (Exception ex) - { - throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex); - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs deleted file mode 100644 index ba2374f4..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Reflection; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// A private method obtained through reflection. - internal class PrivateMethod : IPrivateMethod - { - /********* - ** Properties - *********/ - /// The type that has the method. - private readonly Type ParentType; - - /// The object that has the instance method (if applicable). - private readonly object Parent; - - /// The display name shown in error messages. - private string DisplayName => $"{this.ParentType.FullName}::{this.MethodInfo.Name}"; - - - /********* - ** Accessors - *********/ - /// The reflection metadata. - public MethodInfo MethodInfo { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type that has the method. - /// The object that has the instance method(if applicable). - /// The reflection metadata. - /// Whether the field is static. - /// The or is null. - /// The is null for a non-static method, or not null for a static method. - public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic) - { - // validate - if (parentType == null) - throw new ArgumentNullException(nameof(parentType)); - if (method == null) - throw new ArgumentNullException(nameof(method)); - if (isStatic && obj != null) - throw new ArgumentException("A static method cannot have an object instance."); - if (!isStatic && obj == null) - throw new ArgumentException("A non-static method must have an object instance."); - - // save - this.ParentType = parentType; - this.Parent = obj; - this.MethodInfo = method; - } - - /// Invoke the method. - /// The return type. - /// The method arguments to pass in. - public TValue Invoke(params object[] arguments) - { - // invoke method - object result; - try - { - result = this.MethodInfo.Invoke(this.Parent, arguments); - } - catch (Exception ex) - { - throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); - } - - // cast return value - try - { - return (TValue)result; - } - catch (InvalidCastException) - { - throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); - } - } - - /// Invoke the method. - /// The method arguments to pass in. - public void Invoke(params object[] arguments) - { - // invoke method - try - { - this.MethodInfo.Invoke(this.Parent, arguments); - } - catch (Exception ex) - { - throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); - } - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs deleted file mode 100644 index 08204b7e..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Reflection; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// A private property obtained through reflection. - /// The property value type. - internal class PrivateProperty : IPrivateProperty - { - /********* - ** Properties - *********/ - /// The type that has the field. - private readonly Type ParentType; - - /// The object that has the instance field (if applicable). - private readonly object Parent; - - /// The display name shown in error messages. - private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}"; - - - /********* - ** Accessors - *********/ - /// The reflection metadata. - public PropertyInfo PropertyInfo { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type that has the field. - /// The object that has the instance field (if applicable). - /// The reflection metadata. - /// Whether the field is static. - /// The or is null. - /// The is null for a non-static field, or not null for a static field. - public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) - { - // validate - if (parentType == null) - throw new ArgumentNullException(nameof(parentType)); - if (property == null) - throw new ArgumentNullException(nameof(property)); - if (isStatic && obj != null) - throw new ArgumentException("A static property cannot have an object instance."); - if (!isStatic && obj == null) - throw new ArgumentException("A non-static property must have an object instance."); - - // save - this.ParentType = parentType; - this.Parent = obj; - this.PropertyInfo = property; - } - - /// Get the property value. - public TValue GetValue() - { - try - { - return (TValue)this.PropertyInfo.GetValue(this.Parent); - } - catch (InvalidCastException) - { - throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); - } - catch (Exception ex) - { - throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex); - } - } - - /// Set the property value. - //// The value to set. - public void SetValue(TValue value) - { - try - { - this.PropertyInfo.SetValue(this.Parent, value); - } - catch (InvalidCastException) - { - throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); - } - catch (Exception ex) - { - throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex); - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs deleted file mode 100644 index 5c2d90fa..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.Caching; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// Provides helper methods for accessing private game code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). - internal class Reflector - { - /********* - ** Properties - *********/ - /// The cached fields and methods found via reflection. - private readonly MemoryCache Cache = new MemoryCache(typeof(Reflector).FullName); - - /// The sliding cache expiration time. - private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); - - - /********* - ** Public methods - *********/ - /**** - ** Fields - ****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field wrapper, or null if the field doesn't exist and is false. - public IPrivateField GetPrivateField(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); - - // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && field == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); - return field; - } - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) - { - // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && field == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); - return field; - } - - /**** - ** Properties - ****/ - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); - - // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && property == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); - return property; - } - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) - { - // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && property == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); - return property; - } - - /**** - ** Methods - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); - - // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); - return method; - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) - { - // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); - return method; - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - // validate parent - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); - - // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); - if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); - return method; - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); - if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); - return method; - } - - - /********* - ** Private methods - *********/ - /// Get a field from the type hierarchy. - /// The expected field type. - /// The type which has the field. - /// The object which has the field. - /// The field name. - /// The reflection binding which flags which indicates what type of field to find. - private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => - { - FieldInfo fieldInfo = null; - for (; type != null && fieldInfo == null; type = type.BaseType) - fieldInfo = type.GetField(name, bindingFlags); - return fieldInfo; - }); - - return field != null - ? new PrivateField(type, obj, field, isStatic) - : null; - } - - /// Get a property from the type hierarchy. - /// The expected property type. - /// The type which has the property. - /// The object which has the property. - /// The property name. - /// The reflection binding which flags which indicates what type of property to find. - private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => - { - PropertyInfo propertyInfo = null; - for (; type != null && propertyInfo == null; type = type.BaseType) - propertyInfo = type.GetProperty(name, bindingFlags); - return propertyInfo; - }); - - return property != null - ? new PrivateProperty(type, obj, property, isStatic) - : null; - } - - /// Get a method from the type hierarchy. - /// The type which has the method. - /// The object which has the method. - /// The method name. - /// The reflection binding which flags which indicates what type of method to find. - private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => - { - MethodInfo methodInfo = null; - for (; type != null && methodInfo == null; type = type.BaseType) - methodInfo = type.GetMethod(name, bindingFlags); - return methodInfo; - }); - - return method != null - ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) - : null; - } - - /// Get a method from the type hierarchy. - /// The type which has the method. - /// The object which has the method. - /// The method name. - /// The reflection binding which flags which indicates what type of method to find. - /// The argument types of the method signature to find. - private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => - { - MethodInfo methodInfo = null; - for (; type != null && methodInfo == null; type = type.BaseType) - methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); - return methodInfo; - }); - return method != null - ? new PrivateMethod(type, obj, method, isStatic) - : null; - } - - /// Get a method or field through the cache. - /// The expected type. - /// The cache key. - /// Fetches a new value to cache. - private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo - { - // get from cache - if (this.Cache.Contains(key)) - { - CacheEntry entry = (CacheEntry)this.Cache[key]; - return entry.IsValid - ? (TMemberInfo)entry.MemberInfo - : default(TMemberInfo); - } - - // fetch & cache new value - TMemberInfo result = fetch(); - CacheEntry cacheEntry = new CacheEntry(result != null, result); - this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); - return result; - } - } -} diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs deleted file mode 100644 index 12d0ea0c..00000000 --- a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StardewModdingAPI.Framework -{ - /// A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. - /// The module which requested an immediate exit. - /// The reason provided for the shutdown. - internal delegate void RequestExitDelegate(string module, string reason); -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs deleted file mode 100644 index 43de6e96..00000000 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ /dev/null @@ -1,531 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Metadata; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// SMAPI's implementation of the game's content manager which lets it raise content events. - internal class SContentManager : LocalizedContentManager - { - /********* - ** Properties - *********/ - /// The possible directory separator characters in an asset key. - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - - /// The preferred directory separator chaeacter in an asset key. - private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// The underlying content manager's asset cache. - private readonly IDictionary Cache; - - /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. - private readonly Func NormaliseAssetNameForPlatform; - - /// The private method which generates the locale portion of an asset name. - private readonly IPrivateMethod GetKeyLocale; - - /// The language codes used in asset keys. - private readonly IDictionary KeyLocales; - - /// Provides metadata for core game assets. - private readonly CoreAssets CoreAssets; - - /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. - private readonly ContextHash AssetsBeingLoaded = new ContextHash(); - - /// A lookup of the content managers which loaded each asset. - private readonly IDictionary> AssetLoaders = new Dictionary>(); - - - /********* - ** Accessors - *********/ - /// Interceptors which provide the initial versions of matching assets. - internal IDictionary> Loaders { get; } = new Dictionary>(); - - /// Interceptors which edit matching assets after they're loaded. - internal IDictionary> Editors { get; } = new Dictionary>(); - - /// The absolute path to the . - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// The current culture for which to localise content. - /// The current language code for which to localise content. - /// Encapsulates monitoring and logging. - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) - : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) - { - // validate - if (monitor == null) - throw new ArgumentNullException(nameof(monitor)); - - // initialise - var reflection = new Reflector(); - this.Monitor = monitor; - - // get underlying fields for interception - this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); - this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); - - // get asset key normalisation logic - if (Constants.TargetPlatform == Platform.Windows) - { - IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); - this.NormaliseAssetNameForPlatform = path => method.Invoke(path); - } - else - this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic - - // get asset data - this.CoreAssets = new CoreAssets(this.NormaliseAssetName); - this.KeyLocales = this.GetKeyLocales(reflection); - } - - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. - public string NormalisePathSeparators(string path) - { - string[] parts = path.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - string normalised = string.Join(SContentManager.PreferredPathSeparator, parts); - if (path.StartsWith(SContentManager.PreferredPathSeparator)) - normalised = SContentManager.PreferredPathSeparator + normalised; // keep root slash - return normalised; - } - - /// Normalise an asset name so it's consistent with the underlying cache. - /// The asset key. - public string NormaliseAssetName(string assetName) - { - assetName = this.NormalisePathSeparators(assetName); - if (assetName.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)) - return assetName.Substring(0, assetName.Length - 4); - return this.NormaliseAssetNameForPlatform(assetName); - } - - /// Get whether the content manager has already loaded and cached the given asset. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool IsLoaded(string assetName) - { - lock (this.Cache) - { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); - } - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - public override T Load(string assetName) - { - return this.LoadFor(assetName, this); - } - - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The content manager instance for which to load the asset. - public T LoadFor(string assetName, ContentManager instance) - { - lock (this.Cache) - { - assetName = this.NormaliseAssetName(assetName); - - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - { - this.TrackAssetLoader(assetName, instance); - return base.Load(assetName); - } - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load(assetName); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => - { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } - - // update cache & return data - this.Cache[assetName] = data; - this.TrackAssetLoader(assetName, instance); - return data; - } - } - - /// Inject an asset into the cache. - /// The type of asset to inject. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The asset value. - public void Inject(string assetName, T value) - { - lock (this.Cache) - { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - this.TrackAssetLoader(assetName, this); - } - } - - /// Get the current content locale. - public string GetLocale() - { - return this.GetKeyLocale.Invoke(); - } - - /// Get the cached asset keys. - public IEnumerable GetAssetKeys() - { - lock (this.Cache) - { - IEnumerable GetAllAssetKeys() - { - foreach (string cacheKey in this.Cache.Keys) - { - this.ParseCacheKey(cacheKey, out string assetKey, out string _); - yield return assetKey; - } - } - - return GetAllAssetKeys().Distinct(); - } - } - - /// Purge assets from the cache that match one of the interceptors. - /// The asset editors for which to purge matching assets. - /// The asset loaders for which to purge matching assets. - /// Returns whether any cache entries were invalidated. - public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) - { - if (!editors.Any() && !loaders.Any()) - return false; - - // get CanEdit/Load methods - MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); - MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); - - // invalidate matching keys - return this.InvalidateCache((assetName, assetType) => - { - // get asset metadata - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); - - // check loaders - MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(assetType); - if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { info }))) - return true; - - // check editors - MethodInfo canEditGeneric = canEdit.MakeGenericMethod(assetType); - return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { info })); - }); - } - - /// Purge matched assets from the cache. - /// Matches the asset keys to invalidate. - /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns whether any cache entries were invalidated. - public bool InvalidateCache(Func predicate, bool dispose = false) - { - lock (this.Cache) - { - // find matching asset keys - HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string cacheKey in this.Cache.Keys) - { - this.ParseCacheKey(cacheKey, out string assetKey, out _); - Type type = this.Cache[cacheKey].GetType(); - if (predicate(assetKey, type)) - { - purgeAssetKeys.Add(assetKey); - purgeCacheKeys.Add(cacheKey); - } - } - - // purge assets - foreach (string key in purgeCacheKeys) - { - if (dispose && this.Cache[key] is IDisposable disposable) - disposable.Dispose(); - this.Cache.Remove(key); - this.AssetLoaders.Remove(key); - } - - // reload core game assets - int reloaded = 0; - foreach (string key in purgeAssetKeys) - { - if (this.CoreAssets.ReloadForKey(this, key)) - reloaded++; - } - - // report result - if (purgeCacheKeys.Any()) - { - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); - return true; - } - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return false; - } - } - - /// Dispose assets for the given content manager shim. - /// The content manager whose assets to dispose. - internal void DisposeFor(ContentManagerShim shim) - { - this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - - foreach (var entry in this.AssetLoaders) - entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.AssetLoaders[key].Any(), dispose: true); - } - - - /********* - ** Private methods - *********/ - /// Get whether an asset has already been loaded. - /// The normalised asset name. - private bool IsNormalisedKeyLoaded(string normalisedAssetName) - { - return this.Cache.ContainsKey(normalisedAssetName) - || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset - } - - /// Track that a content manager loaded an asset. - /// The asset key that was loaded. - /// The content manager that loaded the asset. - private void TrackAssetLoader(string key, ContentManager manager) - { - if (!this.AssetLoaders.TryGetValue(key, out HashSet hash)) - hash = this.AssetLoaders[key] = new HashSet(); - hash.Add(manager); - } - - /// Get the locale codes (like ja-JP) used in asset keys. - /// Simplifies access to private game code. - private IDictionary GetKeyLocales(Reflector reflection) - { - // get the private code field directly to avoid changed-code logic - IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); - - // remember previous settings - LanguageCode previousCode = codeField.GetValue(); - string previousOverride = this.LanguageCodeOverride; - - // create locale => code map - IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.LanguageCodeOverride = null; - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - { - codeField.SetValue(code); - map[this.GetKeyLocale.Invoke()] = code; - } - - // restore previous settings - codeField.SetValue(previousCode); - this.LanguageCodeOverride = previousOverride; - - return map; - } - - /// Parse a cache key into its component parts. - /// The input cache key. - /// The original asset key. - /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) - { - // handle localised key - if (!string.IsNullOrWhiteSpace(cacheKey)) - { - int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); - if (lastSepIndex >= 0) - { - string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.KeyLocales.ContainsKey(suffix)) - { - assetKey = cacheKey.Substring(0, lastSepIndex); - localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - return; - } - } - } - - // handle simple key - assetKey = cacheKey; - localeCode = null; - } - - /// Load the initial asset from the registered . - /// The basic asset metadata. - /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData ApplyLoader(IAssetInfo info) - { - // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) - .Where(entry => - { - try - { - return entry.Value.CanLoad(info); - } - catch (Exception ex) - { - this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return false; - } - }) - .ToArray(); - - // validate loaders - if (!loaders.Any()) - return null; - if (loaders.Length > 1) - { - string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); - this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); - return null; - } - - // fetch asset from loader - IModMetadata mod = loaders[0].Key; - IAssetLoader loader = loaders[0].Value; - T data; - try - { - data = loader.Load(info); - this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); - } - catch (Exception ex) - { - this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - return null; - } - - // validate asset - if (data == null) - { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); - return null; - } - - // return matched asset - return new AssetDataForObject(info, data, this.NormaliseAssetName); - } - - /// Apply any to a loaded asset. - /// The asset type. - /// The basic asset metadata. - /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) - { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); - - // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) - { - // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; - try - { - if (!editor.CanEdit(info)) - continue; - } - catch (Exception ex) - { - this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // try edit - object prevAsset = asset.Data; - try - { - editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - } - catch (Exception ex) - { - this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // validate edit - if (asset.Data == null) - { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - else if (!(asset.Data is T)) - { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); - asset = GetNewData(prevAsset); - } - } - - // return result - return asset; - } - - /// Get all registered interceptors from a list. - private IEnumerable> GetInterceptors(IDictionary> entries) - { - foreach (var entry in entries) - { - IModMetadata metadata = entry.Key; - IList interceptors = entry.Value; - - // special case if mod is an interceptor - if (metadata.Mod is T modAsInterceptor) - yield return new KeyValuePair(metadata, modAsInterceptor); - - // registered editors - foreach (T interceptor in interceptors) - yield return new KeyValuePair(metadata, interceptor); - } - } - - /// Dispose held resources. - /// Whether the content manager is disposing (rather than finalising). - protected override void Dispose(bool disposing) - { - this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - base.Dispose(disposing); - } - } -} diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs deleted file mode 100644 index 7287cab7..00000000 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ /dev/null @@ -1,1403 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Utilities; -using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Locations; -using StardewValley.Menus; -using StardewValley.Tools; -using xTile.Dimensions; -using xTile.Layers; - -namespace StardewModdingAPI.Framework -{ - /// SMAPI's extension of the game's core , used to inject events. - internal class SGame : Game1 - { - /********* - ** Properties - *********/ - /**** - ** SMAPI state - ****/ - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. - private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second - - /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. - private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second - - /// The number of ticks until SMAPI should notify mods that the game has loaded. - /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. - private int AfterLoadTimer = 5; - - /// Whether the game is returning to the menu. - private bool IsExitingToTitle; - - /// Whether the game is saving and SMAPI has already raised . - private bool IsBetweenSaveEvents; - - /**** - ** Game state - ****/ - /// A record of the buttons pressed as of the previous tick. - private SButton[] PreviousPressedButtons = new SButton[0]; - - /// A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick. - private KeyboardState PreviousKeyState; - - /// A record of the controller state (i.e. the up/down state for each button) as of the previous tick. - private GamePadState PreviousControllerState; - - /// A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick. - private MouseState PreviousMouseState; - - /// The previous mouse position on the screen adjusted for the zoom level. - private Point PreviousMousePosition; - - /// The window size value at last check. - private Point PreviousWindowSize; - - /// The save ID at last check. - private ulong PreviousSaveID; - - /// A hash of at last check. - private int PreviousGameLocations; - - /// A hash of the current location's at last check. - private int PreviousLocationObjects; - - /// The player's inventory at last check. - private IDictionary PreviousItems; - - /// The player's combat skill level at last check. - private int PreviousCombatLevel; - - /// The player's farming skill level at last check. - private int PreviousFarmingLevel; - - /// The player's fishing skill level at last check. - private int PreviousFishingLevel; - - /// The player's foraging skill level at last check. - private int PreviousForagingLevel; - - /// The player's mining skill level at last check. - private int PreviousMiningLevel; - - /// The player's luck skill level at last check. - private int PreviousLuckLevel; - - /// The player's location at last check. - private GameLocation PreviousGameLocation; - - /// The active game menu at last check. - private IClickableMenu PreviousActiveMenu; - - /// The mine level at last check. - private int PreviousMineLevel; - - /// The time of day (in 24-hour military format) at last check. - private int PreviousTime; - - /// The previous content locale. - private LocalizedContentManager.LanguageCode? PreviousLocale; - - /// An index incremented on every tick and reset every 60th tick (0–59). - private int CurrentUpdateTick; - - /// Whether this is the very first update tick since the game started. - private bool FirstUpdate; - - /// The current game instance. - private static SGame Instance; - - /**** - ** Private wrappers - ****/ - /// Simplifies access to private game code. - private static Reflector Reflection; - - // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming - /// Used to access private fields and methods. - private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); - private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); - private static float _fps - { - set => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); - } - private static Task _newDayTask => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); - public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); - private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(); - private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); - // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming - - - /********* - ** Accessors - *********/ - /// SMAPI's content manager. - public SContentManager SContentManager { get; } - - /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// Encapsulates monitoring and logging. - /// Simplifies access to private game code. - internal SGame(IMonitor monitor, Reflector reflection) - { - // initialise - this.Monitor = monitor; - this.FirstUpdate = true; - SGame.Instance = this; - SGame.Reflection = reflection; - - // set XNA option required by Stardew Valley - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - - // override content manager - this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); - this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); - this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); - Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); - reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager - } - - /**** - ** Intercepted methods & events - ****/ - /// Constructor a content manager to read XNB files. - /// The service provider to use to locate services. - /// The root directory to search for content. - protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) - { - // return default if SMAPI's content manager isn't initialised yet - if (this.SContentManager == null) - { - this.Monitor?.Log("SMAPI's content manager isn't initialised; skipping content manager interception.", LogLevel.Trace); - return base.CreateContentManager(serviceProvider, rootDirectory); - } - - // return single instance if valid - if (serviceProvider != this.Content.ServiceProvider) - throw new InvalidOperationException("SMAPI uses a single content manager internally. You can't get a new content manager with a different service provider."); - if (rootDirectory != this.Content.RootDirectory) - throw new InvalidOperationException($"SMAPI uses a single content manager internally. You can't get a new content manager with a different root directory (current is {this.Content.RootDirectory}, requested {rootDirectory})."); - return new ContentManagerShim(this.SContentManager, "(generated instance)"); - } - - /// The method called when the game is updating its state. This happens roughly 60 times per second. - /// A snapshot of the game timing state. - protected override void Update(GameTime gameTime) - { - try - { - /********* - ** Skip conditions - *********/ - // SMAPI exiting, stop processing game updates - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); - return; - } - - // While a background new-day task is in progress, the game skips its own update logic - // and defers to the XNA Update method. Running mod code in parallel to the background - // update is risky, because data changes can conflict (e.g. collection changed during - // enumeration errors) and data may change unexpectedly from one mod instruction to the - // next. - // - // Therefore we can just run Game1.Update here without raising any SMAPI events. There's - // a small chance that the task will finish after we defer but before the game checks, - // which means technically events should be raised, but the effects of missing one - // update tick are neglible and not worth the complications of bypassing Game1.Update. - if (SGame._newDayTask != null) - { - base.Update(gameTime); - return; - } - - // While the game is writing to the save file in the background, mods can unexpectedly - // fail since they don't have exclusive access to resources (e.g. collection changed - // during enumeration errors). To avoid problems, events are not invoked while a save - // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is - // opened (since the save hasn't started yet), but all other events should be suppressed. - if (Context.IsSaving) - { - // raise before-save - if (!this.IsBetweenSaveEvents) - { - this.IsBetweenSaveEvents = true; - this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); - } - - // suppress non-save events - base.Update(gameTime); - return; - } - if (this.IsBetweenSaveEvents) - { - // raise after-save - this.IsBetweenSaveEvents = false; - this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - - /********* - ** Game loaded events - *********/ - if (this.FirstUpdate) - { - GameEvents.InvokeInitialize(this.Monitor); - } - - /********* - ** Locale changed events - *********/ - if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) - { - var oldValue = this.PreviousLocale; - var newValue = LocalizedContentManager.CurrentLanguageCode; - - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); - - if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); - this.PreviousLocale = newValue; - } - - /********* - ** After load events - *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) - { - if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) - this.AfterLoadTimer--; - - if (this.AfterLoadTimer == 0) - { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - Context.IsWorldReady = true; - - SaveEvents.InvokeAfterLoad(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - } - - /********* - ** Exit to title events - *********/ - // before exit to title - if (Game1.exitToTitle) - this.IsExitingToTitle = true; - - // after exit to title - if (Context.IsWorldReady && this.IsExitingToTitle && Game1.activeClickableMenu is TitleMenu) - { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - - this.IsExitingToTitle = false; - this.CleanupAfterReturnToTitle(); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); - } - - /********* - ** Window events - *********/ - // Here we depend on the game's viewport instead of listening to the Window.Resize - // event because we need to notify mods after the game handles the resize, so the - // game's metadata (like Game1.viewport) are updated. That's a bit complicated - // since the game adds & removes its own handler on the fly. - if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) - { - Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); - GraphicsEvents.InvokeResize(this.Monitor); - this.PreviousWindowSize = size; - } - - /********* - ** Input events (if window has focus) - *********/ - if (Game1.game1.IsActive) - { - // get latest state - KeyboardState keyState; - GamePadState controllerState; - MouseState mouseState; - Point mousePosition; - try - { - keyState = Keyboard.GetState(); - controllerState = GamePad.GetState(PlayerIndex.One); - mouseState = Mouse.GetState(); - mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); - } - catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true - { - keyState = this.PreviousKeyState; - controllerState = this.PreviousControllerState; - mouseState = this.PreviousMouseState; - mousePosition = this.PreviousMousePosition; - } - - // analyse state - SButton[] currentlyPressedKeys = this.GetPressedButtons(keyState, mouseState, controllerState).ToArray(); - SButton[] previousPressedKeys = this.PreviousPressedButtons; - SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); - SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); - bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); - - // get cursor position - ICursorPosition cursor; - { - // cursor position - Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - Vector2 tile = new Vector2((Game1.viewport.X + screenPixels.X) / Game1.tileSize, (Game1.viewport.Y + screenPixels.Y) / Game1.tileSize); - Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton - ? tile - : Game1.player.GetGrabTile(); - cursor = new CursorPosition(screenPixels, tile, grabTile); - } - - // raise button pressed - foreach (SButton button in framePressedKeys) - { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); - - // legacy events - if (button.TryGetKeyboard(out Keys key)) - { - if (key != Keys.None) - ControlEvents.InvokeKeyPressed(this.Monitor, key); - } - else if (button.TryGetController(out Buttons controllerButton)) - { - if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); - else - ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); - } - } - - // raise button released - foreach (SButton button in frameReleasedKeys) - { - bool wasClick = - (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click - || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); - - // legacy events - if (button.TryGetKeyboard(out Keys key)) - { - if (key != Keys.None) - ControlEvents.InvokeKeyReleased(this.Monitor, key); - } - else if (button.TryGetController(out Buttons controllerButton)) - { - if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); - else - ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); - } - } - - // raise legacy state-changed events - if (keyState != this.PreviousKeyState) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousKeyState, keyState); - if (mouseState != this.PreviousMouseState) - ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousMouseState, mouseState, this.PreviousMousePosition, mousePosition); - - // track state - this.PreviousMouseState = mouseState; - this.PreviousMousePosition = mousePosition; - this.PreviousKeyState = keyState; - this.PreviousControllerState = controllerState; - this.PreviousPressedButtons = currentlyPressedKeys; - } - - /********* - ** Menu events - *********/ - if (Game1.activeClickableMenu != this.PreviousActiveMenu) - { - IClickableMenu previousMenu = this.PreviousActiveMenu; - IClickableMenu newMenu = Game1.activeClickableMenu; - - // log context - if (this.VerboseLogging) - { - if (previousMenu == null) - this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); - else if (newMenu == null) - this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); - else - this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); - } - - // raise menu events - if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); - else - MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); - - // update previous menu - // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) - this.PreviousActiveMenu = newMenu; - } - - /********* - ** World & player events - *********/ - if (Context.IsWorldReady) - { - // raise current location changed - if (Game1.currentLocation != this.PreviousGameLocation) - { - if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); - } - - // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - - // raise events that shouldn't be triggered on initial load - if (Game1.uniqueIDForThisGame == this.PreviousSaveID) - { - // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); - if (Game1.player.fishingLevel != this.PreviousFishingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); - if (Game1.player.foragingLevel != this.PreviousForagingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); - if (Game1.player.miningLevel != this.PreviousMiningLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); - if (Game1.player.luckLevel != this.PreviousLuckLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); - - // raise player inventory changed - ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); - if (changedItems.Any()) - PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems); - - // raise current location's object list changed - if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); - - // raise time changed - if (Game1.timeOfDay != this.PreviousTime) - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); - - // raise mine level changed - if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); - } - - // update state - this.PreviousGameLocations = this.GetHash(Game1.locations); - this.PreviousGameLocation = Game1.currentLocation; - this.PreviousCombatLevel = Game1.player.combatLevel; - this.PreviousFarmingLevel = Game1.player.farmingLevel; - this.PreviousFishingLevel = Game1.player.fishingLevel; - this.PreviousForagingLevel = Game1.player.foragingLevel; - this.PreviousMiningLevel = Game1.player.miningLevel; - this.PreviousLuckLevel = Game1.player.luckLevel; - this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); - this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); - this.PreviousTime = Game1.timeOfDay; - this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; - this.PreviousSaveID = Game1.uniqueIDForThisGame; - } - - /********* - ** Game update - *********/ - try - { - base.Update(gameTime); - } - catch (Exception ex) - { - this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); - } - - /********* - ** Update events - *********/ - GameEvents.InvokeUpdateTick(this.Monitor); - if (this.FirstUpdate) - this.FirstUpdate = false; - if (this.CurrentUpdateTick % 2 == 0) - GameEvents.InvokeSecondUpdateTick(this.Monitor); - if (this.CurrentUpdateTick % 4 == 0) - GameEvents.InvokeFourthUpdateTick(this.Monitor); - if (this.CurrentUpdateTick % 8 == 0) - GameEvents.InvokeEighthUpdateTick(this.Monitor); - if (this.CurrentUpdateTick % 15 == 0) - GameEvents.InvokeQuarterSecondTick(this.Monitor); - if (this.CurrentUpdateTick % 30 == 0) - GameEvents.InvokeHalfSecondTick(this.Monitor); - if (this.CurrentUpdateTick % 60 == 0) - GameEvents.InvokeOneSecondTick(this.Monitor); - this.CurrentUpdateTick += 1; - if (this.CurrentUpdateTick >= 60) - this.CurrentUpdateTick = 0; - - this.UpdateCrashTimer.Reset(); - } - catch (Exception ex) - { - // log error - this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error); - - // exit if irrecoverable - if (!this.UpdateCrashTimer.Decrement()) - this.Monitor.ExitGameImmediately("the game crashed when updating, and SMAPI was unable to recover the game."); - } - } - - /// The method called to draw everything to the screen. - /// A snapshot of the game timing state. - protected override void Draw(GameTime gameTime) - { - Context.IsInDrawLoop = true; - try - { - this.DrawImpl(gameTime); - this.DrawCrashTimer.Reset(); - } - catch (Exception ex) - { - // log error - this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); - - // exit if irrecoverable - if (!this.DrawCrashTimer.Decrement()) - { - this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); - return; - } - - // recover sprite batch - try - { - if (Game1.spriteBatch.IsOpen(SGame.Reflection)) - { - this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); - Game1.spriteBatch.End(); - } - } - catch (Exception innerEx) - { - this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error); - } - } - Context.IsInDrawLoop = false; - } - - /// Replicate the game's draw logic with some changes for SMAPI. - /// A snapshot of the game timing state. - /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. - [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] - private void DrawImpl(GameTime gameTime) - { - if (Game1.debugMode) - { - if (SGame._fpsStopwatch.IsRunning) - { - float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; - SGame._fpsList.Add(totalSeconds); - while (SGame._fpsList.Count >= 120) - SGame._fpsList.RemoveAt(0); - float num = 0.0f; - foreach (float fps in SGame._fpsList) - num += fps; - SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); - } - SGame._fpsStopwatch.Restart(); - } - else - { - if (SGame._fpsStopwatch.IsRunning) - SGame._fpsStopwatch.Reset(); - SGame._fps = 0.0f; - SGame._fpsList.Clear(); - } - if (SGame._newDayTask != null) - { - this.GraphicsDevice.Clear(this.bgColor); - //base.Draw(gameTime); - } - else - { - if ((double)Game1.options.zoomLevel != 1.0) - this.GraphicsDevice.SetRenderTarget(this.screenWrapper); - if (this.IsSaving) - { - this.GraphicsDevice.Clear(this.bgColor); - IClickableMenu activeClickableMenu = Game1.activeClickableMenu; - if (activeClickableMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - activeClickableMenu.exitThisMenu(); - } - Game1.spriteBatch.End(); - } - //base.Draw(gameTime); - this.renderScreenBuffer(); - } - else - { - this.GraphicsDevice.Clear(this.bgColor); - if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else if ((int)Game1.gameMode == 11) - { - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); - Game1.spriteBatch.End(); - } - else if (Game1.currentMinigame != null) - { - Game1.currentMinigame.draw(Game1.spriteBatch); - if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); - } - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else if (Game1.showingEndOfNightStuff) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.activeClickableMenu != null) - { - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else if ((int)Game1.gameMode == 6) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - string str1 = ""; - for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) - str1 += "."; - string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string str3 = str1; - string s = str2 + str3; - string str4 = "..."; - string str5 = str2 + str4; - int widthOfString = SpriteText.getWidthOfString(str5); - int height = 64; - int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; - SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else - { - Microsoft.Xna.Framework.Rectangle rectangle; - if ((int)Game1.gameMode == 0) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - } - else - { - if (Game1.drawLighting) - { - this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Color.White * 0.0f); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight)); - for (int index = 0; index < Game1.currentLightSources.Count; ++index) - { - if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(index).position, (int)((double)Game1.currentLightSources.ElementAt(index).radius * (double)Game1.tileSize * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); - } - Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); - } - if (Game1.bloomDay && Game1.bloom != null) - Game1.bloom.BeginDraw(); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); - if (Game1.background != null) - Game1.background.draw(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.currentLocation.drawWater(Game1.spriteBatch); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); - } - } - Microsoft.Xna.Framework.Rectangle bounds; - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - } - } - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - rectangle = Game1.shadowTexture.Bounds; - double y = (double)rectangle.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.displayFarmer) - Game1.player.draw(Game1.spriteBatch); - if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0)); - Game1.currentLocation.draw(Game1.spriteBatch); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; - } - if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Name.Equals("Farm")) - this.drawFarmBuildings(); - if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); - foreach (Warp warp in Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U) - { - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - } - if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null) - { - if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) - { - Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size1 = Game1.viewport.Size; - if (layer1.PickTile(mapDisplayLocation1, size1) != null) - { - Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size2 = Game1.viewport.Size; - if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_127; - } - else - goto label_127; - } - Game1.drawPlayerHeldObject(Game1.player); - } - label_127: - if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) - { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - } - if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) - { - Color color = Color.White; - switch ((int)((double)Game1.toolHold / 600.0) + 2) - { - case 1: - color = Tool.copperColor; - break; - case 2: - color = Tool.steelColor; - break; - case 3: - color = Tool.goldColor; - break; - case 4: - color = Tool.iridiumColor; - break; - } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color); - } - if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) - Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize))))) - { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); - } - Game1.spriteBatch.End(); - //base.Draw(gameTime); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - foreach (NPC actor in Game1.currentLocation.currentEvent.actors) - { - if (actor.isEmoting) - { - Vector2 localPosition = actor.getLocalPosition(Game1.viewport); - localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3); - if (actor.age == 2) - localPosition.Y += (float)(Game1.tileSize / 2); - else if (actor.gender == 1) - localPosition.Y += (float)(Game1.tileSize / 6); - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); - } - } - } - Game1.spriteBatch.End(); - if (Game1.drawLighting) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); - if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.drawGrid) - { - int x1 = -Game1.viewport.X % Game1.tileSize; - float num1 = (float)(-Game1.viewport.Y % Game1.tileSize); - int x2 = x1; - while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); - x2 += Game1.tileSize; - } - float num2 = num1; - while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); - num2 += (float)Game1.tileSize; - } - } - if (Game1.currentBillboard != 0) - this.drawBillboard(); - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) - { - GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); - this.drawHUD(); - GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); - } - else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); - if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) - { - for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) - Game1.hudMessages[i].draw(Game1.spriteBatch, i); - } - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) - this.drawDialogueBox(); - Viewport viewport; - if (Game1.progressBar) - { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea; - int y1 = rectangle.Bottom - Game1.tileSize * 2; - int dialogueWidth = Game1.dialogueWidth; - int height1 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); - Color lightGray = Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - viewport = Game1.graphics.GraphicsDevice.Viewport; - rectangle = viewport.TitleSafeArea; - int y2 = rectangle.Bottom - Game1.tileSize * 2; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - int height2 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); - Color dimGray = Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); - } - if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Blue * 0.2f; - spriteBatch.Draw(staminaRect, bounds, color); - } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - else if ((double)Game1.flashAlpha > 0.0) - { - if (Game1.options.screenFlash) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.White * Math.Min(1f, Game1.flashAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - Game1.flashAlpha -= 0.1f; - } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) - this.drawDialogueBox(); - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0); - if (Game1.debugMode) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - SpriteFont smallFont = Game1.smallFont; - object[] objArray = new object[10]; - int index1 = 0; - string str1; - if (!Game1.panMode) - str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize); - else - str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize); - objArray[index1] = (object)str1; - int index2 = 1; - string str2 = " mouseTransparency: "; - objArray[index2] = (object)str2; - int index3 = 2; - float cursorTransparency = Game1.mouseCursorTransparency; - objArray[index3] = (object)cursorTransparency; - int index4 = 3; - string str3 = " mousePosition: "; - objArray[index4] = (object)str3; - int index5 = 4; - int mouseX = Game1.getMouseX(); - objArray[index5] = (object)mouseX; - int index6 = 5; - string str4 = ","; - objArray[index6] = (object)str4; - int index7 = 6; - int mouseY = Game1.getMouseY(); - objArray[index7] = (object)mouseY; - int index8 = 7; - string newLine = Environment.NewLine; - objArray[index8] = (object)newLine; - int index9 = 8; - string str5 = "debugOutput: "; - objArray[index9] = (object)str5; - int index10 = 9; - string debugOutput = Game1.debugOutput; - objArray[index10] = (object)debugOutput; - string text = string.Concat(objArray); - Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); - Color red = Color.Red; - double num1 = 0.0; - Vector2 zero = Vector2.Zero; - double num2 = 1.0; - int num3 = 0; - double num4 = 0.99999988079071; - spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.showKeyHelp) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null) - { - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - else if (Game1.farmEvent != null) - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - Game1.spriteBatch.End(); - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - - if (GraphicsEvents.HasPostRenderListeners()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); - Game1.spriteBatch.End(); - } - - this.renderScreenBuffer(); - } - } - } - } - - /**** - ** Methods - ****/ - /// Perform any cleanup needed when the player unloads a save and returns to the title screen. - private void CleanupAfterReturnToTitle() - { - Context.IsWorldReady = false; - this.AfterLoadTimer = 5; - this.PreviousSaveID = 0; - } - - /// Get the buttons pressed in the given stats. - /// The keyboard state. - /// The mouse state. - /// The controller state. - private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) - { - // keyboard - foreach (Keys key in keyboard.GetPressedKeys()) - yield return key.ToSButton(); - - // mouse - if (mouse.LeftButton == ButtonState.Pressed) - yield return SButton.MouseLeft; - if (mouse.RightButton == ButtonState.Pressed) - yield return SButton.MouseRight; - if (mouse.MiddleButton == ButtonState.Pressed) - yield return SButton.MouseMiddle; - if (mouse.XButton1 == ButtonState.Pressed) - yield return SButton.MouseX1; - if (mouse.XButton2 == ButtonState.Pressed) - yield return SButton.MouseX2; - - // controller - if (controller.IsConnected) - { - if (controller.Buttons.A == ButtonState.Pressed) - yield return SButton.ControllerA; - if (controller.Buttons.B == ButtonState.Pressed) - yield return SButton.ControllerB; - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; - if (controller.Buttons.LeftStick == ButtonState.Pressed) - yield return SButton.LeftStick; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; - if (controller.Buttons.RightStick == ButtonState.Pressed) - yield return SButton.RightStick; - if (controller.Buttons.Start == ButtonState.Pressed) - yield return SButton.ControllerStart; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; - if (controller.DPad.Up == ButtonState.Pressed) - yield return SButton.DPadUp; - if (controller.DPad.Down == ButtonState.Pressed) - yield return SButton.DPadDown; - if (controller.DPad.Left == ButtonState.Pressed) - yield return SButton.DPadLeft; - if (controller.DPad.Right == ButtonState.Pressed) - yield return SButton.DPadRight; - if (controller.Triggers.Left > 0.2f) - yield return SButton.LeftTrigger; - if (controller.Triggers.Right > 0.2f) - yield return SButton.RightTrigger; - } - } - - /// Get the player inventory changes between two states. - /// The player's current inventory. - /// The player's previous inventory. - private IEnumerable GetInventoryChanges(IEnumerable current, IDictionary previous) - { - current = current.Where(n => n != null).ToArray(); - foreach (Item item in current) - { - // stack size changed - if (previous != null && previous.ContainsKey(item)) - { - if (previous[item] != item.Stack) - yield return new ItemStackChange { Item = item, StackChange = item.Stack - previous[item], ChangeType = ChangeType.StackChange }; - } - - // new item - else - yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; - } - - // removed items - if (previous != null) - { - foreach (var entry in previous) - { - if (current.Any(i => i == entry.Key)) - continue; - - yield return new ItemStackChange { Item = entry.Key, StackChange = -entry.Key.Stack, ChangeType = ChangeType.Removed }; - } - } - } - - /// Get a hash value for an enumeration. - /// The enumeration of items to hash. - private int GetHash(IEnumerable enumerable) - { - int hash = 0; - foreach (object v in enumerable) - hash ^= v.GetHashCode(); - return hash; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs deleted file mode 100644 index 3193aa3c..00000000 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Xna.Framework.Input; -using Newtonsoft.Json; -using StardewModdingAPI.Utilities; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Encapsulates SMAPI's JSON file parsing. - internal class JsonHelper - { - /********* - ** Accessors - *********/ - /// The JSON settings to use when serialising and deserialising files. - private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded - Converters = new List - { - new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) - } - }; - - - /********* - ** Public methods - *********/ - /// Read a JSON file. - /// The model type. - /// The absolete file path. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - /// The given path is empty or invalid. - public TModel ReadJsonFile(string fullPath) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // read file - string json; - try - { - json = File.ReadAllText(fullPath); - } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) - { - return null; - } - - // deserialise model - try - { - return JsonConvert.DeserializeObject(json, this.JsonSettings); - } - catch (JsonReaderException ex) - { - string message = $"The file at {fullPath} doesn't seem to be valid JSON."; - - string text = File.ReadAllText(fullPath); - if (text.Contains("“") || text.Contains("”")) - message += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; - - message += $"\nTechnical details: {ex.Message}"; - throw new JsonReaderException(message); - } - } - - /// Save to a JSON file. - /// The model type. - /// The absolete file path. - /// The model to save. - /// The given path is empty or invalid. - public void WriteJsonFile(string fullPath, TModel model) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // create directory if needed - string dir = Path.GetDirectoryName(fullPath); - if (dir == null) - throw new ArgumentException("The file path is invalid.", nameof(fullPath)); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - // write file - string json = JsonConvert.SerializeObject(model, this.JsonSettings); - File.WriteAllText(fullPath, json); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs deleted file mode 100644 index 917c950d..00000000 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Overrides how SMAPI reads and writes and fields. - internal class SFieldConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return - objectType == typeof(ISemanticVersion) - || objectType == typeof(IManifestDependency[]) - || objectType == typeof(ModDataID) - || objectType == typeof(ModCompatibility[]); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // semantic version - if (objectType == typeof(ISemanticVersion)) - { - JToken token = JToken.Load(reader); - switch (token.Type) - { - case JTokenType.Object: - { - JObject obj = (JObject)token; - int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); - string build = obj.Value(nameof(ISemanticVersion.Build)); - return new SemanticVersion(major, minor, patch, build); - } - - case JTokenType.String: - { - string str = token.Value(); - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) - throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta."); - return version; - } - - default: - throw new SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); - } - } - - // manifest dependencies - if (objectType == typeof(IManifestDependency[])) - { - List result = new List(); - foreach (JObject obj in JArray.Load(reader).Children()) - { - string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); - string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); - bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; - result.Add(new ManifestDependency(uniqueID, minVersion, required)); - } - return result.ToArray(); - } - - // mod data ID - if (objectType == typeof(ModDataID)) - { - JToken token = JToken.Load(reader); - return new ModDataID(token.Value()); - } - - // mod compatibility records - if (objectType == typeof(ModCompatibility[])) - { - List result = new List(); - foreach (JProperty property in JObject.Load(reader).Properties()) - { - string range = property.Name; - ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value(nameof(ModCompatibility.Status))); - string reasonPhrase = property.Value.Value(nameof(ModCompatibility.ReasonPhrase)); - - result.Add(new ModCompatibility(range, status, reasonPhrase)); - } - return result.ToArray(); - } - - // unknown - throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs deleted file mode 100644 index 37108556..00000000 --- a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// A variant of which only converts certain enums. - internal class SelectiveStringEnumConverter : StringEnumConverter - { - /********* - ** Properties - *********/ - /// The enum type names to convert. - private readonly HashSet Types; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The enum types to convert. - public SelectiveStringEnumConverter(params Type[] types) - { - this.Types = new HashSet(types.Select(p => p.FullName)); - } - - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type type) - { - return - base.CanConvert(type) - && this.Types.Contains((Nullable.GetUnderlyingType(type) ?? type).FullName); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs deleted file mode 100644 index 6c0fdc90..00000000 --- a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework.Utilities -{ - /// A wrapper meant for tracking recursive contexts. - /// The key type. - internal class ContextHash : HashSet - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public ContextHash() { } - - /// Construct an instance. - /// The implementation to use when comparing values in the set, or null to use the default comparer for the set type. - public ContextHash(IEqualityComparer comparer) - : base(comparer) { } - - /// Add a key while an action is in progress, and remove it when it completes. - /// The key to add. - /// The action to perform. - /// The specified key is already added. - public void Track(T key, Action action) - { - if (this.Contains(key)) - throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); - - this.Add(key); - try - { - action(); - } - finally - { - this.Remove(key); - } - } - - /// Add a key while an action is in progress, and remove it when it completes. - /// The value type returned by the method. - /// The key to add. - /// The action to perform. - public TResult Track(T key, Func action) - { - if (this.Contains(key)) - throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); - - this.Add(key); - try - { - return action(); - } - finally - { - this.Remove(key); - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs deleted file mode 100644 index 921a35ce..00000000 --- a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace StardewModdingAPI.Framework.Utilities -{ - /// Counts down from a baseline value. - internal class Countdown - { - /********* - ** Accessors - *********/ - /// The initial value from which to count down. - public int Initial { get; } - - /// The current value. - public int Current { get; private set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The initial value from which to count down. - public Countdown(int initial) - { - this.Initial = initial; - this.Current = initial; - } - - /// Reduce the current value by one. - /// Returns whether the value was decremented (i.e. wasn't already zero). - public bool Decrement() - { - if (this.Current <= 0) - return false; - - this.Current--; - return true; - } - - /// Restart the countdown. - public void Reset() - { - this.Current = this.Initial; - } - } -} diff --git a/src/StardewModdingAPI/Framework/WebApiClient.cs b/src/StardewModdingAPI/Framework/WebApiClient.cs deleted file mode 100644 index f3c7de28..00000000 --- a/src/StardewModdingAPI/Framework/WebApiClient.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Newtonsoft.Json; -using StardewModdingAPI.Models; - -namespace StardewModdingAPI.Framework -{ - /// Provides methods for interacting with the SMAPI web API. - internal class WebApiClient - { - /********* - ** Properties - *********/ - /// The base URL for the web API. - private readonly Uri BaseUrl; - - /// The API version number. - private readonly ISemanticVersion Version; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The base URL for the web API. - /// The web API version. - public WebApiClient(string baseUrl, ISemanticVersion version) - { -#if !SMAPI_FOR_WINDOWS - baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac -#endif - this.BaseUrl = new Uri(baseUrl); - this.Version = version; - } - - /// Get the latest SMAPI version. - /// The mod keys for which to fetch the latest version. - public IDictionary GetModInfo(params string[] modKeys) - { - return this.Post>( - $"v{this.Version}/mods", - new ModSearchModel(modKeys) - ); - } - - - /********* - ** Private methods - *********/ - /// Fetch the response from the backend API. - /// The body content type. - /// The expected response type. - /// The request URL, optionally excluding the base URL. - /// The body content to post. - private TResult Post(string url, TBody content) - { - /*** - ** Note: avoid HttpClient for Mac compatibility. - ***/ - using (WebClient client = new WebClient()) - { - Uri fullUrl = new Uri(this.BaseUrl, url); - string data = JsonConvert.SerializeObject(content); - - client.Headers["Content-Type"] = "application/json"; - client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; - string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response); - } - } - } -} diff --git a/src/StardewModdingAPI/IAssetData.cs b/src/StardewModdingAPI/IAssetData.cs deleted file mode 100644 index c3021144..00000000 --- a/src/StardewModdingAPI/IAssetData.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// Generic metadata and methods for a content asset being loaded. - /// The expected data type. - public interface IAssetData : IAssetInfo - { - /********* - ** Accessors - *********/ - /// The content data being read. - TValue Data { get; } - - - /********* - ** Public methods - *********/ - /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. - /// The new content value. - /// The is null. - /// The 's type is not compatible with the loaded asset's type. - void ReplaceWith(TValue value); - } - - /// Generic metadata and methods for a content asset being loaded. - public interface IAssetData : IAssetData - { - /********* - ** Public methods - *********/ - /// Get a helper to manipulate the data as a dictionary. - /// The expected dictionary key. - /// The expected dictionary value. - /// The content being read isn't a dictionary. - IAssetDataForDictionary AsDictionary(); - - /// Get a helper to manipulate the data as an image. - /// The content being read isn't an image. - IAssetDataForImage AsImage(); - - /// Get the data as a given type. - /// The expected data type. - /// The data can't be converted to . - TData GetData(); - } -} diff --git a/src/StardewModdingAPI/IAssetDataForDictionary.cs b/src/StardewModdingAPI/IAssetDataForDictionary.cs deleted file mode 100644 index 53c24346..00000000 --- a/src/StardewModdingAPI/IAssetDataForDictionary.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - public interface IAssetDataForDictionary : IAssetData> - { - /********* - ** Public methods - *********/ - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - void Set(TKey key, TValue value); - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - void Set(TKey key, Func value); - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - void Set(Func replacer); - } -} diff --git a/src/StardewModdingAPI/IAssetDataForImage.cs b/src/StardewModdingAPI/IAssetDataForImage.cs deleted file mode 100644 index 4584a20e..00000000 --- a/src/StardewModdingAPI/IAssetDataForImage.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - public interface IAssetDataForImage : IAssetData - { - /********* - ** Public methods - *********/ - /// Overwrite part of the image. - /// The image to patch into the content. - /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. - /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. - /// Indicates how an image should be patched. - /// One of the arguments is null. - /// The is outside the bounds of the spritesheet. - /// The content being read isn't an image. - void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); - } -} diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/StardewModdingAPI/IAssetEditor.cs deleted file mode 100644 index d2c6f295..00000000 --- a/src/StardewModdingAPI/IAssetEditor.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace StardewModdingAPI -{ - /// Edits matching content assets. - public interface IAssetEditor - { - /********* - ** Public methods - *********/ - /// Get whether this instance can edit the given asset. - /// Basic metadata about the asset being loaded. - bool CanEdit(IAssetInfo asset); - - /// Edit a matched asset. - /// A helper which encapsulates metadata about an asset and enables changes to it. - void Edit(IAssetData asset); - } -} diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/StardewModdingAPI/IAssetInfo.cs deleted file mode 100644 index 5dd58e2e..00000000 --- a/src/StardewModdingAPI/IAssetInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// Basic metadata for a content asset. - public interface IAssetInfo - { - /********* - ** Accessors - *********/ - /// The content's locale code, if the content is localised. - string Locale { get; } - - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. - string AssetName { get; } - - /// The content data type. - Type DataType { get; } - - - /********* - ** Public methods - *********/ - /// Get whether the asset name being loaded matches a given name after normalisation. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - bool AssetNameEquals(string path); - } -} diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/StardewModdingAPI/IAssetLoader.cs deleted file mode 100644 index ad97b941..00000000 --- a/src/StardewModdingAPI/IAssetLoader.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace StardewModdingAPI -{ - /// Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use instead. - public interface IAssetLoader - { - /********* - ** Public methods - *********/ - /// Get whether this instance can load the initial version of the given asset. - /// Basic metadata about the asset being loaded. - bool CanLoad(IAssetInfo asset); - - /// Load a matched asset. - /// Basic metadata about the asset being loaded. - T Load(IAssetInfo asset); - } -} diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/StardewModdingAPI/ICommandHelper.cs deleted file mode 100644 index fb562e32..00000000 --- a/src/StardewModdingAPI/ICommandHelper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// Provides an API for managing console commands. - public interface ICommandHelper : IModLinked - { - /********* - ** Public methods - *********/ - /// Add a console command. - /// The command name, which the user must type to trigger it. - /// The human-readable documentation shown when the player runs the built-in 'help' command. - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - /// The or is null or empty. - /// The is not a valid format. - /// There's already a command with that name. - ICommandHelper Add(string name, string documentation, Action callback); - - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. - bool Trigger(string name, string[] arguments); - } -} diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs deleted file mode 100644 index b78b165b..00000000 --- a/src/StardewModdingAPI/IContentHelper.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewValley; - -namespace StardewModdingAPI -{ - /// Provides an API for loading content assets. - public interface IContentHelper : IModLinked - { - /********* - ** Accessors - *********/ - /// Interceptors which provide the initial versions of matching content assets. - IList AssetLoaders { get; } - - /// Interceptors which edit matching content assets after they're loaded. - IList AssetEditors { get; } - - /// The game's current locale code (like pt-BR). - string CurrentLocale { get; } - - /// The game's current locale as an enum value. - LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } - - - /********* - ** Public methods - *********/ - /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - T Load(string key, ContentSource source = ContentSource.ModFolder); - - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); - - /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to invalidate in the content folder. - /// The is empty or contains invalid characters. - /// Returns whether the given asset key was cached. - bool InvalidateCache(string key); - - /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. - /// The asset type to remove from the cache. - /// Returns whether any assets were invalidated. - bool InvalidateCache(); - } -} diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/StardewModdingAPI/ICursorPosition.cs deleted file mode 100644 index ddb8eb49..00000000 --- a/src/StardewModdingAPI/ICursorPosition.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace StardewModdingAPI -{ - /// Represents a cursor position in the different coordinate systems. - public interface ICursorPosition - { - /// The pixel position relative to the top-left corner of the visible screen. - Vector2 ScreenPixels { get; } - - /// The tile position under the cursor relative to the top-left corner of the map. - Vector2 Tile { get; } - - /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. - Vector2 GrabTile { get; } - } -} diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs deleted file mode 100644 index 9db1d538..00000000 --- a/src/StardewModdingAPI/IManifest.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// A manifest which describes a mod for SMAPI. - public interface IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - string Name { get; } - - /// A brief description of the mod. - string Description { get; } - - /// The mod author's name. - string Author { get; } - - /// The mod version. - ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - ISemanticVersion MinimumApiVersion { get; } - - /// The unique mod ID. - string UniqueID { get; } - - /// The name of the DLL in the directory that has the method. - string EntryDll { get; } - - /// The other mods that must be loaded before this mod. - IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; set; } - - /// Any manifest fields which didn't match a valid field. - IDictionary ExtraFields { get; } - } -} diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs deleted file mode 100644 index e86cd1f4..00000000 --- a/src/StardewModdingAPI/IManifestDependency.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI -{ - /// A mod dependency listed in a mod manifest. - public interface IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - bool IsRequired { get; } - } -} diff --git a/src/StardewModdingAPI/IMod.cs b/src/StardewModdingAPI/IMod.cs deleted file mode 100644 index 35ac7c0f..00000000 --- a/src/StardewModdingAPI/IMod.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace StardewModdingAPI -{ - /// The implementation for a Stardew Valley mod. - public interface IMod - { - /********* - ** Accessors - *********/ - /// Provides simplified APIs for writing mods. - IModHelper Helper { get; } - - /// Writes messages to the console and log file. - IMonitor Monitor { get; } - - /// The mod's manifest. - IManifest ModManifest { get; } - - - /********* - ** Public methods - *********/ - /// The mod entry point, called after the mod is first loaded. - /// Provides simplified APIs for writing mods. - void Entry(IModHelper helper); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs deleted file mode 100644 index 116e8508..00000000 --- a/src/StardewModdingAPI/IModHelper.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace StardewModdingAPI -{ - /// Provides simplified APIs for writing mods. - public interface IModHelper - { - /********* - ** Accessors - *********/ - /// The full path to the mod's folder. - string DirectoryPath { get; } - - /// An API for loading content assets. - IContentHelper Content { get; } - - /// Simplifies access to private game code. - IReflectionHelper Reflection { get; } - - /// Metadata about loaded mods. - IModRegistry ModRegistry { get; } - - /// An API for managing console commands. - ICommandHelper ConsoleCommands { get; } - - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - ITranslationHelper Translation { get; } - - - /********* - ** Public methods - *********/ - /**** - ** Mod config file - ****/ - /// Read the mod's configuration file (and create it if needed). - /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. - TConfig ReadConfig() where TConfig : class, new(); - - /// Save to the mod's configuration file. - /// The config class type. - /// The config settings to save. - void WriteConfig(TConfig config) where TConfig : class, new(); - - /**** - ** Generic JSON files - ****/ - /// Read a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - TModel ReadJsonFile(string path) where TModel : class; - - /// Save to a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// The model to save. - void WriteJsonFile(string path, TModel model) where TModel : class; - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/IModLinked.cs b/src/StardewModdingAPI/IModLinked.cs deleted file mode 100644 index 172ee30c..00000000 --- a/src/StardewModdingAPI/IModLinked.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI -{ - /// An instance linked to a mod. - public interface IModLinked - { - /********* - ** Accessors - *********/ - /// The unique ID of the mod for which the instance was created. - string ModID { get; } - } -} diff --git a/src/StardewModdingAPI/IModRegistry.cs b/src/StardewModdingAPI/IModRegistry.cs deleted file mode 100644 index 5ef3fd65..00000000 --- a/src/StardewModdingAPI/IModRegistry.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// Provides an API for fetching metadata about loaded mods. - public interface IModRegistry : IModLinked - { - /// Get metadata for all loaded mods. - IEnumerable GetAll(); - - /// Get metadata for a loaded mod. - /// The mod's unique ID. - /// Returns the matching mod's metadata, or null if not found. - IManifest Get(string uniqueID); - - /// Get whether a mod has been loaded. - /// The mod's unique ID. - bool IsLoaded(string uniqueID); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/IMonitor.cs b/src/StardewModdingAPI/IMonitor.cs deleted file mode 100644 index 62c479bc..00000000 --- a/src/StardewModdingAPI/IMonitor.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace StardewModdingAPI -{ - /// Encapsulates monitoring and logging for a given module. - public interface IMonitor - { - /********* - ** Accessors - *********/ - /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. - bool IsExiting { get; } - - - /********* - ** Methods - *********/ - /// Log a message for the player or developer. - /// The message to log. - /// The log severity level. - void Log(string message, LogLevel level = LogLevel.Debug); - - /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. - /// The reason for the shutdown. - void ExitGameImmediately(string reason); - } -} diff --git a/src/StardewModdingAPI/IPrivateField.cs b/src/StardewModdingAPI/IPrivateField.cs deleted file mode 100644 index 3e681c12..00000000 --- a/src/StardewModdingAPI/IPrivateField.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI -{ - /// A private field obtained through reflection. - /// The field value type. - public interface IPrivateField - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - FieldInfo FieldInfo { get; } - - - /********* - ** Public methods - *********/ - /// Get the field value. - TValue GetValue(); - - /// Set the field value. - //// The value to set. - void SetValue(TValue value); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/IPrivateMethod.cs b/src/StardewModdingAPI/IPrivateMethod.cs deleted file mode 100644 index 67fc8b3c..00000000 --- a/src/StardewModdingAPI/IPrivateMethod.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI -{ - /// A private method obtained through reflection. - public interface IPrivateMethod - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - MethodInfo MethodInfo { get; } - - - /********* - ** Public methods - *********/ - /// Invoke the method. - /// The return type. - /// The method arguments to pass in. - TValue Invoke(params object[] arguments); - - /// Invoke the method. - /// The method arguments to pass in. - void Invoke(params object[] arguments); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/IPrivateProperty.cs b/src/StardewModdingAPI/IPrivateProperty.cs deleted file mode 100644 index 8d67fa7a..00000000 --- a/src/StardewModdingAPI/IPrivateProperty.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI -{ - /// A private property obtained through reflection. - /// The property value type. - public interface IPrivateProperty - { - /********* - ** Accessors - *********/ - /// The reflection metadata. - PropertyInfo PropertyInfo { get; } - - - /********* - ** Public methods - *********/ - /// Get the property value. - TValue GetValue(); - - /// Set the property value. - //// The value to set. - void SetValue(TValue value); - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/StardewModdingAPI/IReflectionHelper.cs deleted file mode 100644 index fb2c7861..00000000 --- a/src/StardewModdingAPI/IReflectionHelper.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// Provides an API for accessing private game code. - public interface IReflectionHelper : IModLinked - { - /********* - ** Public methods - *********/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateField GetPrivateField(object obj, string name, bool required = true); - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateField GetPrivateField(Type type, string name, bool required = true); - - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true); - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true); - - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// This is a shortcut for followed by . - TValue GetPrivateValue(object obj, string name, bool required = true); - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// This is a shortcut for followed by . - TValue GetPrivateValue(Type type, string name, bool required = true); - - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); - } -} diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/StardewModdingAPI/ISemanticVersion.cs deleted file mode 100644 index 0483c97b..00000000 --- a/src/StardewModdingAPI/ISemanticVersion.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - int PatchVersion { get; } - - /// An optional build tag. - string Build { get; } - - - /********* - ** Accessors - *********/ - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsOlderThan(string other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsNewerThan(string other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - bool IsBetween(string min, string max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs deleted file mode 100644 index c4b72444..00000000 --- a/src/StardewModdingAPI/ITranslationHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using StardewValley; - -namespace StardewModdingAPI -{ - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - public interface ITranslationHelper : IModLinked - { - /********* - ** Accessors - *********/ - /// The current locale. - string Locale { get; } - - /// The game's current language code. - LocalizedContentManager.LanguageCode LocaleEnum { get; } - - - /********* - ** Public methods - *********/ - /// Get all translations for the current locale. - IEnumerable GetTranslations(); - - /// Get a translation for the current locale. - /// The translation key. - Translation Get(string key); - - /// Get a translation for the current locale. - /// The translation key. - /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. - Translation Get(string key, object tokens); - } -} diff --git a/src/StardewModdingAPI/LogLevel.cs b/src/StardewModdingAPI/LogLevel.cs deleted file mode 100644 index 89647876..00000000 --- a/src/StardewModdingAPI/LogLevel.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StardewModdingAPI -{ - /// The log severity levels. - public enum LogLevel - { - /// Tracing info intended for developers. - Trace, - - /// Troubleshooting info that may be relevant to the player. - Debug, - - /// Info relevant to the player. This should be used judiciously. - Info, - - /// An issue the player should be aware of. This should be used rarely. - Warn, - - /// A message indicating something went wrong. - Error, - - /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. - Alert - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/StardewModdingAPI/Metadata/CoreAssets.cs deleted file mode 100644 index 24f23af7..00000000 --- a/src/StardewModdingAPI/Metadata/CoreAssets.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework; -using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Buildings; -using StardewValley.Locations; -using StardewValley.Objects; -using StardewValley.Projectiles; -using StardewValley.TerrainFeatures; - -namespace StardewModdingAPI.Metadata -{ - /// Provides metadata about core assets in the game. - internal class CoreAssets - { - /********* - ** Properties - *********/ - /// Normalises an asset key to match the cache key. - protected readonly Func GetNormalisedPath; - - /// Setters which update static or singleton texture fields indexed by normalised asset key. - private readonly IDictionary> SingletonSetters; - - - /********* - ** Public methods - *********/ - /// Initialise the core asset data. - /// Normalises an asset key to match the cache key. - public CoreAssets(Func getNormalisedPath) - { - this.GetNormalisedPath = getNormalisedPath; - this.SingletonSetters = - new Dictionary> - { - // from Game1.loadContent - ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), - ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), - ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), - ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), - ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), - ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), - ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), - ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), - ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), - ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), - ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), - ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), - ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), - ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), - ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), - ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), - ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), - ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), - ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), - ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), - ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), - ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), - ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), - ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), - ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), - ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), - ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), - ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), - ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), - ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), - ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), - ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), - ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), - ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), - ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), - ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), - - // from Game1.ResetToolSpriteSheet - ["TileSheets\\tools"] = (content, key) => Game1.ResetToolSpriteSheet(), - - // from Bush - ["TileSheets\\bushes"] = (content, key) => Bush.texture = content.Load(key), - - // from Critter - ["TileSheets\\critters"] = (content, key) => Critter.critterTexture = content.Load(key), - - // from Farm - ["Buildings\\houses"] = (content, key) => - { - Farm farm = Game1.getFarm(); - if (farm != null) - farm.houseTextures = content.Load(key); - }, - - // from Farmer - ["Characters\\Farmer\\farmer_base"] = (content, key) => - { - if (Game1.player != null && Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - }, - ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => - { - if (Game1.player != null && !Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - }, - - // from Flooring - ["TerrainFeatures\\Flooring"] = (content, key) => Flooring.floorsTexture = content.Load(key), - - // from FruitTree - ["TileSheets\\fruitTrees"] = (content, key) => FruitTree.texture = content.Load(key), - - // from HoeDirt - ["TerrainFeatures\\hoeDirt"] = (content, key) => HoeDirt.lightTexture = content.Load(key), - ["TerrainFeatures\\hoeDirtDark"] = (content, key) => HoeDirt.darkTexture = content.Load(key), - ["TerrainFeatures\\hoeDirtSnow"] = (content, key) => HoeDirt.snowTexture = content.Load(key), - - // from Wallpaper - ["Maps\\walls_and_floors"] = (content, key) => Wallpaper.wallpaperTexture = content.Load(key) - } - .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value); - } - - /// Reload one of the game's core assets (if applicable). - /// The content manager through which to reload the asset. - /// The asset key to reload. - /// Returns whether an asset was reloaded. - public bool ReloadForKey(SContentManager content, string key) - { - // static assets - if (this.SingletonSetters.TryGetValue(key, out Action reload)) - { - reload(content, key); - return true; - } - - // building textures - if (key.StartsWith(this.GetNormalisedPath("Buildings\\"))) - { - Building[] buildings = this.GetAllBuildings().Where(p => key == this.GetNormalisedPath($"Buildings\\{p.buildingType}")).ToArray(); - if (buildings.Any()) - { - Texture2D texture = content.Load(key); - foreach (Building building in buildings) - building.texture = texture; - return true; - } - return false; - } - - return false; - } - - - /********* - ** Private methods - *********/ - /// Get all player-constructed buildings in the world. - private IEnumerable GetAllBuildings() - { - return Game1.locations - .OfType() - .SelectMany(p => p.buildings); - } - } -} diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs deleted file mode 100644 index 3346f1ac..00000000 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.AssemblyRewriters; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModLoading.Finders; -using StardewModdingAPI.Framework.ModLoading.Rewriters; -using StardewValley; - -namespace StardewModdingAPI.Metadata -{ - /// Provides CIL instruction handlers which rewrite mods for compatibility and throw exceptions for incompatible code. - internal class InstructionMetadata - { - /********* - ** Public methods - *********/ - /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. - public IEnumerable GetHandlers() - { - return new IInstructionHandler[] - { - /**** - ** throw exception for incompatible code - ****/ - // changes in Stardew Valley 1.2 (with no rewriters) - new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible), - - // APIs removed in SMAPI 1.9 - new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Extensions", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Inheritance.SGame", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Inheritance.SObject", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.LogWriter", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Manifest", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Version", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), - - // APIs removed in SMAPI 2.0 - new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay", InstructionHandleResult.NotCompatible), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), - - /**** - ** detect code which may impact game stability - ****/ - new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch), - new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic), - new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), - new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), - new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), - - /**** - ** rewrite CIL to fix incompatible code - ****/ - // crossplatform - new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), - - // Stardew Valley 1.2 - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), - new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), - new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), - - // SMAPI 1.9 - new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), - - // SMAPI 2.0 - new VirtualEntryCallRemover() // Mod.Entry changed from virtual to abstract in SMAPI 2.0, which breaks the few mods which called base.Entry() - }; - } - } -} diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs deleted file mode 100644 index ee75ba54..00000000 --- a/src/StardewModdingAPI/Mod.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// The base class for a mod. - public abstract class Mod : IMod, IDisposable - { - /********* - ** Accessors - *********/ - /// Provides simplified APIs for writing mods. - public IModHelper Helper { get; internal set; } - - /// Writes messages to the console and log file. - public IMonitor Monitor { get; internal set; } - - /// The mod's manifest. - public IManifest ModManifest { get; internal set; } - - - /********* - ** Public methods - *********/ - /// The mod entry point, called after the mod is first loaded. - /// Provides simplified APIs for writing mods. - public abstract void Entry(IModHelper helper); - - /// Release or reset unmanaged resources. - public void Dispose() - { - (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it - this.Dispose(true); - GC.SuppressFinalize(this); - } - - - /********* - ** Private methods - *********/ - /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. - /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. - protected virtual void Dispose(bool disposing) { } - - /// Destruct the instance. - ~Mod() - { - this.Dispose(false); - } - } -} diff --git a/src/StardewModdingAPI/PatchMode.cs b/src/StardewModdingAPI/PatchMode.cs deleted file mode 100644 index b4286a89..00000000 --- a/src/StardewModdingAPI/PatchMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI -{ - /// Indicates how an image should be patched. - public enum PatchMode - { - /// Erase the original content within the area before drawing the new content. - Replace, - - /// Draw the new content over the original content, so the original content shows through any transparent pixels. - Overlay - } -} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs deleted file mode 100644 index 7dfdc745..00000000 --- a/src/StardewModdingAPI/Program.cs +++ /dev/null @@ -1,966 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Security; -using System.Threading; -#if SMAPI_FOR_WINDOWS -using System.Management; -using System.Windows.Forms; -#endif -using Newtonsoft.Json; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Logging; -using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Framework.ModHelpers; -using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Models; -using StardewValley; -using Monitor = StardewModdingAPI.Framework.Monitor; -using SObject = StardewValley.Object; - -namespace StardewModdingAPI -{ - /// The main entry point for SMAPI, responsible for hooking into and launching the game. - internal class Program : IDisposable - { - /********* - ** Properties - *********/ - /// The log file to which to write messages. - private readonly LogFileManager LogFile; - - /// Manages console output interception. - private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); - - /// The core logger and monitor for SMAPI. - private readonly Monitor Monitor; - - /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. - private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); - - /// Simplifies access to private game code. - private readonly Reflector Reflection = new Reflector(); - - /// The underlying game instance. - private SGame GameInstance; - - /// The underlying content manager. - private SContentManager ContentManager => this.GameInstance.SContentManager; - - /// The SMAPI configuration settings. - /// This is initialised after the game starts. - private SConfig Settings; - - /// Tracks the installed mods. - /// This is initialised after the game starts. - private ModRegistry ModRegistry; - - /// Manages deprecation warnings. - /// This is initialised after the game starts. - private DeprecationManager DeprecationManager; - - /// Manages console commands. - /// This is initialised after the game starts. - private CommandManager CommandManager; - - /// Whether the game is currently running. - private bool IsGameRunning; - - /// Whether the program has been disposed. - private bool IsDisposed; - - - /********* - ** Public methods - *********/ - /// The main entry point which hooks into and launches the game. - /// The command-line arguments. - public static void Main(string[] args) - { - Program.AssertMinimumCompatibility(); - - // get flags from arguments - bool writeToConsole = !args.Contains("--no-terminal"); - - // get log path from arguments - string logPath = null; - { - int pathIndex = Array.LastIndexOf(args, "--log-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - { - logPath = args[pathIndex]; - if (!Path.IsPathRooted(logPath)) - logPath = Path.Combine(Constants.LogDir, logPath); - } - } - if (string.IsNullOrWhiteSpace(logPath)) - logPath = Constants.DefaultLogPath; - - // load SMAPI - using (Program program = new Program(writeToConsole, logPath)) - program.RunInteractively(); - } - - /// Construct an instance. - /// Whether to output log messages to the console. - /// The full file path to which to write log messages. - public Program(bool writeToConsole, string logPath) - { - this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; - } - - /// Launch SMAPI. - [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions - public void RunInteractively() - { - // initialise SMAPI - try - { - // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); - this.Monitor.Log($"Mods go here: {Constants.ModPath}"); - this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); - - // validate paths - this.VerifyPath(Constants.ModPath); - this.VerifyPath(Constants.LogDir); - - // validate game version - if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) - { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) - { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - - // add error handlers -#if SMAPI_FOR_WINDOWS - Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); -#endif - AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - - // override game - this.GameInstance = new SGame(this.Monitor, this.Reflection); - StardewValley.Program.gamePtr = this.GameInstance; - - // add exit handler - new Thread(() => - { - this.CancellationTokenSource.Token.WaitHandle.WaitOne(); - if (this.IsGameRunning) - { - try - { - File.WriteAllText(Constants.FatalCrashMarker, string.Empty); - File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); - } - - this.GameInstance.Exit(); - } - }).Start(); - - // hook into game events -#if SMAPI_FOR_WINDOWS - ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); -#endif - this.GameInstance.Exiting += (sender, e) => this.Dispose(); - GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); - ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); - - // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - - // show details if game crashed during last session - if (File.Exists(Constants.FatalCrashMarker)) - { - this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: http://community.playstarbound.com/threads/108375/.", LogLevel.Error); - this.Monitor.Log($"If you ask for help, make sure to attach this file: {Constants.FatalCrashLog}", LogLevel.Error); - this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); - Console.ReadKey(); - File.Delete(Constants.FatalCrashLog); - File.Delete(Constants.FatalCrashMarker); - } - - // start game - this.Monitor.Log("Starting game...", LogLevel.Trace); - try - { - this.IsGameRunning = true; - this.GameInstance.Run(); - } - catch (Exception ex) - { - this.Monitor.Log($"The game failed unexpectedly: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); - } - finally - { - this.Dispose(); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - this.Monitor.Log("Disposing...", LogLevel.Trace); - - // skip if already disposed - if (this.IsDisposed) - return; - this.IsDisposed = true; - - // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetMods()) - { - try - { - (mod.Mod as IDisposable)?.Dispose(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {mod.DisplayName} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); - } - } - - // dispose core components - this.IsGameRunning = false; - this.LogFile?.Dispose(); - this.ConsoleManager?.Dispose(); - this.CancellationTokenSource?.Dispose(); - this.GameInstance?.Dispose(); - } - - - /********* - ** Private methods - *********/ - /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. - private static void AssertMinimumCompatibility() - { - void PrintErrorAndExit(string message) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(message); - Console.ResetColor(); - Program.PressAnyKeyToExit(showMessage: true); - } - - // get game assembly name - const string gameAssemblyName = -#if SMAPI_FOR_WINDOWS - "Stardew Valley"; -#else - "StardewValley"; -#endif - - // game not present - if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null) - { - PrintErrorAndExit( - "Oops! SMAPI can't find the game. " - + (Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Windows")) || Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Mono")) - ? "It looks like you're running SMAPI from the download package, but you need to run the installed version instead. " - : "Make sure you're running StardewModdingAPI.exe in your game folder. " - ) - + "See the readme.txt file for details." - ); - } - - // Stardew Valley 1.2 types not present - if (Type.GetType($"StardewValley.LocalizedContentManager+LanguageCode, {gameAssemblyName}", throwOnError: false) == null) - { - PrintErrorAndExit(Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion) - ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." - : "Oops! SMAPI doesn't seem to be compatible with your game. Make sure you're running the latest version of Stardew Valley and SMAPI." - ); - } - } - - /// Initialise SMAPI and mods after the game starts. - private void InitialiseAfterGameStart() - { - // load settings - this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); - this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; - - // load core components - this.ModRegistry = new ModRegistry(); - this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); - this.CommandManager = new CommandManager(); - - // redirect direct console output - { - Monitor monitor = this.GetSecondaryMonitor("Console.Out"); - if (monitor.WriteToConsole) - this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message); - } - - // add headers - if (this.Settings.DeveloperMode) - { - this.Monitor.ShowTraceInConsole = true; - this.Monitor.ShowFullStampInConsole = true; - this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - } - if (!this.Settings.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); - 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.VerboseLog("Verbose logging enabled."); - - // validate XNB integrity - if (!this.ValidateContentIntegrity()) - this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); - - // load mods - { - this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); - ModResolver resolver = new ModResolver(); - - // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); - - // process dependencies - mods = resolver.ProcessDependencies(mods).ToArray(); - - // load mods - this.LoadMods(mods, new JsonHelper(), this.ContentManager); - - // check for updates - this.CheckForUpdatesAsync(mods); - } - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); - return; - } - - // update window titles - int modsLoaded = this.ModRegistry.GetMods().Count(); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; - - // start SMAPI console - new Thread(this.RunConsoleLoop).Start(); - } - - /// Handle the game changing locale. - private void OnLocaleChanged() - { - // get locale - string locale = this.ContentManager.GetLocale(); - LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); - - // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetMods()) - (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); - } - - /// Run a loop handling console input. - [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] - private void RunConsoleLoop() - { - // prepare console - this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); - this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); - - // start handling command line input - Thread inputThread = new Thread(() => - { - while (true) - { - // get input - string input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) - continue; - - // parse input - try - { - if (!this.CommandManager.Trigger(input)) - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); - } - catch (Exception ex) - { - this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - }); - inputThread.Start(); - - // keep console thread alive while the game is running - while (this.IsGameRunning && !this.Monitor.IsExiting) - Thread.Sleep(1000 / 10); - if (inputThread.ThreadState == ThreadState.Running) - inputThread.Abort(); - } - - /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. - /// Returns whether all integrity checks passed. - private bool ValidateContentIntegrity() - { - this.Monitor.Log("Detecting common issues...", LogLevel.Trace); - bool issuesFound = false; - - // object format (commonly broken by outdated files) - { - // detect issues - bool hasObjectIssues = false; - void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace); - foreach (KeyValuePair entry in Game1.objectInformation) - { - // must not be empty - if (string.IsNullOrWhiteSpace(entry.Value)) - { - LogIssue(entry.Key, "entry is empty"); - hasObjectIssues = true; - continue; - } - - // require core fields - string[] fields = entry.Value.Split('/'); - if (fields.Length < SObject.objectInfoDescriptionIndex + 1) - { - LogIssue(entry.Key, "too few fields for an object"); - hasObjectIssues = true; - continue; - } - - // check min length for specific types - switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) - { - case "Cooking": - if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) - { - LogIssue(entry.Key, "too few fields for a cooking item"); - hasObjectIssues = true; - } - break; - } - } - - // log error - if (hasObjectIssues) - { - issuesFound = true; - this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); - } - } - - return !issuesFound; - } - - /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. - /// The mods to include in the update check (if eligible). - private void CheckForUpdatesAsync(IModMetadata[] mods) - { - if (!this.Settings.CheckForUpdates) - return; - - new Thread(() => - { - // create client - WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); - - // check SMAPI version - try - { - this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); - - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; - if (response.Error != null) - { - this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {response.Error}"); - } - else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) - this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); - else - this.VerboseLog(" OK."); - } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {ex.GetLogSummary()}"); - } - - // check mod versions - try - { - // log issues - if (this.Settings.VerboseLogging) - { - this.VerboseLog("Validating mod update keys..."); - foreach (IModMetadata mod in mods) - { - if (mod.Manifest == null) - this.VerboseLog($" {mod.DisplayName}: no manifest."); - else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) - this.VerboseLog($" {mod.DisplayName}: no update keys."); - } - } - - // prepare update keys - Dictionary modsByKey = - ( - from mod in mods - where mod.Manifest?.UpdateKeys != null - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); - - // fetch results - this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary updatesByMod = new Dictionary(); - foreach (var result in results) - { - IModMetadata mod = result.Mod; - ModInfoModel info = result.Info; - - // handle error - if (info.Error != null) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); - continue; - } - - // track update - ISemanticVersion localVersion = mod.DataRecord != null - ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) - : mod.Manifest.Version; - ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null - ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) - : info.Version - ); - bool isUpdate = latestVersion.IsNewerThan(localVersion); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); - if (isUpdate) - { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) - updatesByMod[mod] = info; - } - } - - // output - if (updatesByMod.Any()) - { - this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); - } - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); - } - }).Start(); - } - - /// Create a directory path if it doesn't exist. - /// The directory path. - private void VerifyPath(string path) - { - try - { - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - /// Load and hook up the given mods. - /// The mods to load. - /// The JSON helper with which to read mods' JSON files. - /// The content manager to use for mod content. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) - { - this.Monitor.Log("Loading mods...", LogLevel.Trace); - - // load mod assemblies - IDictionary skippedMods = new Dictionary(); - { - void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; - - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); - foreach (IModMetadata metadata in mods) - { - // get basic info - IManifest manifest = metadata.Manifest; - string assemblyPath = metadata.Manifest?.EntryDll != null - ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) - : null; - this.Monitor.Log(assemblyPath != null - ? $"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}..." - : $"Loading {metadata.DisplayName}...", LogLevel.Trace); - - // validate status - if (metadata.Status == ModMetadataStatus.Failed) - { - this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); - TrackSkip(metadata, metadata.Error); - continue; - } - - // preprocess & load mod assembly - Assembly modAssembly; - try - { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); - } - catch (IncompatibleInstructionException ex) - { - TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); - continue; - } - catch (SAssemblyLoadFailedException ex) - { - TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}"); - continue; - } - catch (Exception ex) - { - TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); - continue; - } - - // validate assembly - try - { - int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); - if (modEntries == 0) - { - TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); - continue; - } - if (modEntries > 1) - { - TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); - continue; - } - } - catch (Exception ex) - { - TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); - continue; - } - - // initialise mod - try - { - // get implementation - TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); - Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); - if (mod == null) - { - TrackSkip(metadata, "its entry class couldn't be instantiated."); - continue; - } - - // inject data - { - IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); - - mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); - mod.Monitor = monitor; - } - - // track mod - metadata.SetMod(mod); - this.ModRegistry.Add(metadata); - } - catch (Exception ex) - { - TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); - } - } - } - IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); - - // log skipped mods - this.Monitor.Newline(); - if (skippedMods.Any()) - { - this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); - foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) - { - IModMetadata mod = pair.Key; - string reason = pair.Value; - - if (mod.Manifest?.Version != null) - this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); - else - this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); - } - this.Monitor.Newline(); - } - - // log loaded mods - this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); - foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - - // initialise translations - this.ReloadTranslations(); - - // initialise loaded mods - foreach (IModMetadata metadata in loadedMods) - { - // add interceptors - if (metadata.Mod.Helper.Content is ContentHelper helper) - { - this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; - } - - // call entry method - try - { - IMod mod = metadata.Mod; - mod.Entry(mod.Helper); - } - catch (Exception ex) - { - this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - // invalidate cache entries when needed - // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) - foreach (IModMetadata metadata in loadedMods) - { - if (metadata.Mod.Helper.Content is ContentHelper helper) - { - helper.ObservableAssetEditors.CollectionChanged += (sender, e) => - { - if (e.NewItems.Count > 0) - { - this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); - } - }; - helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => - { - if (e.NewItems.Count > 0) - { - this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); - } - }; - } - } - - // reset cache now if any editors or loaders were added during entry - IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); - IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); - if (editors.Any() || loaders.Any()) - { - this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(editors, loaders); - } - } - - /// Reload translations for all mods. - private void ReloadTranslations() - { - JsonHelper jsonHelper = new JsonHelper(); - foreach (IModMetadata metadata in this.ModRegistry.GetMods()) - { - // read translation files - IDictionary> translations = new Dictionary>(); - DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); - if (translationsDir.Exists) - { - foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) - { - string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); - try - { - translations[locale] = jsonHelper.ReadJsonFile>(file.FullName); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); - } - } - } - - // update translation - TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; - translationHelper.SetTranslations(translations); - } - } - - /// The method called when the user submits a core SMAPI command in the console. - /// The command name. - /// The command arguments. - private void HandleCommand(string name, string[] arguments) - { - switch (name) - { - case "help": - if (arguments.Any()) - { - Command result = this.CommandManager.Get(arguments[0]); - if (result == null) - this.Monitor.Log("There's no command with that name.", LogLevel.Error); - else - this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); - } - else - { - string message = "The following commands are registered:\n"; - IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); - foreach (var group in groups) - { - string modName = group.Key; - string[] commandNames = group.ToArray(); - message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; - } - message += "For more information about a command, type 'help command_name'."; - - this.Monitor.Log(message, LogLevel.Info); - } - break; - - case "reload_i18n": - this.ReloadTranslations(); - this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); - break; - - default: - throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); - } - } - - /// Redirect messages logged directly to the console to the given monitor. - /// The monitor with which to log messages. - /// The message to log. - private void HandleConsoleMessage(IMonitor monitor, string message) - { - LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; // intercept potential exceptions - monitor.Log(message, level); - } - - /// Show a 'press any key to exit' message, and exit when they press a key. - private void PressAnyKeyToExit() - { - this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); - Program.PressAnyKeyToExit(showMessage: false); - } - - /// Show a 'press any key to exit' message, and exit when they press a key. - /// Whether to print a 'press any key to exit' message to the console. - private static void PressAnyKeyToExit(bool showMessage) - { - if (showMessage) - Console.WriteLine("Game has ended. Press any key to exit."); - Thread.Sleep(100); - Console.ReadKey(); - Environment.Exit(0); - } - - /// Get a monitor instance derived from SMAPI's current settings. - /// The name of the module which will log messages with this instance. - private Monitor GetSecondaryMonitor(string name) - { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) - { - WriteToConsole = this.Monitor.WriteToConsole, - ShowTraceInConsole = this.Settings.DeveloperMode, - ShowFullStampInConsole = this.Settings.DeveloperMode - }; - } - - /// Get a human-readable name for the current platform. - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] - private string GetFriendlyPlatformName() - { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - return Environment.OSVersion.ToString(); - } - - /// Log a message if verbose mode is enabled. - /// The message to log. - private void VerboseLog(string message) - { - if (this.Settings.VerboseLogging) - this.Monitor.Log(message, LogLevel.Trace); - } - } -} diff --git a/src/StardewModdingAPI/Properties/AssemblyInfo.cs b/src/StardewModdingAPI/Properties/AssemblyInfo.cs deleted file mode 100644 index b0a065f5..00000000 --- a/src/StardewModdingAPI/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Stardew Modding API (SMAPI)")] -[assembly: AssemblyDescription("A modding API for Stardew Valley.")] -[assembly: Guid("5c3f7f42-fefd-43db-aaea-92ea3bcad531")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Tests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs deleted file mode 100644 index 1b99dae6..00000000 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Newtonsoft.Json; - -namespace StardewModdingAPI -{ - /// A semantic version with an optional release tag. - public class SemanticVersion : ISemanticVersion - { - /********* - ** Properties - *********/ - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - private static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - public int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - public int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - public int PatchVersion { get; } - - /// An optional build tag. - public string Build { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible bug fixes. - /// An optional build tag. - [JsonConstructor] - public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - { - this.MajorVersion = majorVersion; - this.MinorVersion = minorVersion; - this.PatchVersion = patchVersion; - this.Build = this.GetNormalisedTag(build); - } - - /// Construct an instance. - /// The semantic version string. - /// The is null. - /// The is not a valid semantic version. - public SemanticVersion(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersion.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.MajorVersion = int.Parse(match.Groups["major"].Value); - this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. - /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). - public int CompareTo(ISemanticVersion other) - { - if (other == null) - throw new ArgumentNullException(nameof(other)); - - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.MajorVersion != other.MajorVersion) - return this.MajorVersion.CompareTo(other.MajorVersion); - if (this.MinorVersion != other.MinorVersion) - return this.MinorVersion.CompareTo(other.MinorVersion); - if (this.PatchVersion != other.PatchVersion) - return this.PatchVersion.CompareTo(other.PatchVersion); - if (this.Build == other.Build) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Build); - bool otherIsStable = string.IsNullOrWhiteSpace(other.Build); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Build.Split('.', '-'); - string[] otherParts = other.Build.Split('.', '-'); - for (int i = 0; i < curParts.Length; i++) - { - // longer prerelease tag supercedes if otherwise equal - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - public bool IsOlderThan(ISemanticVersion other) - { - return this.CompareTo(other) < 0; - } - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - public bool IsOlderThan(string other) - { - return this.IsOlderThan(new SemanticVersion(other)); - } - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - public bool IsNewerThan(ISemanticVersion other) - { - return this.CompareTo(other) > 0; - } - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - public bool IsNewerThan(string other) - { - return this.IsNewerThan(new SemanticVersion(other)); - } - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - public bool IsBetween(ISemanticVersion min, ISemanticVersion max) - { - return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; - } - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - public bool IsBetween(string min, string max) - { - return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); - } - - /// Indicates whether the current object is equal to another object of the same type. - /// true if the current object is equal to the parameter; otherwise, false. - /// An object to compare with this object. - public bool Equals(ISemanticVersion other) - { - return other != null && this.CompareTo(other) == 0; - } - - /// Get a string representation of the version. - public override string ToString() - { - // version - string result = this.PatchVersion != 0 - ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" - : $"{this.MajorVersion}.{this.MinorVersion}"; - - // tag - string tag = this.Build; - if (tag != null) - result += $"-{tag}"; - return result; - } - - /// Parse a version string without throwing an exception if it fails. - /// The version string. - /// The parsed representation. - /// Returns whether parsing the version succeeded. - internal static bool TryParse(string version, out ISemanticVersion parsed) - { - try - { - parsed = new SemanticVersion(version); - return true; - } - catch - { - parsed = null; - return false; - } - } - - - /********* - ** Private methods - *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) - { - tag = tag?.Trim(); - if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation - return null; - return tag; - } - } -} diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json deleted file mode 100644 index ebc1235b..00000000 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ /dev/null @@ -1,2063 +0,0 @@ -/* - - - -This file contains advanced configuration for SMAPI. You generally shouldn't change this file. - - - -*/ -{ - /** - * Whether to enable features intended for mod developers. Currently this only makes TRACE-level - * messages appear in the console. - */ - "DeveloperMode": true, - - /** - * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new - * versions are available, an alert will be shown in the console. This doesn't affect the load - * time even if your connection is offline or slow, because it happens in the background. - */ - "CheckForUpdates": true, - - /** - * SMAPI's GitHub project name, used to perform update checks. - */ - "GitHubProjectName": "Pathoschild/SMAPI", - - /** - * The base URL for SMAPI's web API, used to perform update checks. - * Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the - * game's bundled Mono. - */ - "WebApiBaseUrl": "https://api.smapi.io", - - /** - * Whether SMAPI should log more information about the game context. - */ - "VerboseLogging": false, - - /** - * Extra metadata about some SMAPI mods. All fields except 'ID' are optional. - * - * - 'ID' uniquely identifies the mod across all versions, even if its manifest fields changed or - * the mod doesn't have a unique ID. The format is as follows: - * - If the mod's identifier changed over time, multiple variants are separated by |. - * - Each variant can take one of two forms: a simple string matching the mod's UniqueID, - * or a JSON structure containing any of three manifest fields (ID, Name, and Author) to - * match. - * - * - 'UpdateKeys' specifies the value of the equivalent manifest field if it's not already set. - * This is used to enable update checks for older mods that haven't been updated to use it yet. - * - * - 'AlternativeUrl' specifies a URL where the player can find an unofficial update or - * alternative if the mod is no longer compatible. - * - * - 'Compatibility' overrides SMAPI's normal compatibility detection. The keys are version - * ranges in the form lower~upper, where either side can be blank for an unbounded range. (For - * example, "~1.0" means all versions up to 1.0 inclusively.) The values have two fields: - * - 'Status' specifies the compatibility. Valid values are Obsolete (SMAPI won't load it - * because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will load it even - * if it detects incompatible code). - * - 'ReasonPhrase' (optional) specifies a message to show to the player explaining why the - * mod isn't loaded. This has no effect for AssumeCompatible. - * - * - 'MapLocalVersions' and 'MapRemoteVersions' substitute versions for update checks. For - * example, if the API returns version '1.1-1078', MapRemoteVersions can map it to '1.1' when - * comparing to the mod's current version. This is only intended to support legacy mods with - * injected update keys. - */ - "ModData": [ - { - // AccessChestAnywhere - "ID": "AccessChestAnywhere", - "UpdateKeys": [ "Nexus:257" ], - "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 - }, - "MapLocalVersions": { - "1.1-1078": "1.1" - } - }, - { - // AdjustArtisanPrices - "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", - "UpdateKeys": [ "Chucklefish:3532" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 - } - }, - { - // Adjust Monster - "ID": "mmanlapat.AdjustMonster", - "UpdateKeys": [ "Nexus:1161" ] - }, - { - // Advanced Location Loader - "ID": "Entoarox.AdvancedLocationLoader", - //"UpdateKeys": [ "Chucklefish:3619" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Adventure Shop Inventory - "ID": "HammurabiAdventureShopInventory", - "UpdateKeys": [ "Chucklefish:4608" ] - }, - { - // AgingMod - "ID": "skn.AgingMod", - "UpdateKeys": [ "Nexus:1129" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // All Crops All Seasons - "ID": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 - "UpdateKeys": [ "Nexus:170" ] - }, - { - // All Professions - "ID": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 - "UpdateKeys": [ "Nexus:174" ] - }, - { - // Almighty Tool - "ID": "AlmightyTool.dll | 439", // changed in 1.2.1 - "UpdateKeys": [ "Nexus:439" ], - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapRemoteVersions": { - "1.21": "1.2.1" - } - }, - { - // Animal Mood Fix - "ID": "GPeters-AnimalMoodFix", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - } - } - }, - { - // Animal Sitter - "ID": "AnimalSitter.dll | jwdred.AnimalSitter", // changed in 1.0.9 - "UpdateKeys": [ "Nexus:581" ], - "Compatibility": { - "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // A Tapper's Dream - "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "UpdateKeys": [ "Nexus:260" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Auto Animal Doors - "ID": "AaronTaggart.AutoAnimalDoors", - "UpdateKeys": [ "Nexus:1019" ], - "MapRemoteVersions": { - "1.1.1": "1.1" // manifest not updated - } - }, - { - // Auto-Eat - "ID": "BALANCEMOD_AutoEat | Permamiss.AutoEat", // changed in 1.1.1 - "UpdateKeys": [ "Nexus:643" ] - }, - { - // AutoGate - "ID": "AutoGate", - "UpdateKeys": [ "Nexus:820" ] - }, - { - // Automate - "ID": "Pathoschild.Automate", - "UpdateKeys": [ "Nexus:1063" ] - }, - { - // Automated Doors - "ID": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b | azah.automated-doors", // changed in 1.4.1 - "UpdateKeys": [ "GitHub:azah/AutomatedDoors" ], - "MapLocalVersions": { - "1.4.1-1": "1.4.1" - } - }, - { - // AutoSpeed - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'} | Omegasis.AutoSpeed", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:443" ] - }, - { - // Basic Sprinkler Improved - "ID": "lrsk_sdvm_bsi.0117171308", - "UpdateKeys": [ "Nexus:833" ], - "MapRemoteVersions": { - "1.0.2": "1.0.1-release" // manifest not updated - } - }, - { - // Better Hay - "ID": "cat.betterhay", - "UpdateKeys": [ "Nexus:1430" ] - }, - { - // Better Quality More Seasons - "ID": "SB_BQMS", - "UpdateKeys": [ "Nexus:935" ] - }, - { - // Better Quarry - "ID": "BetterQuarry", - "UpdateKeys": [ "Nexus:771" ] - }, - { - // Better Ranching - "ID": "BetterRanching", - "UpdateKeys": [ "Nexus:859" ] - }, - { - // Better Shipping Box - "ID": "Kithio:BetterShippingBox", - "UpdateKeys": [ "Chucklefish:4302" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.0.1": "1.0.2" - } - }, - { - // Better Sprinklers - "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 - "UpdateKeys": [ "Nexus:41" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Billboard Anywhere - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'} | Omegasis.BillboardAnywhere", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:492" ] - }, - { - // Birthday Mail - "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d | KathrynHazuka.BirthdayMail", // changed in 1.2.3-pathoschild-update - "UpdateKeys": [ "Nexus:276" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Breed Like Rabbits - "ID": "dycedarger.breedlikerabbits", - "UpdateKeys": [ "Nexus:948" ] - }, - { - // Build Endurance - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'} | Omegasis.BuildEndurance", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:445" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Build Health - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'} | Omegasis.BuildHealth", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:446" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Buy Cooking Recipes - "ID": "Denifia.BuyRecipes", - "UpdateKeys": [ "Nexus:1126" ], // added in 1.0.1 (2017-10-04) - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Buy Back Collectables - "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 - "UpdateKeys": [ "Nexus:507" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Carry Chest - "ID": "spacechase0.CarryChest", - "UpdateKeys": [ "Nexus:1333" ] - }, - { - // Casks Anywhere - "ID": "CasksAnywhere", - "UpdateKeys": [ "Nexus:878" ], - "MapLocalVersions": { - "1.1-alpha": "1.1" - } - }, - { - // Categorize Chests - "ID": "CategorizeChests", - "UpdateKeys": [ "Nexus:1300" ] - }, - { - // ChefsCloset - "ID": "Duder.ChefsCloset", - "UpdateKeys": [ "Nexus:1030" ], - "MapLocalVersions": { - "1.3-1": "1.3" - } - }, - { - // Chest Label System - "ID": "SPDChestLabel | Speeder.ChestLabel", // changed in 1.5.1-pathoschild-update - "UpdateKeys": [ "Nexus:242" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // Chest Pooling - "ID": "ChestPooling.dll | mralbobo.ChestPooling", // changed in 1.3 - "UpdateKeys": [ "GitHub:mralbobo/stardew-chest-pooling" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Chests Anywhere - "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 - "UpdateKeys": [ "Nexus:518" ], - "Compatibility": { - "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Choose Baby Gender - "ID": "ChooseBabyGender.dll", - "UpdateKeys": [ "Nexus:590" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // CJB Automation - "ID": "CJBAutomation", - "UpdateKeys": [ "Nexus:211" ], - "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // CJB Cheats Menu - "ID": "CJBCheatsMenu | CJBok.CheatsMenu", // changed in 1.14 - "UpdateKeys": [ "Nexus:4" ], - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // CJB Item Spawner - "ID": "CJBItemSpawner | CJBok.ItemSpawner", // changed in 1.7 - "UpdateKeys": [ "Nexus:93" ], - "Compatibility": { - "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // CJB Show Item Sell Price - "ID": "CJBShowItemSellPrice | CJBok.ShowItemSellPrice", // changed in 1.7 - "UpdateKeys": [ "Nexus:5" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Clean Farm - "ID": "tstaples.CleanFarm", - "UpdateKeys": [ "Nexus:794" ] - }, - { - // Climates of Ferngill - "ID": "KoihimeNakamura.ClimatesOfFerngill", - "UpdateKeys": [ "Nexus:604" ], - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Cold Weather Haley - "ID": "LordXamon.ColdWeatherHaleyPRO", - "UpdateKeys": [ "Nexus:1169" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Colored Chests - "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." - } - } - }, - { - // Combat with Farm Implements - "ID": "SPDFarmingImplementsInCombat", - "UpdateKeys": [ "Nexus:313" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Community Bundle Item Tooltip - "ID": "musbah.bundleTooltip", - "UpdateKeys": [ "Nexus:1329" ] - }, - { - // Concentration on Farming - "ID": "punyo.ConcentrationOnFarming", - "UpdateKeys": [ "Nexus:1445" ] - }, - { - // Configurable Machines - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "UpdateKeys": [ "Nexus:280" ], - "MapLocalVersions": { - "1.2-beta": "1.2" - } - }, - { - // Configurable Shipping Dates - "ID": "ConfigurableShippingDates", - "UpdateKeys": [ "Nexus:675" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Cooking Skill - "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 - "UpdateKeys": [ "Nexus:522" ], - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // CrabNet - "ID": "CrabNet.dll | jwdred.CrabNet", // changed in 1.0.5 - "UpdateKeys": [ "Nexus:584" ], - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Current Location - "ID": "CurrentLocation102120161203", - "UpdateKeys": [ "Nexus:638" ] - }, - { - // Custom Critters - "ID": "spacechase0.CustomCritters", - "UpdateKeys": [ "Nexus:1255" ] - }, - { - // Custom Element Handler - "ID": "Platonymous.CustomElementHandler", - "UpdateKeys": [ "Nexus:1068" ] // added in 1.3.1 - }, - { - // Custom Farming - "ID": "Platonymous.CustomFarming", - "UpdateKeys": [ "Nexus:991" ] // added in 0.6.1 - }, - { - // Custom Farming Automate Bridge - "ID": "Platonymous.CFAutomate", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // no longer compatible with Automate - } - }, - { - // Custom Farm Types - "ID": "spacechase0.CustomFarmTypes", - "UpdateKeys": [ "Nexus:1140" ] - }, - { - // Custom Furniture - "ID": "Platonymous.CustomFurniture", - "UpdateKeys": [ "Nexus:1254" ] // added in 0.4.1 - }, - { - // Customize Exterior - "ID": "CustomizeExterior | spacechase0.CustomizeExterior", // changed in 1.0.3 - "UpdateKeys": [ "Nexus:1099" ], - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Customizable Cart Redux - "ID": "KoihimeNakamura.CCR", - "UpdateKeys": [ "Nexus:1402" ], - "MapLocalVersions": { - "1.1-20170917": "1.1" - } - }, - { - // Customizable Traveling Cart Days - "ID": "TravelingCartYyeahdude", - "UpdateKeys": [ "Nexus:567" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Custom Linens - "ID": "Mevima.CustomLinens", - "UpdateKeys": [ "Nexus:1027" ], - "MapRemoteVersions": { - "1.1": "1.0" // manifest not updated - } - }, - { - // Custom Shops Redux - "ID": "Omegasis.CustomShopReduxGui", - "UpdateKeys": [ "Nexus:1378" ] - }, - { - // Custom TV - "ID": "Platonymous.CustomTV", - "UpdateKeys": [ "Nexus:1139" ] // added in 1.0.6 - }, - { - // Daily Luck Message - "ID": "Schematix.DailyLuckMessage", - "UpdateKeys": [ "Nexus:1327" ] - }, - { - // Daily News - "ID": "bashNinja.DailyNews", - "UpdateKeys": [ "Nexus:1141" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Daily Quest Anywhere - "ID": "DailyQuest | Omegasis.DailyQuestAnywhere", // changed in 1.4 - "UpdateKeys": [ "Nexus:513" ] - }, - { - // Debug Mode - "ID": "Pathoschild.Stardew.DebugMode | Pathoschild.DebugMode", // changed in 1.4 - "UpdateKeys": [ "Nexus:679" ] - }, - { - // Dynamic Checklist - "ID": "gunnargolf.DynamicChecklist", - "UpdateKeys": [ "Nexus:1145" ], // added in 1.0.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Dynamic Horses - "ID": "Bpendragon-DynamicHorses", - "UpdateKeys": [ "Nexus:874" ], - "MapRemoteVersions": { - "1.2": "1.1-release" // manifest not updated - } - }, - { - // Dynamic Machines - "ID": "DynamicMachines", - "UpdateKeys": [ "Nexus:374" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.1": "1.1.1" - } - }, - { - // Dynamic NPC Sprites - "ID": "BashNinja.DynamicNPCSprites", - "UpdateKeys": [ "Nexus:1183" ] - }, - { - // Easier Farming - "ID": "cautiouswafffle.EasierFarming", - "UpdateKeys": [ "Nexus:1426" ] - }, - { - // Empty Hands - "ID": "QuicksilverFox.EmptyHands", - "UpdateKeys": [ "Nexus:1176" ], // added in 1.0.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Enemy Health Bars - "ID": "SPDHealthBar | Speeder.HealthBars", // changed in 1.7.1-pathoschild-update - "UpdateKeys": [ "Nexus:193" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Entoarox Framework - "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - //"UpdateKeys": [ "Chucklefish:4228" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Expanded Fridge / Dynamic Expanded Fridge - "ID": "Uwazouri.ExpandedFridge", - "UpdateKeys": [ "Nexus:1191" ] - }, - { - // Experience Bars - "ID": "ExperienceBars | spacechase0.ExperienceBars", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:509" ] - }, - { - // Extended Bus System - "ID": "ExtendedBusSystem", - "UpdateKeys": [ "Chucklefish:4373" ] - }, - { - // Extended Fridge - "ID": "Mystra007ExtendedFridge | Crystalmir.ExtendedFridge", // changed in 1.0.1 - "UpdateKeys": [ "Nexus:485" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Extended Greenhouse - "ID": "ExtendedGreenhouse", - "UpdateKeys": [ "Chucklefish:4303" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Extended Minecart - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 - //"UpdateKeys": [ "Chucklefish:4359" ] // Entoarox opted out of mod update checks - }, - { - // Extended Reach - "ID": "spacechase0.ExtendedReach", - "UpdateKeys": [ "Nexus:1493" ] - }, - { - // Fall 28 Snow Day - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:486" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Automation: Barn Door Automation - "ID": "FarmAutomation.BarnDoorAutomation.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Automation: Item Collector - "ID": "FarmAutomation.ItemCollector.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Farm Automation Unofficial: Item Collector - "ID": "Maddy99.FarmAutomation.ItemCollector", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Expansion - "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5 | Advize.FarmExpansion", // changed in 2.0, 2.0.5, and 3.0 - "UpdateKeys": [ "Nexus:130" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Resource Generator - "ID": "FarmResourceGenerator.dll", - "UpdateKeys": [ "Nexus:647" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Fast Animations - "ID": "Pathoschild.FastAnimations", - "UpdateKeys": [ "Nexus:1089" ] - }, - { - // Faster Paths - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths" // changed in 1.2 and 1.3; disambiguate from Shop Expander - // "UpdateKeys": [ "Chucklefish:3641" ] // Entoarox opted out of mod update checks - }, - { - // Faster Run - "ID": "FasterRun.dll | KathrynHazuka.FasterRun", // changed in 1.1.1-pathoschild-update - "UpdateKeys": [ "Nexus:733" ], // added in 1.1.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Fishing Adjust - "ID": "shuaiz.FishingAdjustMod", - "UpdateKeys": [ "Nexus:1350" ] - }, - { - // Fishing Tuner Redux - "ID": "HammurabiFishingTunerRedux", - "UpdateKeys": [ "Chucklefish:4578" ] - }, - { - // FlorenceMod - "ID": "FlorenceMod.dll", - "UpdateKeys": [ "Nexus:591" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.0.1": "1.1" - } - }, - { - // Flower Color Picker - "ID": "spacechase0.FlowerColorPicker", - "UpdateKeys": [ "Nexus:1229" ] - }, - { - // Forage at the Farm - "ID": "ForageAtTheFarm", - "UpdateKeys": [ "Nexus:673" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Furniture Anywhere - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere" // changed in 1.1; disambiguate from Extended Minecart - // "UpdateKeys": [ "Chucklefish:4324" ] // Entoarox opted out of mod update checks - }, - { - // Game Reminder - "ID": "mmanlapat.GameReminder", - "UpdateKeys": [ "Nexus:1153" ] - }, - { - // Gate Opener - "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 - "UpdateKeys": [ "GitHub:mralbobo/stardew-gate-opener" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // GenericShopExtender - "ID": "GenericShopExtender", - "UpdateKeys": [ "Nexus:814" ], // added in 0.1.3 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Geode Info Menu - "ID": "cat.geodeinfomenu", - "UpdateKeys": [ "Nexus:1448" ] - }, - { - // Get Dressed - "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 - "UpdateKeys": [ "Nexus:331" ], - "Compatibility": { - "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Giant Crop Ring - "ID": "cat.giantcropring", - "UpdateKeys": [ "Nexus:1182" ] - }, - { - // Gift Taste Helper - "ID": "8008db57-fa67-4730-978e-34b37ef191d6 | tstaples.GiftTasteHelper", // changed in 2.5 - "UpdateKeys": [ "Nexus:229" ], - "Compatibility": { - "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Grandfather's Gift - "ID": "ShadowDragon.GrandfathersGift", - "UpdateKeys": [ "Nexus:985" ] - }, - { - // Happy Animals - "ID": "HappyAnimals", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Happy Birthday (Omegasis) - "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'} | Omegasis.HappyBirthday", // changed in 1.4; disambiguate from Oxyligen's fork - "UpdateKeys": [ "Nexus:520" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Happy Birthday (Oxyligen fork) - "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "UpdateKeys": [ "Nexus:1064" ] - }, - { - // Harp of Yoba Redux - "ID": "Platonymous.HarpOfYobaRedux", - "UpdateKeys": [ "Nexus:914" ] // added in 2.0.3 - }, - { - // Harvest Moon Witch Princess - "ID": "Sasara.WitchPrincess", - "UpdateKeys": [ "Nexus:1157" ] - }, - { - // Harvest With Scythe - "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "UpdateKeys": [ "Nexus:236" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Horse Whistle (icepuente) - "ID": "icepuente.HorseWhistle", - "UpdateKeys": [ "Nexus:1131" ] - }, - { - // Hunger (Yyeadude) - "ID": "HungerYyeadude", - "UpdateKeys": [ "Nexus:613" ] - }, - { - // Hunger for Food (Tigerle) - "ID": "HungerForFoodByTigerle", - "UpdateKeys": [ "Nexus:810" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Hunger Mod (skn) - "ID": "skn.HungerMod", - "UpdateKeys": [ "Nexus:1127" ], - "MapRemoteVersions": { - "1.2.1": "1.0" // manifest not updated - } - }, - { - // Idle Pause - "ID": "Veleek.IdlePause", - "UpdateKeys": [ "Nexus:1092" ], - "MapRemoteVersions": { - "1.2": "1.1" // manifest not updated - } - }, - { - // Improved Quality of Life - "ID": "Demiacle.ImprovedQualityOfLife", - "UpdateKeys": [ "Nexus:1025" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Instant Geode - "ID": "InstantGeode", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Instant Grow Trees - "ID": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 - "UpdateKeys": [ "Nexus:173" ] - }, - { - // Interaction Helper - "ID": "HammurabiInteractionHelper", - "UpdateKeys": [ "Chucklefish:4640" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Item Auto Stacker - "ID": "cat.autostacker", - "UpdateKeys": [ "Nexus:1184" ], - "MapRemoteVersions": { - "1.0.1": "1.0" // manifest not updated - } - }, - { - // Junimo Farm - "ID": "Platonymous.JunimoFarm", - "UpdateKeys": [ "Nexus:984" ], // added in 1.1.3 - "MapRemoteVersions": { - "1.1.2": "1.1.1" // manifest not updated - } - }, - { - // Less Strict Over-Exertion (AntiExhaustion) - "ID": "BALANCEMOD_AntiExhaustion", - "UpdateKeys": [ "Nexus:637" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.0": "1.1" - } - }, - { - // Level Extender - "ID": "Devin Lematty.Level Extender", - "UpdateKeys": [ "Nexus:1471" ], - "MapRemoteVersions": { - "1.1": "1.0" // manifest not updated - } - }, - { - // Level Up Notifications - "ID": "Level Up Notifications", - "UpdateKeys": [ "Nexus:855" ] - }, - { - // Location and Music Logging - "ID": "Brandy Lover.LMlog", - "UpdateKeys": [ "Nexus:1366" ] - }, - { - // Longevity - "ID": "RTGOAT.Longevity", - "UpdateKeys": [ "Nexus:649" ] - }, - { - // Lookup Anything - "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 - "UpdateKeys": [ "Nexus:541" ], - "Compatibility": { - "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Love Bubbles - "ID": "LoveBubbles", - "UpdateKeys": [ "Nexus:1318" ] - }, - { - // Loved Labels - "ID": "LovedLabels.dll", - "UpdateKeys": [ "Nexus:279" ], - "Compatibility": { - "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Luck Skill - "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 - "UpdateKeys": [ "Nexus:521" ], - "Compatibility": { - "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // MailOrderPigs - "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:632" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Makeshift Multiplayer - "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 - "UpdateKeys": [ "Nexus:501" ], - "Compatibility": { - "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Map Image Exporter - "ID": "MapImageExporter | spacechase0.MapImageExporter", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:1073" ] - }, - { - // Message Box [API]? (ChatMod) - "ID": "Kithio:ChatMod", - "UpdateKeys": [ "Chucklefish:4296" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Mining at the Farm - "ID": "MiningAtTheFarm", - "UpdateKeys": [ "Nexus:674" ] - }, - { - // Mining With Explosives - "ID": "MiningWithExplosives", - "UpdateKeys": [ "Nexus:770" ] - }, - { - // Modder Serialization Utility - "ID": "SerializerUtils-0-1", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." - } - } - }, - { - // More Artifact Spots - "ID": "451", - "UpdateKeys": [ "Nexus:451" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // More Map Layers - "ID": "Platonymous.MoreMapLayers", - "UpdateKeys": [ "Nexus:1134" ] // added in 1.1.1 - }, - { - // More Pets - "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - // "UpdateKeys": [ "Chucklefish:4288" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility - } - }, - { - // More Rain - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:441" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // More Weapons - "ID": "Joco80.MoreWeapons", - "UpdateKeys": [ "Nexus:1168" ] - }, - { - // Move Faster - "ID": "shuaiz.MoveFasterMod", - "UpdateKeys": [ "Nexus:1351" ] - }, - { - // Multiple Sprites and Portraits On Rotation (File Loading) - "ID": "FileLoading", - "UpdateKeys": [ "Nexus:1094" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.1": "1.12" - } - }, - { - // Museum Rearranger - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'} | Omegasis.MuseumRearranger", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:428" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // New Machines - "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "UpdateKeys": [ "Chucklefish:3683" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Night Owl - "UpdateKeys": [ "Nexus:433" ], - "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "2.1": "1.3" // 1.3 had wrong version in manifest - } - }, - { - // No Kids Ever - "ID": "Hangy.NoKidsEver", - "UpdateKeys": [ "Nexus:1464" ] - }, - { - // No Debug Mode - "ID": "NoDebugMode", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "debug mode was removed in SMAPI 1.0." - } - } - }, - { - // No Fence Decay - "ID": "cat.nofencedecay", - "UpdateKeys": [ "Nexus:1180" ] - }, - { - // No More Pets - "ID": "NoMorePets | Omegasis.NoMorePets", // changed in 1.4 - "UpdateKeys": [ "Nexus:506" ] - }, - { - // NoSoilDecay - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "UpdateKeys": [ "Nexus:237" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location - } - }, - { - // No Soil Decay Redux - "ID": "Platonymous.NoSoilDecayRedux", - "UpdateKeys": [ "Nexus:1084" ] // added in 1.1.9 - }, - { - // NPC Map Locations - "ID": "NPCMapLocationsMod", - "UpdateKeys": [ "Nexus:239" ], - "Compatibility": { - "1.42~1.43": { - "Status": "AssumeBroken", - "ReasonPhrase": "this version has an update check error which crashes the game." - } - } - }, - { - // NPC Speak - "ID": "NpcEcho.dll", - "UpdateKeys": [ "Nexus:694" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Object Time Left - "ID": "spacechase0.ObjectTimeLeft", - "UpdateKeys": [ "Nexus:1315" ] - }, - { - // OmniFarm - "ID": "BlueMod_OmniFarm | PhthaloBlue.OmniFarm", // changed in 2.0.2-pathoschild-update - "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_OmniFarm" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Out of Season Bonuses / Seasonal Items - "ID": "midoriarmstrong.seasonalitems", - "UpdateKeys": [ "Nexus:1452" ] - }, - { - // Part of the Community - "ID": "SB_PotC", - "UpdateKeys": [ "Nexus:923" ], - "Compatibility": { - "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // PelicanFiber - "ID": "PelicanFiber.dll | jwdred.PelicanFiber", // changed in 3.0.1 - "UpdateKeys": [ "Nexus:631" ], - "Compatibility": { - "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "3.0.2": "3.0.1" // didn't change manifest version - } - }, - { - // PelicanTTS - "ID": "Platonymous.PelicanTTS", - "UpdateKeys": [ "Nexus:1079" ], // added in 1.6.1 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Persia the Mermaid - Standalone Custom NPC - "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "UpdateKeys": [ "Nexus:1419" ] - }, - { - // Persival's BundleMod - "ID": "BundleMod.dll", - "UpdateKeys": [ "Nexus:438" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // Plant on Grass - "ID": "Demiacle.PlantOnGrass", - "UpdateKeys": [ "Nexus:1026" ] - }, - { - // Point-and-Plant - "ID": "PointAndPlant.dll | jwdred.PointAndPlant", // changed in 1.0.3 - "UpdateKeys": [ "Nexus:572" ], - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Pony Weight Loss Program - "ID": "BadNetCode.PonyWeightLossProgram", - "UpdateKeys": [ "Nexus:1232" ] - }, - { - // Portraiture - "ID": "Platonymous.Portraiture", - "UpdateKeys": [ "Nexus:999" ] // added in 1.3.1 - }, - { - // Prairie King Made Easy - "ID": "PrairieKingMadeEasy.dll | Mucchan.PrairieKingMadeEasy", // changed in 1.0.1 - "UpdateKeys": [ "Chucklefish:3594" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Quest Delay - "ID": "BadNetCode.QuestDelay", - "UpdateKeys": [ "Nexus:1239" ] - }, - { - // Rain Randomizer - "ID": "RainRandomizer.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Recatch Legendary Fish - "ID": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 - "UpdateKeys": [ "Nexus:172" ] - }, - { - // Regeneration - "ID": "HammurabiRegeneration", - "UpdateKeys": [ "Chucklefish:4584" ] - }, - { - // Relationship Bar UI - "ID": "RelationshipBar", - "UpdateKeys": [ "Nexus:1009" ] - }, - { - // RelationshipsEnhanced - "ID": "relationshipsenhanced", - "UpdateKeys": [ "Chucklefish:4435" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Relationship Status - "ID": "relationshipstatus", - "UpdateKeys": [ "Nexus:751" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.0.5": "1.0.4" // not updated in manifest - } - }, - { - // Rented Tools - "ID": "JarvieK.RentedTools", - "UpdateKeys": [ "Nexus:1307" ] - }, - { - // Replanter - "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 - "UpdateKeys": [ "Nexus:589" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // ReRegeneration - "ID": "lrsk_sdvm_rerg.0925160827", - "UpdateKeys": [ "Chucklefish:4465" ], - "MapLocalVersions": { - "1.1.2-release": "1.1.2" - } - }, - { - // Reseed - "ID": "Roc.Reseed", - "UpdateKeys": [ "Nexus:887" ] - }, - { - // Reusable Wallpapers and Floors (Wallpaper Retain) - "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "UpdateKeys": [ "Nexus:356" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Ring of Fire - "ID": "Platonymous.RingOfFire", - "UpdateKeys": [ "Nexus:1166" ] // added in 1.0.1 - }, - { - // Rope Bridge - "ID": "RopeBridge", - "UpdateKeys": [ "Nexus:824" ] - }, - { - // Rotate Toolbar - "ID": "Pathoschild.RotateToolbar", - "UpdateKeys": [ "Nexus:1100" ] - }, - { - // Rush Orders - "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 - "UpdateKeys": [ "Nexus:605" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Save Anywhere - "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl - "UpdateKeys": [ "Nexus:444" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "2.6": "2.5" // not updated in manifest - } - }, - { - // Save Backup - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'} | Omegasis.SaveBackup", // changed in 1.3; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:435" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Scroll to Blank - "ID": "caraxian.scroll.to.blank", - "UpdateKeys": [ "Chucklefish:4405" ] - }, - { - // Scythe Harvesting - "ID": "ScytheHarvesting | mmanlapat.ScytheHarvesting", // changed in 1.6 - "UpdateKeys": [ "Nexus:1106" ] - }, - { - // Seasonal Immersion - "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - // "UpdateKeys": [ "Chucklefish:4262" ], // Entoarox opted out of mod update checks - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Seed Bag - "ID": "Platonymous.SeedBag", - "UpdateKeys": [ "Nexus:1133" ] // added in 1.1.2 - }, - { - // Self Service - "ID": "JarvieK.SelfService", - "UpdateKeys": [ "Nexus:1304" ], - "MapRemoteVersions": { - "0.2.1": "0.2" // manifest not updated - } - }, - { - // Send Items - "ID": "Denifia.SendItems", - "UpdateKeys": [ "Nexus:1087" ], // added in 1.0.3 (2017-10-04) - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Shed Notifications (BuildingsNotifications) - "ID": "TheCroak.BuildingsNotifications", - "UpdateKeys": [ "Nexus:620" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Shenandoah Project - "ID": "Shenandoah Project", - "UpdateKeys": [ "Nexus:756" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.1.1": "1.1" // not updated in manifest - } - }, - { - // Ship Anywhere - "ID": "spacechase0.ShipAnywhere", - "UpdateKeys": [ "Nexus:1379" ] - }, - { - // Shipment Tracker - "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "UpdateKeys": [ "Nexus:321" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Shop Expander - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - // "UpdateKeys": [ "Chucklefish:4381" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Showcase Mod - "ID": "Igorious.Showcase", - "UpdateKeys": [ "Chucklefish:4487" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.9-500": "0.9" - } - }, - { - // Shroom Spotter - "ID": "TehPers.ShroomSpotter", - "UpdateKeys": [ "Nexus:908" ] - }, - { - // Simple Crop Label - "ID": "SimpleCropLabel", - "UpdateKeys": [ "Nexus:314" ] - }, - { - // Simple Sound Manager - "ID": "Omegasis.SimpleSoundManager", - "UpdateKeys": [ "Nexus:1410" ] - }, - { - // Simple Sprinklers - "ID": "SimpleSprinkler.dll | tZed.SimpleSprinkler", // changed in 1.5 - "UpdateKeys": [ "Nexus:76" ], - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Siv's Marriage Mod - "ID": "6266959802", - "UpdateKeys": [ "Nexus:366" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) - }, - "MapLocalVersions": { - "0.0": "1.4" - } - }, - { - // Skill Prestige - "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b | alphablackwolf.skillPrestige", // changed circa 1.2.3 - "UpdateKeys": [ "Nexus:569" ], - "Compatibility": { - "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Skill Prestige: Cooking Adapter - "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63 | Alphablackwolf.CookingSkillPrestigeAdapter", // changed circa 1.1 - "UpdateKeys": [ "Nexus:569" ], - "Compatibility": { - "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.2.3": "1.1" // manifest not updated - } - }, - { - // Skip Intro - "ID": "SkipIntro | Pathoschild.SkipIntro", // changed in 1.4 - "UpdateKeys": [ "Nexus:533" ] - }, - { - // Skull Cavern Elevator - "ID": "SkullCavernElevator", - "UpdateKeys": [ "Nexus:963" ] - }, - { - // Skull Cave Saver - "ID": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 - "UpdateKeys": [ "Nexus:175" ] - }, - { - // Sleepy Eye - "ID": "spacechase0.SleepyEye", - "UpdateKeys": [ "Nexus:1152" ] - }, - { - // Slower Fence Decay - "ID": "SPDSlowFenceDecay | Speeder.SlowerFenceDecay", // changed in 0.5.2-pathoschild-update - "UpdateKeys": [ "Nexus:252" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Smart Mod - "ID": "KuroBear.SmartMod", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Solar Eclipse Event - "ID": "KoihimeNakamura.SolarEclipseEvent", - "UpdateKeys": [ "Nexus:897" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.3-20170917": "1.3" - } - }, - { - // SpaceCore - "ID": "spacechase0.SpaceCore", - "UpdateKeys": [ "Nexus:1348" ] - }, - { - // Speedster - "ID": "Platonymous.Speedster", - "UpdateKeys": [ "Nexus:1102" ] // added in 1.3.1 - }, - { - // Sprinkler Range - "ID": "cat.sprinklerrange", - "UpdateKeys": [ "Nexus:1179" ], - "MapRemoteVersions": { - "1.0.1": "1.0" // manifest not updated - } - }, - { - // Sprinkles - "ID": "Platonymous.Sprinkles", - "UpdateKeys": [ "Chucklefish:4592" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Sprint and Dash - "ID": "SPDSprintAndDash", - "UpdateKeys": [ "Chucklefish:3531" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Sprint and Dash Redux - "ID": "lrsk_sdvm_sndr.0921161059 | littleraskol.SprintAndDashRedux", // changed in 1.3 - "UpdateKeys": [ "Chucklefish:4201" ] - }, - { - // Sprinting Mod - "ID": "a10d3097-b073-4185-98ba-76b586cba00c", - "UpdateKeys": [ "GitHub:oliverpl/SprintingMod" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapLocalVersions": { - "1.0": "2.1" // not updated in manifest - } - }, - { - // StackSplitX - "ID": "StackSplitX.dll | tstaples.StackSplitX", // changed circa 1.3.1 - "UpdateKeys": [ "Nexus:798" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // StaminaRegen - "ID": "StaminaRegen.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Stardew Config Menu - "ID": "Juice805.StardewConfigMenu", - "UpdateKeys": [ "Nexus:1312" ] - }, - { - // Stardew Content Compatibility Layer (SCCL) - "ID": "SCCL", - "UpdateKeys": [ "Nexus:889" ], - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Stardew Editor Game Integration - "ID": "spacechase0.StardewEditor.GameIntegration", - "UpdateKeys": [ "Nexus:1298" ] - }, - { - // Stardew Notification - "ID": "stardewnotification", - "UpdateKeys": [ "GitHub:monopandora/StardewNotification" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Stardew Symphony - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'} | Omegasis.StardewSymphony", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:425" ], - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // StarDustCore - "ID": "StarDustCore", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - } - } - }, - { - // Starting Money - "ID": "StartingMoney | mmanlapat.StartingMoney", // changed in 1.1 - "UpdateKeys": [ "Nexus:1138" ] - }, - { - // StashItemsToChest - "ID": "BlueMod_StashItemsToChest", - "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_StashItemsToChest" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Stephan's Lots of Crops - "ID": "stephansstardewcrops", - "UpdateKeys": [ "Chucklefish:4314" ], - "MapRemoteVersions": { - "1.41": "1.1" // manifest not updated - } - }, - { - // Stone Bridge Over Pond (PondWithBridge) - "ID": "PondWithBridge.dll", - "UpdateKeys": [ "Nexus:316" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.0": "1.0" - } - }, - { - // Stumps to Hardwood Stumps - "ID": "StumpsToHardwoodStumps", - "UpdateKeys": [ "Nexus:691" ] - }, - { - // Super Greenhouse Warp Modifier - "ID": "SuperGreenhouse", - "UpdateKeys": [ "Chucklefish:4334" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Swim Almost Anywhere / Swim Suit - "ID": "Platonymous.SwimSuit", - "UpdateKeys": [ "Nexus:1215" ] // added in 0.5.1 - }, - { - // Tainted Cellar - "ID": "TaintedCellar.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 - } - }, - { - // Tapper Ready - "ID": "skunkkk.TapperReady", - "UpdateKeys": [ "Nexus:1219" ] - }, - { - // Teh's Fishing Overhaul - "ID": "TehPers.FishingOverhaul", - "UpdateKeys": [ "Nexus:866" ] - }, - { - // Teleporter - "ID": "Teleporter", - "UpdateKeys": [ "Chucklefish:4374" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // The Long Night - "ID": "Pathoschild.TheLongNight", - "UpdateKeys": [ "Nexus:1369" ] - }, - { - // Three-heart Dance Partner - "ID": "ThreeHeartDancePartner", - "UpdateKeys": [ "Nexus:500" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // TimeFreeze - "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe", - "UpdateKeys": [ "Nexus:973" ] - }, - { - // Time Reminder - "ID": "KoihimeNakamura.TimeReminder", - "UpdateKeys": [ "Nexus:1000" ], - "MapLocalVersions": { - "1.0-20170314": "1.0.2" - } - }, - { - // TimeSpeed - "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:169" ], - "Compatibility": { - "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // TractorMod - "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "UpdateKeys": [ "Nexus:1401" ] - }, - { - // Tree Transplant - "ID": "TreeTransplant", - "UpdateKeys": [ "Nexus:1342" ] - }, - { - // UI Info Suite - "ID": "Cdaragorn.UiInfoSuite", - "UpdateKeys": [ "Nexus:1150" ] - }, - { - // UiModSuite - "ID": "Demiacle.UiModSuite", - "UpdateKeys": [ "Nexus:1023" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapLocalVersions": { - "0.5": "1.0" // not updated in manifest - } - }, - { - // Variable Grass - "ID": "dantheman999.VariableGrass", - "UpdateKeys": [ "GitHub:dantheman999301/StardewMods" ] - }, - { - // Vertical Toolbar - "ID": "SB_VerticalToolMenu", - "UpdateKeys": [ "Nexus:943" ] - }, - { - // WakeUp - "ID": "WakeUp.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Wallpaper Fix - "ID": "WallpaperFix.dll", - "UpdateKeys": [ "Chucklefish:4211" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // WarpAnimals - "ID": "Symen.WarpAnimals", - "UpdateKeys": [ "Nexus:1400" ] - }, - { - // Weather Controller - "ID": "WeatherController.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // What Farm Cave / WhatAMush - "ID": "WhatAMush", - "UpdateKeys": [ "Nexus:1097" ] - }, - { - // WHats Up - "ID": "wHatsUp", - "UpdateKeys": [ "Nexus:1082" ] - }, - { - // Wonderful Farm Life - "ID": "WonderfulFarmLife.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 - } - }, - { - // XmlSerializerRetool - "ID": "XmlSerializerRetool.dll", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." - } - } - }, - { - // Xnb Loader - "ID": "Entoarox.XnbLoader", - // "UpdateKeys": [ "Chucklefish:4506" ], // Entoarox opted out of mod update checks - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // zDailyIncrease - "ID": "zdailyincrease", - "UpdateKeys": [ "Chucklefish:4247" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapRemoteVersions": { - "1.3.5": "1.3.4" // not updated in manifest - } - }, - { - // Zoom Out Extreme - "ID": "ZoomMod | RockinMods.ZoomMod", // changed circa 1.2.1 - "UpdateKeys": [ "Nexus:1326" ], - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Better RNG - "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Calendar Anywhere - "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Durable Fences - "ID": "56d3439c-7b9b-497e-9496-0c4890e8a00e | Zoryn.DurableFences", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] - }, - { - // Zoryn's Health Bars - "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Fishing Mod - "ID": "fa277b1f-265e-47c3-a84f-cd320cc74949 | Zoryn.FishingMod", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] - }, - { - // Zoryn's Junimo Deposit Anywhere - "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Movement Mod - "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Regen Mod - "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - } - ] -} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj deleted file mode 100644 index 3721a11b..00000000 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ /dev/null @@ -1,277 +0,0 @@ - - - - - Debug - x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} - Exe - Properties - StardewModdingAPI - StardewModdingAPI - v4.5 - 512 - false - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true - - - x86 - false - DEBUG;TRACE - true - false - $(SolutionDir)\..\bin\Debug\SMAPI - $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml - true - - - x86 - false - $(SolutionDir)\..\bin\Release\SMAPI - $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml - TRACE - true - true - pdbonly - true - - - icon.ico - - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll - True - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll - True - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll - True - - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True - - - - - - - True - - - True - - - - - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - Designer - - - Always - - - Always - - - - - - Always - - - - - False - Microsoft .NET Framework 4.5 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.AssemblyRewriters - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs deleted file mode 100644 index ce344f81..00000000 --- a/src/StardewModdingAPI/Translation.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI -{ - /// A translation string with a fluent API to customise it. - public class Translation - { - /********* - ** Properties - *********/ - /// The placeholder text when the translation is null or empty, where {0} is the translation key. - internal const string PlaceholderText = "(no translation:{0})"; - - /// The name of the relevant mod for error messages. - private readonly string ModName; - - /// The locale for which the translation was fetched. - private readonly string Locale; - - /// The underlying translation text. - private readonly string Text; - - /// The value to return if the translations is undefined. - private readonly string Placeholder; - - - /********* - ** Accessors - *********/ - /// The original translation key. - public string Key { get; } - - - /********* - ** Public methods - *********/ - /// Construct an isntance. - /// The name of the relevant mod for error messages. - /// The locale for which the translation was fetched. - /// The translation key. - /// The underlying translation text. - internal Translation(string modName, string locale, string key, string text) - : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } - - /// Construct an isntance. - /// The name of the relevant mod for error messages. - /// The locale for which the translation was fetched. - /// The translation key. - /// The underlying translation text. - /// The value to return if the translations is undefined. - internal Translation(string modName, string locale, string key, string text, string placeholder) - { - this.ModName = modName; - this.Locale = locale; - this.Key = key; - this.Text = text; - this.Placeholder = placeholder; - } - - /// Throw an exception if the translation text is null or empty. - /// There's no available translation matching the requested key and locale. - public Translation Assert() - { - if (!this.HasValue()) - throw new KeyNotFoundException($"The '{this.ModName}' mod doesn't have a translation with key '{this.Key}' for the '{this.Locale}' locale or its fallbacks."); - return this; - } - - /// Replace the text if it's null or empty. If you set a null or empty value, the translation will show the fallback "no translation" placeholder (see if you want to disable that). Returns a new instance if changed. - /// The default value. - public Translation Default(string @default) - { - return this.HasValue() - ? this - : new Translation(this.ModName, this.Locale, this.Key, @default); - } - - /// Whether to return a "no translation" placeholder if the translation is null or empty. Returns a new instance. - /// Whether to return a placeholder. - public Translation UsePlaceholder(bool use) - { - return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); - } - - /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. - /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. - /// The argument is null. - public Translation Tokens(object tokens) - { - if (string.IsNullOrWhiteSpace(this.Text) || tokens == null) - return this; - - // get dictionary of tokens - IDictionary tokenLookup = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - { - // from dictionary - if (tokens is IDictionary inputLookup) - { - foreach (DictionaryEntry entry in inputLookup) - { - string key = entry.Key?.ToString().Trim(); - if (key != null) - tokenLookup[key] = entry.Value?.ToString(); - } - } - - // from object properties - else - { - Type type = tokens.GetType(); - foreach (PropertyInfo prop in type.GetProperties()) - tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString(); - foreach (FieldInfo field in type.GetFields()) - tokenLookup[field.Name] = field.GetValue(tokens)?.ToString(); - } - } - - // format translation - string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => - { - string key = match.Groups[1].Value.Trim(); - return tokenLookup.TryGetValue(key, out string value) - ? value - : match.Value; - }); - return new Translation(this.ModName, this.Locale, this.Key, text); - } - - /// Get whether the translation has a defined value. - public bool HasValue() - { - return !string.IsNullOrEmpty(this.Text); - } - - /// Get the translation text. Calling this method isn't strictly necessary, since you can assign a value directly to a string. - public override string ToString() - { - return this.Placeholder != null && !this.HasValue() - ? this.Placeholder - : this.Text; - } - - /// Get a string representation of the given translation. - /// The translation key. - public static implicit operator string(Translation translation) - { - return translation?.ToString(); - } - } -} diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/StardewModdingAPI/Utilities/SButton.cs deleted file mode 100644 index fa5ae648..00000000 --- a/src/StardewModdingAPI/Utilities/SButton.cs +++ /dev/null @@ -1,675 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI.Utilities -{ - /// A unified button constant which includes all controller, keyboard, and mouse buttons. - /// Derived from , , and . - public enum SButton - { - /// No valid key. - None = 0, - - /********* - ** Mouse - *********/ - /// The left mouse button. - MouseLeft = 1000, - - /// The right mouse button. - MouseRight = 1001, - - /// The middle mouse button. - MouseMiddle = 1002, - - /// The first mouse XButton. - MouseX1 = 1003, - - /// The second mouse XButton. - MouseX2 = 1004, - - /********* - ** Controller - *********/ - /// The 'A' button on a controller. - ControllerA = SButtonExtensions.ControllerOffset + Buttons.A, - - /// The 'B' button on a controller. - ControllerB = SButtonExtensions.ControllerOffset + Buttons.B, - - /// The 'X' button on a controller. - ControllerX = SButtonExtensions.ControllerOffset + Buttons.X, - - /// The 'Y' button on a controller. - ControllerY = SButtonExtensions.ControllerOffset + Buttons.Y, - - /// The back button on a controller. - ControllerBack = SButtonExtensions.ControllerOffset + Buttons.Back, - - /// The start button on a controller. - ControllerStart = SButtonExtensions.ControllerOffset + Buttons.Start, - - /// The up button on the directional pad of a controller. - DPadUp = SButtonExtensions.ControllerOffset + Buttons.DPadUp, - - /// The down button on the directional pad of a controller. - DPadDown = SButtonExtensions.ControllerOffset + Buttons.DPadDown, - - /// The left button on the directional pad of a controller. - DPadLeft = SButtonExtensions.ControllerOffset + Buttons.DPadLeft, - - /// The right button on the directional pad of a controller. - DPadRight = SButtonExtensions.ControllerOffset + Buttons.DPadRight, - - /// The left bumper (shoulder) button on a controller. - LeftShoulder = SButtonExtensions.ControllerOffset + Buttons.LeftShoulder, - - /// The right bumper (shoulder) button on a controller. - RightShoulder = SButtonExtensions.ControllerOffset + Buttons.RightShoulder, - - /// The left trigger on a controller. - LeftTrigger = SButtonExtensions.ControllerOffset + Buttons.LeftTrigger, - - /// The right trigger on a controller. - RightTrigger = SButtonExtensions.ControllerOffset + Buttons.RightTrigger, - - /// The left analog stick on a controller (when pressed). - LeftStick = SButtonExtensions.ControllerOffset + Buttons.LeftStick, - - /// The right analog stick on a controller (when pressed). - RightStick = SButtonExtensions.ControllerOffset + Buttons.RightStick, - - /// The 'big button' on a controller. - BigButton = SButtonExtensions.ControllerOffset + Buttons.BigButton, - - /// The left analog stick on a controller (when pushed left). - LeftThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickLeft, - - /// The left analog stick on a controller (when pushed right). - LeftThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickRight, - - /// The left analog stick on a controller (when pushed down). - LeftThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickDown, - - /// The left analog stick on a controller (when pushed up). - LeftThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickUp, - - /// The right analog stick on a controller (when pushed left). - RightThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickLeft, - - /// The right analog stick on a controller (when pushed right). - RightThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickRight, - - /// The right analog stick on a controller (when pushed down). - RightThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickDown, - - /// The right analog stick on a controller (when pushed up). - RightThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickUp, - - /********* - ** Keyboard - *********/ - /// The A button on a keyboard. - A = Keys.A, - - /// The Add button on a keyboard. - Add = Keys.Add, - - /// The Applications button on a keyboard. - Apps = Keys.Apps, - - /// The Attn button on a keyboard. - Attn = Keys.Attn, - - /// The B button on a keyboard. - B = Keys.B, - - /// The Backspace button on a keyboard. - Back = Keys.Back, - - /// The Browser Back button on a keyboard in Windows 2000/XP. - BrowserBack = Keys.BrowserBack, - - /// The Browser Favorites button on a keyboard in Windows 2000/XP. - BrowserFavorites = Keys.BrowserFavorites, - - /// The Browser Favorites button on a keyboard in Windows 2000/XP. - BrowserForward = Keys.BrowserForward, - - /// The Browser Home button on a keyboard in Windows 2000/XP. - BrowserHome = Keys.BrowserHome, - - /// The Browser Refresh button on a keyboard in Windows 2000/XP. - BrowserRefresh = Keys.BrowserRefresh, - - /// The Browser Search button on a keyboard in Windows 2000/XP. - BrowserSearch = Keys.BrowserSearch, - - /// The Browser Stop button on a keyboard in Windows 2000/XP. - BrowserStop = Keys.BrowserStop, - - /// The C button on a keyboard. - C = Keys.C, - - /// The Caps Lock button on a keyboard. - CapsLock = Keys.CapsLock, - - /// The Green ChatPad button on a keyboard. - ChatPadGreen = Keys.ChatPadGreen, - - /// The Orange ChatPad button on a keyboard. - ChatPadOrange = Keys.ChatPadOrange, - - /// The CrSel button on a keyboard. - Crsel = Keys.Crsel, - - /// The D button on a keyboard. - D = Keys.D, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D0 = Keys.D0, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D1 = Keys.D1, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D2 = Keys.D2, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D3 = Keys.D3, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D4 = Keys.D4, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D5 = Keys.D5, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D6 = Keys.D6, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D7 = Keys.D7, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D8 = Keys.D8, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - D9 = Keys.D9, - - /// The Decimal button on a keyboard. - Decimal = Keys.Decimal, - - /// The Delete button on a keyboard. - Delete = Keys.Delete, - - /// The Divide button on a keyboard. - Divide = Keys.Divide, - - /// The Down arrow button on a keyboard. - Down = Keys.Down, - - /// The E button on a keyboard. - E = Keys.E, - - /// The End button on a keyboard. - End = Keys.End, - - /// The Enter button on a keyboard. - Enter = Keys.Enter, - - /// The Erase EOF button on a keyboard. - EraseEof = Keys.EraseEof, - - /// The Escape button on a keyboard. - Escape = Keys.Escape, - - /// The Execute button on a keyboard. - Execute = Keys.Execute, - - /// The ExSel button on a keyboard. - Exsel = Keys.Exsel, - - /// The F button on a keyboard. - F = Keys.F, - - /// The F1 button on a keyboard. - F1 = Keys.F1, - - /// The F10 button on a keyboard. - F10 = Keys.F10, - - /// The F11 button on a keyboard. - F11 = Keys.F11, - - /// The F12 button on a keyboard. - F12 = Keys.F12, - - /// The F13 button on a keyboard. - F13 = Keys.F13, - - /// The F14 button on a keyboard. - F14 = Keys.F14, - - /// The F15 button on a keyboard. - F15 = Keys.F15, - - /// The F16 button on a keyboard. - F16 = Keys.F16, - - /// The F17 button on a keyboard. - F17 = Keys.F17, - - /// The F18 button on a keyboard. - F18 = Keys.F18, - - /// The F19 button on a keyboard. - F19 = Keys.F19, - - /// The F2 button on a keyboard. - F2 = Keys.F2, - - /// The F20 button on a keyboard. - F20 = Keys.F20, - - /// The F21 button on a keyboard. - F21 = Keys.F21, - - /// The F22 button on a keyboard. - F22 = Keys.F22, - - /// The F23 button on a keyboard. - F23 = Keys.F23, - - /// The F24 button on a keyboard. - F24 = Keys.F24, - - /// The F3 button on a keyboard. - F3 = Keys.F3, - - /// The F4 button on a keyboard. - F4 = Keys.F4, - - /// The F5 button on a keyboard. - F5 = Keys.F5, - - /// The F6 button on a keyboard. - F6 = Keys.F6, - - /// The F7 button on a keyboard. - F7 = Keys.F7, - - /// The F8 button on a keyboard. - F8 = Keys.F8, - - /// The F9 button on a keyboard. - F9 = Keys.F9, - - /// The G button on a keyboard. - G = Keys.G, - - /// The H button on a keyboard. - H = Keys.H, - - /// The Help button on a keyboard. - Help = Keys.Help, - - /// The Home button on a keyboard. - Home = Keys.Home, - - /// The I button on a keyboard. - I = Keys.I, - - /// The IME Convert button on a keyboard. - ImeConvert = Keys.ImeConvert, - - /// The IME NoConvert button on a keyboard. - ImeNoConvert = Keys.ImeNoConvert, - - /// The INS button on a keyboard. - Insert = Keys.Insert, - - /// The J button on a keyboard. - J = Keys.J, - - /// The K button on a keyboard. - K = Keys.K, - - /// The Kana button on a Japanese keyboard. - Kana = Keys.Kana, - - /// The Kanji button on a Japanese keyboard. - Kanji = Keys.Kanji, - - /// The L button on a keyboard. - L = Keys.L, - - /// The Start Applications 1 button on a keyboard in Windows 2000/XP. - LaunchApplication1 = Keys.LaunchApplication1, - - /// The Start Applications 2 button on a keyboard in Windows 2000/XP. - LaunchApplication2 = Keys.LaunchApplication2, - - /// The Start Mail button on a keyboard in Windows 2000/XP. - LaunchMail = Keys.LaunchMail, - - /// The Left arrow button on a keyboard. - Left = Keys.Left, - - /// The Left Alt button on a keyboard. - LeftAlt = Keys.LeftAlt, - - /// The Left Control button on a keyboard. - LeftControl = Keys.LeftControl, - - /// The Left Shift button on a keyboard. - LeftShift = Keys.LeftShift, - - /// The Left Windows button on a keyboard. - LeftWindows = Keys.LeftWindows, - - /// The M button on a keyboard. - M = Keys.M, - - /// The MediaNextTrack button on a keyboard in Windows 2000/XP. - MediaNextTrack = Keys.MediaNextTrack, - - /// The MediaPlayPause button on a keyboard in Windows 2000/XP. - MediaPlayPause = Keys.MediaPlayPause, - - /// The MediaPreviousTrack button on a keyboard in Windows 2000/XP. - MediaPreviousTrack = Keys.MediaPreviousTrack, - - /// The MediaStop button on a keyboard in Windows 2000/XP. - MediaStop = Keys.MediaStop, - - /// The Multiply button on a keyboard. - Multiply = Keys.Multiply, - - /// The N button on a keyboard. - N = Keys.N, - - /// The Num Lock button on a keyboard. - NumLock = Keys.NumLock, - - /// The Numeric keypad 0 button on a keyboard. - NumPad0 = Keys.NumPad0, - - /// The Numeric keypad 1 button on a keyboard. - NumPad1 = Keys.NumPad1, - - /// The Numeric keypad 2 button on a keyboard. - NumPad2 = Keys.NumPad2, - - /// The Numeric keypad 3 button on a keyboard. - NumPad3 = Keys.NumPad3, - - /// The Numeric keypad 4 button on a keyboard. - NumPad4 = Keys.NumPad4, - - /// The Numeric keypad 5 button on a keyboard. - NumPad5 = Keys.NumPad5, - - /// The Numeric keypad 6 button on a keyboard. - NumPad6 = Keys.NumPad6, - - /// The Numeric keypad 7 button on a keyboard. - NumPad7 = Keys.NumPad7, - - /// The Numeric keypad 8 button on a keyboard. - NumPad8 = Keys.NumPad8, - - /// The Numeric keypad 9 button on a keyboard. - NumPad9 = Keys.NumPad9, - - /// The O button on a keyboard. - O = Keys.O, - - /// A miscellaneous button on a keyboard; can vary by keyboard. - Oem8 = Keys.Oem8, - - /// The OEM Auto button on a keyboard. - OemAuto = Keys.OemAuto, - - /// The OEM Angle Bracket or Backslash button on the RT 102 keyboard in Windows 2000/XP. - OemBackslash = Keys.OemBackslash, - - /// The Clear button on a keyboard. - OemClear = Keys.OemClear, - - /// The OEM Close Bracket button on a US standard keyboard in Windows 2000/XP. - OemCloseBrackets = Keys.OemCloseBrackets, - - /// The ',' button on a keyboard in any country/region in Windows 2000/XP. - OemComma = Keys.OemComma, - - /// The OEM Copy button on a keyboard. - OemCopy = Keys.OemCopy, - - /// The OEM Enlarge Window button on a keyboard. - OemEnlW = Keys.OemEnlW, - - /// The '-' button on a keyboard in any country/region in Windows 2000/XP. - OemMinus = Keys.OemMinus, - - /// The OEM Open Bracket button on a US standard keyboard in Windows 2000/XP. - OemOpenBrackets = Keys.OemOpenBrackets, - - /// The '.' button on a keyboard in any country/region. - OemPeriod = Keys.OemPeriod, - - /// The OEM Pipe button on a US standard keyboard. - OemPipe = Keys.OemPipe, - - /// The '+' button on a keyboard in Windows 2000/XP. - OemPlus = Keys.OemPlus, - - /// The OEM Question Mark button on a US standard keyboard. - OemQuestion = Keys.OemQuestion, - - /// The OEM Single/Double Quote button on a US standard keyboard. - OemQuotes = Keys.OemQuotes, - - /// The OEM Semicolon button on a US standard keyboard. - OemSemicolon = Keys.OemSemicolon, - - /// The OEM Tilde button on a US standard keyboard. - OemTilde = Keys.OemTilde, - - /// The P button on a keyboard. - P = Keys.P, - - /// The PA1 button on a keyboard. - Pa1 = Keys.Pa1, - - /// The Page Down button on a keyboard. - PageDown = Keys.PageDown, - - /// The Page Up button on a keyboard. - PageUp = Keys.PageUp, - - /// The Pause button on a keyboard. - Pause = Keys.Pause, - - /// The Play button on a keyboard. - Play = Keys.Play, - - /// The Print button on a keyboard. - Print = Keys.Print, - - /// The Print Screen button on a keyboard. - PrintScreen = Keys.PrintScreen, - - /// The IME Process button on a keyboard in Windows 95/98/ME/NT 4.0/2000/XP. - ProcessKey = Keys.ProcessKey, - - /// The Q button on a keyboard. - Q = Keys.Q, - - /// The R button on a keyboard. - R = Keys.R, - - /// The Right Arrow button on a keyboard. - Right = Keys.Right, - - /// The Right Alt button on a keyboard. - RightAlt = Keys.RightAlt, - - /// The Right Control button on a keyboard. - RightControl = Keys.RightControl, - - /// The Right Shift button on a keyboard. - RightShift = Keys.RightShift, - - /// The Right Windows button on a keyboard. - RightWindows = Keys.RightWindows, - - /// The S button on a keyboard. - S = Keys.S, - - /// The Scroll Lock button on a keyboard. - Scroll = Keys.Scroll, - - /// The Select button on a keyboard. - Select = Keys.Select, - - /// The Select Media button on a keyboard in Windows 2000/XP. - SelectMedia = Keys.SelectMedia, - - /// The Separator button on a keyboard. - Separator = Keys.Separator, - - /// The Computer Sleep button on a keyboard. - Sleep = Keys.Sleep, - - /// The Space bar on a keyboard. - Space = Keys.Space, - - /// The Subtract button on a keyboard. - Subtract = Keys.Subtract, - - /// The T button on a keyboard. - T = Keys.T, - - /// The Tab button on a keyboard. - Tab = Keys.Tab, - - /// The U button on a keyboard. - U = Keys.U, - - /// The Up Arrow button on a keyboard. - Up = Keys.Up, - - /// The V button on a keyboard. - V = Keys.V, - - /// The Volume Down button on a keyboard in Windows 2000/XP. - VolumeDown = Keys.VolumeDown, - - /// The Volume Mute button on a keyboard in Windows 2000/XP. - VolumeMute = Keys.VolumeMute, - - /// The Volume Up button on a keyboard in Windows 2000/XP. - VolumeUp = Keys.VolumeUp, - - /// The W button on a keyboard. - W = Keys.W, - - /// The X button on a keyboard. - X = Keys.X, - - /// The Y button on a keyboard. - Y = Keys.Y, - - /// The Z button on a keyboard. - Z = Keys.Z, - - /// The Zoom button on a keyboard. - Zoom = Keys.Zoom - } - - /// Provides extension methods for . - public static class SButtonExtensions - { - /********* - ** Accessors - *********/ - /// The offset added to values when converting them to to avoid collisions with values. - internal const int ControllerOffset = 2000; - - - /********* - ** Public methods - *********/ - /// Get the equivalent for the given button. - /// The keyboard button to convert. - internal static SButton ToSButton(this Keys key) - { - return (SButton)key; - } - - /// Get the equivalent for the given button. - /// The controller button to convert. - internal static SButton ToSButton(this Buttons key) - { - return (SButton)(SButtonExtensions.ControllerOffset + key); - } - - /// Get the equivalent for the given button. - /// The button to convert. - /// The keyboard equivalent. - /// Returns whether the value was converted successfully. - public static bool TryGetKeyboard(this SButton input, out Keys key) - { - if (Enum.IsDefined(typeof(Keys), (int)input)) - { - key = (Keys)input; - return true; - } - - key = Keys.None; - return false; - } - - /// Get the equivalent for the given button. - /// The button to convert. - /// The controller equivalent. - /// Returns whether the value was converted successfully. - public static bool TryGetController(this SButton input, out Buttons button) - { - if (Enum.IsDefined(typeof(Buttons), (int)input - SButtonExtensions.ControllerOffset)) - { - button = (Buttons)(input - SButtonExtensions.ControllerOffset); - return true; - } - - button = 0; - return false; - } - - /// Get the equivalent for the given button. - /// The button to convert. - /// The Stardew Valley input button equivalent. - /// Returns whether the value was converted successfully. - public static bool TryGetStardewInput(this SButton input, out InputButton button) - { - // keyboard - if (input.TryGetKeyboard(out Keys key)) - { - button = new InputButton(key); - return true; - } - - // mouse - if (input == SButton.MouseLeft || input == SButton.MouseRight) - { - button = new InputButton(mouseLeft: input == SButton.MouseLeft); - return true; - } - - // not valid - button = default(InputButton); - return false; - } - } -} diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/StardewModdingAPI/Utilities/SDate.cs deleted file mode 100644 index 326d7fc7..00000000 --- a/src/StardewModdingAPI/Utilities/SDate.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Utilities -{ - /// Represents a Stardew Valley date. - public class SDate : IEquatable - { - /********* - ** Properties - *********/ - /// The internal season names in order. - private readonly string[] Seasons = { "spring", "summer", "fall", "winter" }; - - /// The number of seasons in a year. - private int SeasonsInYear => this.Seasons.Length; - - /// The number of days in a season. - private readonly int DaysInSeason = 28; - - - /********* - ** Accessors - *********/ - /// The day of month. - public int Day { get; } - - /// The season name. - public string Season { get; } - - /// The year. - public int Year { get; } - - /// The day of week. - public DayOfWeek DayOfWeek { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The day of month. - /// The season name. - /// One of the arguments has an invalid value (like day 35). - public SDate(int day, string season) - : this(day, season, Game1.year) { } - - /// Construct an instance. - /// The day of month. - /// The season name. - /// The year. - /// One of the arguments has an invalid value (like day 35). - public SDate(int day, string season, int year) - { - // validate - if (season == null) - throw new ArgumentNullException(nameof(season)); - if (!this.Seasons.Contains(season)) - throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); - if (day < 1 || day > this.DaysInSeason) - throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); - if (year < 1) - throw new ArgumentException($"Invalid year '{year}', must be at least 1."); - - // initialise - this.Day = day; - this.Season = season; - this.Year = year; - this.DayOfWeek = this.GetDayOfWeek(); - } - - /// Get the current in-game date. - public static SDate Now() - { - return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year); - } - - /// Get a new date with the given number of days added. - /// The number of days to add. - /// Returns the resulting date. - /// The offset would result in an invalid date (like year 0). - public SDate AddDays(int offset) - { - // get new hash code - int hashCode = this.GetHashCode() + offset; - if (hashCode < 1) - throw new ArithmeticException($"Adding {offset} days to {this} would result in a date before 01 spring Y1."); - - // get day - int day = hashCode % 28; - if (day == 0) - day = 28; - - // get season index - int seasonIndex = hashCode / 28; - if (seasonIndex > 0 && hashCode % 28 == 0) - seasonIndex -= 1; - seasonIndex %= 4; - - // get year - int year = hashCode / (this.Seasons.Length * this.DaysInSeason) + 1; - - // create date - return new SDate(day, this.Seasons[seasonIndex], year); - } - - /// Get a string representation of the date. This is mainly intended for debugging or console messages. - public override string ToString() - { - return $"{this.Day:00} {this.Season} Y{this.Year}"; - } - - /**** - ** IEquatable - ****/ - /// Get whether this instance is equal to another. - /// The other value to compare. - public bool Equals(SDate other) - { - return this == other; - } - - /// Get whether this instance is equal to another. - /// The other value to compare. - public override bool Equals(object obj) - { - return obj is SDate other && this == other; - } - - /// Get a hash code which uniquely identifies a date. - public override int GetHashCode() - { - // return the number of days since 01 spring Y1 (inclusively) - int yearIndex = this.Year - 1; - return - yearIndex * this.DaysInSeason * this.SeasonsInYear - + this.GetSeasonIndex() * this.DaysInSeason - + this.Day; - } - - /**** - ** Operators - ****/ - /// Get whether one date is equal to another. - /// The base date to compare. - /// The other date to compare. - /// The equality of the dates - public static bool operator ==(SDate date, SDate other) - { - return date?.GetHashCode() == other?.GetHashCode(); - } - - /// Get whether one date is not equal to another. - /// The base date to compare. - /// The other date to compare. - public static bool operator !=(SDate date, SDate other) - { - return date?.GetHashCode() != other?.GetHashCode(); - } - - /// Get whether one date is more than another. - /// The base date to compare. - /// The other date to compare. - public static bool operator >(SDate date, SDate other) - { - return date?.GetHashCode() > other?.GetHashCode(); - } - - /// Get whether one date is more than or equal to another. - /// The base date to compare. - /// The other date to compare. - public static bool operator >=(SDate date, SDate other) - { - return date?.GetHashCode() >= other?.GetHashCode(); - } - - /// Get whether one date is less than or equal to another. - /// The base date to compare. - /// The other date to compare. - public static bool operator <=(SDate date, SDate other) - { - return date?.GetHashCode() <= other?.GetHashCode(); - } - - /// Get whether one date is less than another. - /// The base date to compare. - /// The other date to compare. - public static bool operator <(SDate date, SDate other) - { - return date?.GetHashCode() < other?.GetHashCode(); - } - - - /********* - ** Private methods - *********/ - /// Get the day of week for the current date. - private DayOfWeek GetDayOfWeek() - { - switch (this.Day % 7) - { - case 0: - return DayOfWeek.Sunday; - case 1: - return DayOfWeek.Monday; - case 2: - return DayOfWeek.Tuesday; - case 3: - return DayOfWeek.Wednesday; - case 4: - return DayOfWeek.Thursday; - case 5: - return DayOfWeek.Friday; - case 6: - return DayOfWeek.Saturday; - default: - return 0; - } - } - - /// Get the current season index. - /// The current season wasn't recognised. - private int GetSeasonIndex() - { - int index = Array.IndexOf(this.Seasons, this.Season); - if (index == -1) - throw new InvalidOperationException($"The current season '{this.Season}' wasn't recognised."); - return index; - } - } -} diff --git a/src/StardewModdingAPI/icon.ico b/src/StardewModdingAPI/icon.ico deleted file mode 100644 index 587a6e74..00000000 Binary files a/src/StardewModdingAPI/icon.ico and /dev/null differ diff --git a/src/StardewModdingAPI/packages.config b/src/StardewModdingAPI/packages.config deleted file mode 100644 index e5fa3c3a..00000000 --- a/src/StardewModdingAPI/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI/steam_appid.txt b/src/StardewModdingAPI/steam_appid.txt deleted file mode 100644 index 9fe92b96..00000000 --- a/src/StardewModdingAPI/steam_appid.txt +++ /dev/null @@ -1 +0,0 @@ -413150 \ No newline at end of file diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh deleted file mode 100644 index 70f1873a..00000000 --- a/src/StardewModdingAPI/unix-launcher.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash -# MonoKickstart Shell Script -# Written by Ethan "flibitijibibo" Lee -# Modified for StardewModdingAPI by Viz and Pathoschild - -# Move to script's directory -cd "`dirname "$0"`" - -# Get the system architecture -UNAME=`uname` -ARCH=`uname -m` - -# MonoKickstart picks the right libfolder, so just execute the right binary. -if [ "$UNAME" == "Darwin" ]; then - # ... Except on OSX. - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ - - # El Capitan is a total idiot and wipes this variable out, making the - # Steam overlay disappear. This sidesteps "System Integrity Protection" - # and resets the variable with Valve's own variable (they provided this - # fix by the way, thanks Valve!). Note that you will need to update your - # launch configuration to the script location, NOT just the app location - # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). - # -flibit - if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then - export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" - fi - - # this was here before - ln -sf mcs.bin.osx mcs - - # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI - if [ -f libgdiplus.dylib ]; then - rm libgdiplus.dylib - fi - if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then - ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib - fi - - # launch SMAPI - cp StardewValley.bin.osx StardewModdingAPI.bin.osx - open -a Terminal ./StardewModdingAPI.bin.osx $@ -else - # choose launcher - LAUNCHER="" - if [ "$ARCH" == "x86_64" ]; then - ln -sf mcs.bin.x86_64 mcs - cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" - else - ln -sf mcs.bin.x86 mcs - cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86 $@" - fi - - # get cross-distro version of POSIX command - COMMAND="" - if command -v command 2>/dev/null; then - COMMAND="command -v" - elif type type 2>/dev/null; then - COMMAND="type" - fi - - # open SMAPI in terminal - if $COMMAND x-terminal-emulator 2>/dev/null; then - x-terminal-emulator -e "$LAUNCHER" - elif $COMMAND xfce4-terminal 2>/dev/null; then - xfce4-terminal -e "$LAUNCHER" - elif $COMMAND gnome-terminal 2>/dev/null; then - gnome-terminal -e "$LAUNCHER" - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" - elif $COMMAND konsole 2>/dev/null; then - konsole -e "$LAUNCHER" - elif $COMMAND terminal 2>/dev/null; then - terminal -e "$LAUNCHER" - else - $LAUNCHER - fi - - # some Linux users get error 127 (command not found) from the above block, even though - # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when - # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal - # (which can be slow if there is none). - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi -fi diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 383e8c28..3182338c 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -1,4 +1,4 @@ - + @@ -48,7 +48,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -85,7 +85,7 @@ - + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI False @@ -98,5 +98,5 @@ - + \ No newline at end of file diff --git a/src/common.targets b/src/common.targets deleted file mode 100644 index ee138524..00000000 --- a/src/common.targets +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - $(HOME)/.steam/steam/steamapps/common/Stardew Valley - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - - - - - $(DefineConstants);SMAPI_FOR_WINDOWS - - - - False - - - False - - - False - - - False - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\xTile.dll - False - False - - - - - - $(DefineConstants);SMAPI_FOR_UNIX - - - - $(GamePath)\MonoGame.Framework.dll - False - False - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\xTile.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets deleted file mode 100644 index f2a2b23c..00000000 --- a/src/prepare-install-package.targets +++ /dev/null @@ -1,49 +0,0 @@ - - - - - $(SolutionDir)\..\bin\$(Configuration)\SMAPI - $(SolutionDir)\..\bin\Packaged - $(PackagePath)\internal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- cgit