diff options
-rw-r--r-- | .editorconfig (renamed from src/.editorconfig) | 0 | ||||
-rw-r--r-- | .gitignore | 247 | ||||
-rw-r--r-- | LICENSE.txt (renamed from LICENSE) | 0 | ||||
-rw-r--r-- | build/GlobalAssemblyInfo.cs (renamed from src/GlobalAssemblyInfo.cs) | 4 | ||||
-rw-r--r-- | build/common.targets (renamed from src/common.targets) | 0 | ||||
-rw-r--r-- | build/prepare-install-package.targets (renamed from src/prepare-install-package.targets) | 0 | ||||
-rw-r--r-- | docs/CONTRIBUTING.md (renamed from CONTRIBUTING.md) | 0 | ||||
-rw-r--r-- | docs/README.md | 45 | ||||
-rw-r--r-- | docs/imgs/SMAPI.png | bin | 252371 -> 0 bytes | |||
-rw-r--r-- | docs/mod-build-config.md | 175 | ||||
-rw-r--r-- | docs/release-notes.md (renamed from release-notes.md) | 83 | ||||
-rw-r--r-- | docs/technical-docs.md (renamed from README.md) | 87 | ||||
-rw-r--r-- | src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs | 7 | ||||
-rw-r--r-- | src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs) | 10 | ||||
-rw-r--r-- | src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj (renamed from src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj) | 37 | ||||
-rw-r--r-- | src/SMAPI.Common/Models/ModInfoModel.cs | 52 | ||||
-rw-r--r-- | src/SMAPI.Common/Models/ModSeachModel.cs | 32 | ||||
-rw-r--r-- | src/SMAPI.Common/SemanticVersionImpl.cs | 185 | ||||
-rw-r--r-- | src/SMAPI.Common/StardewModdingAPI.Common.projitems | 19 | ||||
-rw-r--r-- | src/SMAPI.Common/StardewModdingAPI.Common.shproj | 13 | ||||
-rw-r--r-- | src/SMAPI.Installer/Enums/Platform.cs (renamed from src/StardewModdingAPI.Installer/Enums/Platform.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Installer/Enums/ScriptAction.cs (renamed from src/StardewModdingAPI.Installer/Enums/ScriptAction.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Installer/InteractiveInstaller.cs (renamed from src/StardewModdingAPI.Installer/InteractiveInstaller.cs) | 8 | ||||
-rw-r--r-- | src/SMAPI.Installer/Program.cs (renamed from src/StardewModdingAPI.Installer/Program.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Installer/Properties/AssemblyInfo.cs (renamed from src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Installer/StardewModdingAPI.Installer.csproj (renamed from src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj) | 8 | ||||
-rw-r--r-- | src/SMAPI.Installer/readme.txt (renamed from src/StardewModdingAPI.Installer/readme.txt) | 0 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/DeployModTask.cs | 154 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs | 174 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs | 16 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs | 9 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj | 60 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn | bin | 0 -> 7401 bytes | |||
-rw-r--r-- | src/SMAPI.ModBuildConfig/assets/nuget-icon.png | bin | 0 -> 5054 bytes | |||
-rw-r--r-- | src/SMAPI.ModBuildConfig/build/smapi.targets | 144 | ||||
-rw-r--r-- | src/SMAPI.ModBuildConfig/package.nuspec | 32 | ||||
-rw-r--r-- | src/SMAPI.Tests/Core/ModResolverTests.cs (renamed from src/StardewModdingAPI.Tests/Core/ModResolverTests.cs) | 50 | ||||
-rw-r--r-- | src/SMAPI.Tests/Core/TranslationTests.cs (renamed from src/StardewModdingAPI.Tests/Core/TranslationTests.cs) | 3 | ||||
-rw-r--r-- | src/SMAPI.Tests/Properties/AssemblyInfo.cs (renamed from src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Tests/Sample.cs (renamed from src/StardewModdingAPI.Tests/Sample.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Tests/StardewModdingAPI.Tests.csproj (renamed from src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj) | 23 | ||||
-rw-r--r-- | src/SMAPI.Tests/Utilities/SDateTests.cs (renamed from src/StardewModdingAPI.Tests/Utilities/SDateTests.cs) | 2 | ||||
-rw-r--r-- | src/SMAPI.Tests/Utilities/SemanticVersionTests.cs (renamed from src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI.Tests/packages.config | 7 | ||||
-rw-r--r-- | src/SMAPI.Web/Controllers/ModsController.cs | 162 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs | 74 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs | 27 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs | 51 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs | 92 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs | 97 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs | 24 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs | 89 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/RewriteSubdomainRule.cs | 30 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/VersionConstraint.cs | 16 | ||||
-rw-r--r-- | src/SMAPI.Web/Program.cs | 26 | ||||
-rw-r--r-- | src/SMAPI.Web/Properties/AssemblyInfo.cs | 4 | ||||
-rw-r--r-- | src/SMAPI.Web/Properties/launchSettings.json | 29 | ||||
-rw-r--r-- | src/SMAPI.Web/StardewModdingAPI.Web.csproj | 26 | ||||
-rw-r--r-- | src/SMAPI.Web/Startup.cs | 70 | ||||
-rw-r--r-- | src/SMAPI.Web/appsettings.Development.json | 10 | ||||
-rw-r--r-- | src/SMAPI.Web/appsettings.json | 30 | ||||
-rw-r--r-- | src/SMAPI.sln (renamed from src/StardewModdingAPI.sln) | 82 | ||||
-rw-r--r-- | src/SMAPI.sln.DotSettings (renamed from src/StardewModdingAPI.sln.DotSettings) | 1 | ||||
-rw-r--r-- | src/SMAPI/App.config (renamed from src/StardewModdingAPI/App.config) | 0 | ||||
-rw-r--r-- | src/SMAPI/Constants.cs (renamed from src/StardewModdingAPI/Constants.cs) | 95 | ||||
-rw-r--r-- | src/SMAPI/ContentSource.cs (renamed from src/StardewModdingAPI/ContentSource.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Context.cs (renamed from src/StardewModdingAPI/Context.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/ChangeType.cs (renamed from src/StardewModdingAPI/Events/ChangeType.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/ContentEvents.cs (renamed from src/StardewModdingAPI/Events/ContentEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/ControlEvents.cs (renamed from src/StardewModdingAPI/Events/ControlEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsClickableMenuChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsClickableMenuClosed.cs (renamed from src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsControllerButtonPressed.cs (renamed from src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsControllerButtonReleased.cs (renamed from src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsControllerTriggerPressed.cs (renamed from src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsControllerTriggerReleased.cs (renamed from src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsCurrentLocationChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsGameLocationsChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsInput.cs (renamed from src/StardewModdingAPI/Events/EventArgsInput.cs) | 2 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsIntChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsIntChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsInventoryChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsKeyPressed.cs (renamed from src/StardewModdingAPI/Events/EventArgsKeyPressed.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsKeyboardStateChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsLevelUp.cs (renamed from src/StardewModdingAPI/Events/EventArgsLevelUp.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsLocationObjectsChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsMineLevelChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsMouseStateChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/EventArgsValueChanged.cs (renamed from src/StardewModdingAPI/Events/EventArgsValueChanged.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/GameEvents.cs | 96 | ||||
-rw-r--r-- | src/SMAPI/Events/GraphicsEvents.cs (renamed from src/StardewModdingAPI/Events/GraphicsEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/InputEvents.cs (renamed from src/StardewModdingAPI/Events/InputEvents.cs) | 2 | ||||
-rw-r--r-- | src/SMAPI/Events/ItemStackChange.cs (renamed from src/StardewModdingAPI/Events/ItemStackChange.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/LocationEvents.cs (renamed from src/StardewModdingAPI/Events/LocationEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/MenuEvents.cs (renamed from src/StardewModdingAPI/Events/MenuEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/MineEvents.cs (renamed from src/StardewModdingAPI/Events/MineEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/PlayerEvents.cs | 43 | ||||
-rw-r--r-- | src/SMAPI/Events/SaveEvents.cs (renamed from src/StardewModdingAPI/Events/SaveEvents.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Events/TimeEvents.cs | 37 | ||||
-rw-r--r-- | src/SMAPI/Framework/Command.cs (renamed from src/StardewModdingAPI/Framework/Command.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/CommandManager.cs (renamed from src/StardewModdingAPI/Framework/CommandManager.cs) | 9 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/AssetData.cs (renamed from src/StardewModdingAPI/Framework/Content/AssetData.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/AssetDataForDictionary.cs (renamed from src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/AssetDataForImage.cs (renamed from src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/AssetDataForObject.cs (renamed from src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/AssetInfo.cs (renamed from src/StardewModdingAPI/Framework/Content/AssetInfo.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagerShim.cs (renamed from src/StardewModdingAPI/Framework/ContentManagerShim.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/CursorPosition.cs (renamed from src/StardewModdingAPI/Framework/CursorPosition.cs) | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/DeprecationLevel.cs (renamed from src/StardewModdingAPI/Framework/DeprecationLevel.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/DeprecationManager.cs (renamed from src/StardewModdingAPI/Framework/DeprecationManager.cs) | 18 | ||||
-rw-r--r-- | src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs (renamed from src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Exceptions/SContentLoadException.cs (renamed from src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Exceptions/SParseException.cs (renamed from src/StardewModdingAPI/Framework/Exceptions/SParseException.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/GameVersion.cs (renamed from src/StardewModdingAPI/Framework/GameVersion.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/IModMetadata.cs (renamed from src/StardewModdingAPI/Framework/IModMetadata.cs) | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/InternalExtensions.cs (renamed from src/StardewModdingAPI/Framework/InternalExtensions.cs) | 16 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs (renamed from src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/InterceptingTextWriter.cs (renamed from src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/LogFileManager.cs (renamed from src/StardewModdingAPI/Framework/Logging/LogFileManager.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/BaseHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/CommandHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ContentHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/TranslationHelper.cs (renamed from src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyLoader.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs) | 132 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs) | 49 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs) | 47 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs) | 49 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs) | 47 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs) | 52 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/IInstructionHandler.cs | 34 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs) | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs | 24 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/InvalidModStateException.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadata.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs) | 12 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModResolver.cs (renamed from src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs) | 114 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Platform.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Platform.cs) | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/RewriteHelper.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs) | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs) | 27 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs) | 27 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs) | 37 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs) | 43 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs | 90 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModRegistry.cs (renamed from src/StardewModdingAPI/Framework/ModRegistry.cs) | 8 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/Manifest.cs (renamed from src/StardewModdingAPI/Framework/Models/Manifest.cs) | 12 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ManifestDependency.cs (renamed from src/StardewModdingAPI/Framework/Models/ManifestDependency.cs) | 12 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModCompatibility.cs | 55 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModDataID.cs | 85 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModDataRecord.cs | 63 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModStatus.cs | 18 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/SConfig.cs | 27 | ||||
-rw-r--r-- | src/SMAPI/Framework/Monitor.cs (renamed from src/StardewModdingAPI/Framework/Monitor.cs) | 27 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/CacheEntry.cs (renamed from src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/PrivateField.cs (renamed from src/StardewModdingAPI/Framework/Reflection/PrivateField.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/PrivateMethod.cs (renamed from src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/PrivateProperty.cs (renamed from src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/Reflector.cs (renamed from src/StardewModdingAPI/Framework/Reflection/Reflector.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/RequestExitDelegate.cs (renamed from src/StardewModdingAPI/Framework/RequestExitDelegate.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/SContentManager.cs (renamed from src/StardewModdingAPI/Framework/SContentManager.cs) | 170 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs (renamed from src/StardewModdingAPI/Framework/SGame.cs) | 83 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/JsonHelper.cs (renamed from src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/SFieldConverter.cs (renamed from src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs) | 35 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs (renamed from src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/Utilities/ContextHash.cs (renamed from src/StardewModdingAPI/Framework/Utilities/ContextHash.cs) | 3 | ||||
-rw-r--r-- | src/SMAPI/Framework/Utilities/Countdown.cs (renamed from src/StardewModdingAPI/Framework/Utilities/Countdown.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Framework/WebApiClient.cs | 73 | ||||
-rw-r--r-- | src/SMAPI/IAssetData.cs (renamed from src/StardewModdingAPI/IAssetData.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IAssetDataForDictionary.cs (renamed from src/StardewModdingAPI/IAssetDataForDictionary.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IAssetDataForImage.cs (renamed from src/StardewModdingAPI/IAssetDataForImage.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IAssetEditor.cs (renamed from src/StardewModdingAPI/IAssetEditor.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IAssetInfo.cs (renamed from src/StardewModdingAPI/IAssetInfo.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IAssetLoader.cs (renamed from src/StardewModdingAPI/IAssetLoader.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/ICommandHelper.cs (renamed from src/StardewModdingAPI/ICommandHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IContentHelper.cs (renamed from src/StardewModdingAPI/IContentHelper.cs) | 6 | ||||
-rw-r--r-- | src/SMAPI/ICursorPosition.cs (renamed from src/StardewModdingAPI/ICursorPosition.cs) | 2 | ||||
-rw-r--r-- | src/SMAPI/IManifest.cs (renamed from src/StardewModdingAPI/IManifest.cs) | 9 | ||||
-rw-r--r-- | src/SMAPI/IManifestDependency.cs (renamed from src/StardewModdingAPI/IManifestDependency.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/IMod.cs (renamed from src/StardewModdingAPI/IMod.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IModHelper.cs (renamed from src/StardewModdingAPI/IModHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IModLinked.cs (renamed from src/StardewModdingAPI/IModLinked.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IModRegistry.cs (renamed from src/StardewModdingAPI/IModRegistry.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IMonitor.cs (renamed from src/StardewModdingAPI/IMonitor.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IPrivateField.cs (renamed from src/StardewModdingAPI/IPrivateField.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IPrivateMethod.cs (renamed from src/StardewModdingAPI/IPrivateMethod.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IPrivateProperty.cs (renamed from src/StardewModdingAPI/IPrivateProperty.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/IReflectionHelper.cs (renamed from src/StardewModdingAPI/IReflectionHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/ISemanticVersion.cs (renamed from src/StardewModdingAPI/ISemanticVersion.cs) | 9 | ||||
-rw-r--r-- | src/SMAPI/ITranslationHelper.cs (renamed from src/StardewModdingAPI/ITranslationHelper.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/LogLevel.cs (renamed from src/StardewModdingAPI/LogLevel.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Metadata/CoreAssets.cs (renamed from src/StardewModdingAPI/Metadata/CoreAssets.cs) | 4 | ||||
-rw-r--r-- | src/SMAPI/Metadata/InstructionMetadata.cs | 101 | ||||
-rw-r--r-- | src/SMAPI/Mod.cs | 50 | ||||
-rw-r--r-- | src/SMAPI/PatchMode.cs (renamed from src/StardewModdingAPI/PatchMode.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Program.cs (renamed from src/StardewModdingAPI/Program.cs) | 289 | ||||
-rw-r--r-- | src/SMAPI/Properties/AssemblyInfo.cs (renamed from src/StardewModdingAPI/Properties/AssemblyInfo.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/SButton.cs (renamed from src/StardewModdingAPI/Utilities/SButton.cs) | 16 | ||||
-rw-r--r-- | src/SMAPI/SemanticVersion.cs (renamed from src/StardewModdingAPI/SemanticVersion.cs) | 133 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.config.json | 2068 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj (renamed from src/StardewModdingAPI/StardewModdingAPI.csproj) | 50 | ||||
-rw-r--r-- | src/SMAPI/Translation.cs (renamed from src/StardewModdingAPI/Translation.cs) | 0 | ||||
-rw-r--r-- | src/SMAPI/Utilities/SDate.cs (renamed from src/StardewModdingAPI/Utilities/SDate.cs) | 60 | ||||
-rw-r--r-- | src/SMAPI/icon.ico (renamed from src/StardewModdingAPI/icon.ico) | bin | 15086 -> 15086 bytes | |||
-rw-r--r-- | src/SMAPI/packages.config (renamed from src/StardewModdingAPI/packages.config) | 2 | ||||
-rw-r--r-- | src/SMAPI/steam_appid.txt (renamed from src/StardewModdingAPI/steam_appid.txt) | 0 | ||||
-rw-r--r-- | src/SMAPI/unix-launcher.sh (renamed from src/StardewModdingAPI/unix-launcher.sh) | 0 | ||||
-rw-r--r-- | src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs | 38 | ||||
-rw-r--r-- | src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs | 7 | ||||
-rw-r--r-- | src/StardewModdingAPI.AssemblyRewriters/packages.config | 4 | ||||
-rw-r--r-- | src/StardewModdingAPI.Tests/packages.config | 7 | ||||
-rw-r--r-- | src/StardewModdingAPI/Command.cs | 159 | ||||
-rw-r--r-- | src/StardewModdingAPI/Config.cs | 188 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/EventArgsCommand.cs | 28 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs | 33 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs | 27 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/EventArgsNewDay.cs | 37 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/EventArgsStringChanged.cs | 31 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/GameEvents.cs | 216 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/PlayerEvents.cs | 115 | ||||
-rw-r--r-- | src/StardewModdingAPI/Events/TimeEvents.cs | 163 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/DisabledMod.cs | 22 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/GitRelease.cs | 19 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/ModCompatibility.cs | 40 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs | 57 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs | 12 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/SConfig.cs | 24 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/UpdateHelper.cs | 37 | ||||
-rw-r--r-- | src/StardewModdingAPI/Log.cs | 320 | ||||
-rw-r--r-- | src/StardewModdingAPI/Mod.cs | 138 | ||||
-rw-r--r-- | src/StardewModdingAPI/StardewModdingAPI.config.json | 490 | ||||
-rw-r--r-- | src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs | 30 | ||||
-rw-r--r-- | src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs | 29 | ||||
-rw-r--r-- | src/TrainerMod/TrainerMod.csproj | 13 | ||||
-rw-r--r-- | src/TrainerMod/manifest.json | 6 | ||||
-rw-r--r-- | src/TrainerMod/packages.config | 2 |
241 files changed, 5816 insertions, 3720 deletions
diff --git a/src/.editorconfig b/.editorconfig index a5cdcf97..a5cdcf97 100644 --- a/src/.editorconfig +++ b/.editorconfig @@ -1,262 +1,25 @@ -# SMAPI Specific Ignores -StardewModdingAPI/bin/ -StardewModdingAPI/obj/ -TrainerMod/bin/ -TrainerMod/obj/ -StardewInjector/bin/ -StardewInjector/obj/ -packages/ -steamapps/ - -*.symlink -*.lnk -!*.exe -!*.dll - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files +# user-specific files *.suo *.user *.userosscache *.sln.docstates -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results +# build results [Dd]ebug/ -[Dd]ebugPublic/ [Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ -# Visual Studio 2015 cache/options directory +# Visual Studio cache/options .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in +# ReSharper _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages +# NuGet packages *.nupkg -# The packages folder can be ignored because of Package Restore **/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml
\ No newline at end of file diff --git a/src/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs index 882e3bda..196d67c5 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/build/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Reflection; using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.15.4.0")] -[assembly: AssemblyFileVersion("1.15.4.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/common.targets b/build/common.targets index ee138524..ee138524 100644 --- a/src/common.targets +++ b/build/common.targets diff --git a/src/prepare-install-package.targets b/build/prepare-install-package.targets index f2a2b23c..f2a2b23c 100644 --- a/src/prepare-install-package.targets +++ b/build/prepare-install-package.targets diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 52d47a4b..52d47a4b 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..bdfc5c9d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,45 @@ +**SMAPI** is an open-source modding API for [Stardew Valley](http://stardewvalley.net/) that lets +you play the game with mods. It's safely installed alongside the game's executable, and doesn't +change any of your game files. It serves six main purposes: + +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 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 game 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._ + +6. **Provide update checks.** + _SMAPI automatically checks for new versions of your installed mods, and notifies you when any + are available._ + +## Documentation +Have questions? Come [chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other +modders! + +### For players +* [Modding guides](https://stardewvalleywiki.com/Modding:Index#For_players) + +### For modders +* [Modding documentation](https://stardewvalleywiki.com/Modding:Index) +* [Mod build configuration](mod-build-config.md) +* [Release notes](release-notes.md) + +### For SMAPI developers +* [Technical docs](technical-docs.md) diff --git a/docs/imgs/SMAPI.png b/docs/imgs/SMAPI.png Binary files differdeleted file mode 100644 index 50f375b6..00000000 --- a/docs/imgs/SMAPI.png +++ /dev/null diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md new file mode 100644 index 00000000..ca750c86 --- /dev/null +++ b/docs/mod-build-config.md @@ -0,0 +1,175 @@ +The **mod build package** is an open-source NuGet package which automates the MSBuild configuration +for SMAPI mods. + +The package... + +* lets your code compile on any computer (Linux/Mac/Windows) without needing to change the assembly + references or game path. +* packages the mod into the game's `Mods` folder when you rebuild the code (configurable). +* configures Visual Studio so you can debug into the mod code when the game is running (_Windows + only_). + +## Contents +* [Install](#install) +* [Configure](#configure) +* [Troubleshoot](#troubleshoot) +* [Release notes](#release-notes) + +## Install +**When creating a new mod:** + +1. Create an empty library project. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod). +4. Compile on any platform. + +**When migrating an existing mod:** + +1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, + `StardewModdingAPI`, and `xTile`. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. Compile on any platform. + +## Configure +### Deploy files into the `Mods` folder +By default, your mod will be copied into the game's `Mods` folder (with a subfolder matching your +project name) when you rebuild the code. The package will automatically include your +`manifest.json`, any `i18n` files, and the build output. + +To add custom files to the mod folder, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). +(If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) + +You can change the mod's folder name by adding this above the first `</PropertyGroup>` in your +`.csproj`: +```xml +<ModFolderName>YourModName</ModFolderName> +``` + +If you don't want to deploy the mod automatically, you can add this: +```xml +<EnableModDeploy>False</EnableModDeploy> +``` + +### Create release zip +By default, a zip file will be created in the build output when you rebuild the code. This zip file +contains all the files needed to share your mod in the recommended format for uploading to Nexus +Mods or other sites. + +You can change the zipped folder name (and zip name) by adding this above the first +`</PropertyGroup>` in your `.csproj`: +```xml +<ModFolderName>YourModName</ModFolderName> +``` + +You can change the folder path where the zip is created like this: +```xml +<ModZipPath>$(SolutionDir)\_releases</ModZipPath> +``` + +Finally, you can disable the zip creation with this: +```xml +<EnableModZip>False</EnableModZip> +``` + +Or only create it in release builds with this: +```xml +<EnableModZip Condition="$(Configuration) != 'Release'">False</EnableModZip> +``` + +### Game path +The package usually detects where your game is installed automatically. If it can't find your game +or you have multiple installs, you can specify the path yourself. There's two ways to do that: + +* **Option 1: global game path (recommended).** + _This will apply to every project that uses the package._ + + 1. Get the full folder path containing the Stardew Valley executable. + 2. Create this file: + + platform | path + --------- | ---- + Linux/Mac | `~/stardewvalley.targets` + Windows | `%USERPROFILE%\stardewvalley.targets` + + 3. Save the file with this content: + + ```xml + <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <GamePath>PATH_HERE</GamePath> + </PropertyGroup> + </Project> + ``` + + 4. Replace `PATH_HERE` with your game path. + +* **Option 2: path in the project file.** + _You'll need to do this for each project that uses the package._ + + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the `<Project` line: + + ```xml + <PropertyGroup> + <GamePath>PATH_HERE</GamePath> + </PropertyGroup> + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + + +## Troubleshoot +### "Failed to find the game install path" +That error means the package couldn't find your game. You can specify the game path yourself; see +_[Game path](#game-path)_ above. + +## Release notes +### 2.0 +* Added: mods are now copied into the `Mods` folder automatically (configurable). +* Added: release zips are now created automatically in your build output folder (configurable). +* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. +* Added mod's version to release zip filename. +* Improved errors to simplify troubleshooting. +* Fixed release zip not having a mod folder. +* Fixed release zip failing if mod name contains characters that aren't valid in a filename. + +### 1.7.1 +* Fixed issue where i18n folders were flattened. +* The manifest/i18n files in the project now take precedence over those in the build output if both + are present. + +### 1.7 +* Added option to create release zips on build. +* Added reference to XNA's XACT library for audio-related mods. + +### 1.6 +* Added support for deploying mod files into `Mods` automatically. +* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. + +### 1.5 +* Added support for setting a custom game path globally. +* Added default GOG path on Mac. + +### 1.4 +* Fixed detection of non-default game paths on 32-bit Windows. +* Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms + mods automatically). + +### 1.3 +* Added support for non-default game paths on Windows. + +### 1.2 +* Exclude game binaries from mod build output. + +### 1.1 +* Added support for overriding the target platform. + +### 1.0 +* Initial release. +* Added support for detecting the game path automatically. +* Added support for injecting XNA/MonoGame references automatically based on the OS. +* Added support for mod builders like SilVerPLuM. diff --git a/release-notes.md b/docs/release-notes.md index 5e01a264..99e771ce 100644 --- a/release-notes.md +++ b/docs/release-notes.md @@ -1,31 +1,90 @@ -# Release notes -## 2.0 (upcoming) -<!--See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...2.0).--> - +# Release notes +## 2.0 +### Release highlights +* **Mod update checks** + SMAPI now checks if your mods have updates available, and will alert you in the console with a convenient link to the + mod page. This works with mods from the Chucklefish mod site, GitHub, or Nexus Mods. SMAPI 2.0 launches with + update-check support for over 250 existing mods, and more will be added as modders enable the feature. + +* **Mod stability warnings** + SMAPI now detects when a mod contains code which can destabilise your game or corrupt your save, and shows a warning + in the console. + +* **Simpler console** + The console is now simpler and easier to read, some commands have been streamlined, and the colors now adjust to fit + your terminal background color. + +* **New features for modders** + SMAPI 2.0 adds several features to enable new kinds of mods (see + [API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)). + + The **content API** lets you edit, inject, and reload 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). + + The **input events** unify controller + keyboard + mouse input into one event and constant for easy handling, and add + metadata like the cursor position and grab tile to support click handling. They also let you prevent the game from + receiving input, to enable new scenarios like action highjacking and UI overlays. + + The mod manifest has a few changes too: + * The **`UpdateKeys` field** lets you specify your Chucklefish, GitHub, or Nexus mod IDs. SMAPI will automatically + check for newer versions and notify the player. + * The **version field** is now a semantic string like `"1.0-alpha"`. (Mods which still use the version structure will + still work fine.) + * The **dependencies field** now lets you add optional dependencies which should be loaded first if available. + + Finally, the `SDate` utility now has a `DayOfWeek` field for more convenient date calculations, and `ISemanticVersion` + now implements `IEquatable<ISemanticVersion>`. + +* **Goodbye deprecated code** + SMAPI 2.0 removes all deprecated code to unshackle future development. That includes... + * removed all code marked obsolete; + * removed TrainerMod's `save` and `load` commands; + * removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest; + * removed support for multiple mods having the same `UniqueID` value; + * removed access to SMAPI internals through the reflection helper. + +* **Command-line install** + For power users and mod managers, the SMAPI installer can now be scripted using command-line arguments + (see [technical docs](technical-docs.md#command-line-arguments)). + +### Change log For players: -* The SMAPI console is now much simpler and easier to read. -* The SMAPI console now adjusts its colors when you have a light terminal background. +* 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 new APIs to edit, inject, and reload XNB assets loaded by the game at any time. - <small>_This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._</small> -* Added new input events. - <small>_The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._</small> +* Added support for editing, injecting, and reloading XNB data loaded by the game at any time. +* Added support for automatic mod update checks. +* Added unified input events. +* Added support for suppressing input. * Added support for optional dependencies. -* Added support for string versions (like `"1.0-alpha"`) in `manifest.json`. -* Added `IEquatable<ISemanticVersion>` to `ISemanticVersion`. +* 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<ISemanticVersion>` to `ISemanticVersion`. +* Updated Json.NET from 8.0.3 to 10.0.3. * 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 `SDate.Now()` crashing when called during the new-game intro. * Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro. +* Fixed SMAPI allowing map tilesheets with absolute or directory-climbing paths. These are now rejected even if the path exists, to avoid problems when players install the mod. 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. diff --git a/README.md b/docs/technical-docs.md index cbf0ec36..d37d327d 100644 --- a/README.md +++ b/docs/technical-docs.md @@ -1,61 +1,19 @@ -![](docs/imgs/SMAPI.png) +← [README](README.md) + +This file provides more technical documentation about SMAPI. If you only want to use or create +mods, this section isn't relevant to you; see the main README to use or create mods. ## Contents -* [What is SMAPI?](#what-is-smapi) -* **[For players](#for-players)** -* **[For mod developers](#for-mod-developers)** -* [For SMAPI developers](#for-smapi-developers) +* [Development](#development) * [Compiling from source](#compiling-from-source) * [Debugging a local build](#debugging-a-local-build) * [Preparing a release](#preparing-a-release) -* [Advanced usage](#advanced-usage) +* [Customisation](#customisation) * [Configuration file](#configuration-file) * [Command-line arguments](#command-line-arguments) + * [Compile flags](#compile-flags) -## 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._ - +## Development ### Compiling from source Using an official SMAPI release is recommended for most users. @@ -84,13 +42,13 @@ on the wiki for the first-time setup. build type | format | example :--------- | :-------------------------------- | :------ - dev build | `<version>-alpha.<timestamp>` | `1.0-alpha.20171230` - prerelease | `<version>-prerelease.<ID>` | `1.0-prerelease.2` - release | `<version>` | `1.0` + dev build | `<version>-alpha.<timestamp>` | `2.0-alpha.20171230` + prerelease | `<version>-prerelease.<ID>` | `2.0-prerelease.2` + release | `<version>` | `2.0` 2. In Windows: 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI <version>` (e.g. `SMAPI 1.0`). + 2. Rename `bin/Packaged` to `SMAPI <version>` (e.g. `SMAPI 2.0`). 2. Transfer the `SMAPI <version>` 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._ @@ -101,7 +59,7 @@ on the wiki for the first-time setup. 3. If you did everything right so far, you should have a folder like this: ``` - SMAPI-1.x/ + SMAPI-2.x/ install.exe readme.txt internal/ @@ -139,25 +97,19 @@ on the wiki for the first-time setup. * delete `internal/Windows/StardewModdingAPI.xml`. 7. Compress the two folders into `SMAPI <version>.zip` and `SMAPI <version> for developers.zip`. -## Advanced usage +## Customisation ### 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). +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. - -Advanced fields (changing these isn't recommended and may destabilise your game): - -field | purpose ------ | ------- -`DisabledMods` | A list of mods to consider obsolete and not load. -`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. This can be used to force SMAPI to load an incompatible mod, though that isn't recommended. +`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: @@ -183,4 +135,3 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. -`SMAPI_1_x` | Sets legacy SMAPI 1._x_ mode, disables SMAPI 2.0 features, and enables deprecated code. This will be removed when SMAPI 2.0 is released. 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/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs index ee68f1d5..a7f100f2 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs +++ b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs @@ -2,16 +2,16 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers +namespace StardewModdingAPI.AssemblyRewriters { - /// <summary>Wraps <see cref="SpriteBatch"/> methods that are incompatible when converting compiled code between MonoGame and XNA.</summary> - public class SpriteBatchWrapper : SpriteBatch + /// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows.</summary> + public class SpriteBatchMethods : SpriteBatch { /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - public SpriteBatchWrapper(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } /**** @@ -56,4 +56,4 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); } } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index 8416bd51..651b822d 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> @@ -30,44 +30,15 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> - <Reference Include="Mono.Cecil, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL"> - <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll</HintPath> - <Private>True</Private> - </Reference> - <Reference Include="Mono.Cecil.Mdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL"> - <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll</HintPath> - <Private>True</Private> - </Reference> - <Reference Include="Mono.Cecil.Pdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL"> - <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath> - <Private>True</Private> - </Reference> <Reference Include="System" /> </ItemGroup> <ItemGroup> - <Compile Include="..\GlobalAssemblyInfo.cs"> + <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> - <Compile Include="Finders\EventFinder.cs" /> - <Compile Include="Finders\PropertyFinder.cs" /> - <Compile Include="Finders\FieldFinder.cs" /> - <Compile Include="Finders\MethodFinder.cs" /> - <Compile Include="Finders\TypeFinder.cs" /> - <Compile Include="IncompatibleInstructionException.cs" /> - <Compile Include="RewriteHelper.cs" /> - <Compile Include="IInstructionRewriter.cs" /> - <Compile Include="Platform.cs" /> - <Compile Include="PlatformAssemblyMap.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Rewriters\TypeReferenceRewriter.cs" /> - <Compile Include="Rewriters\FieldReplaceRewriter.cs" /> - <Compile Include="Rewriters\FieldToPropertyRewriter.cs" /> - <Compile Include="Rewriters\MethodParentRewriter.cs" /> - <Compile Include="Rewriters\Wrappers\SpriteBatchWrapper.cs" /> - </ItemGroup> - <ItemGroup> - <None Include="packages.config" /> + <Compile Include="SpriteBatchMethods.cs" /> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\common.targets" /> + <Import Project="..\..\build\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs new file mode 100644 index 00000000..48305cb8 --- /dev/null +++ b/src/SMAPI.Common/Models/ModInfoModel.cs @@ -0,0 +1,52 @@ +namespace StardewModdingAPI.Common.Models +{ + /// <summary>Generic metadata about a mod.</summary> + internal class ModInfoModel + { + /********* + ** Accessors + *********/ + /// <summary>The mod name.</summary> + public string Name { get; set; } + + /// <summary>The mod's semantic version number.</summary> + public string Version { get; set; } + + /// <summary>The mod's web URL.</summary> + public string Url { get; set; } + + /// <summary>The error message indicating why the mod is invalid (if applicable).</summary> + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + public ModInfoModel() + { + // needed for JSON deserialising + } + + + /// <summary>Construct an instance.</summary> + /// <param name="name">The mod name.</param> + /// <param name="version">The mod's semantic version number.</param> + /// <param name="url">The mod's web URL.</param> + /// <param name="error">The error message indicating why the mod is invalid (if applicable).</param> + 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 + } + + /// <summary>Construct an instance.</summary> + /// <param name="error">The error message indicating why the mod is invalid.</param> + public ModInfoModel(string error) + { + this.Error = error; + } + } +} diff --git a/src/SMAPI.Common/Models/ModSeachModel.cs b/src/SMAPI.Common/Models/ModSeachModel.cs new file mode 100644 index 00000000..13b05d2d --- /dev/null +++ b/src/SMAPI.Common/Models/ModSeachModel.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Common.Models +{ + /// <summary>Specifies mods whose update-check info to fetch.</summary> + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// <summary>The namespaced mod keys to search.</summary> + public string[] ModKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// <summary>Construct an instance.</summary> + /// <param name="modKeys">The namespaced mod keys to search.</param> + public ModSearchModel(IEnumerable<string> modKeys) + { + this.ModKeys = modKeys.ToArray(); + } + } +} diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs new file mode 100644 index 00000000..193d23f9 --- /dev/null +++ b/src/SMAPI.Common/SemanticVersionImpl.cs @@ -0,0 +1,185 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Common +{ + /// <summary>A low-level implementation of a semantic version with an optional release tag.</summary> + /// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks> + internal class SemanticVersionImpl + { + /********* + ** Accessors + *********/ + /// <summary>The major version incremented for major API changes.</summary> + public int Major { get; } + + /// <summary>The minor version incremented for backwards-compatible changes.</summary> + public int Minor { get; } + + /// <summary>The patch version for backwards-compatible bug fixes.</summary> + public int Patch { get; } + + /// <summary>An optional prerelease tag.</summary> + public string Tag { get; } + + /// <summary>A regular expression matching a semantic version string.</summary> + /// <remarks> + /// This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// </remarks> + internal static readonly Regex Regex = new Regex(@"^(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="major">The major version incremented for major API changes.</param> + /// <param name="minor">The minor version incremented for backwards-compatible changes.</param> + /// <param name="patch">The patch version for backwards-compatible bug fixes.</param> + /// <param name="tag">An optional prerelease tag.</param> + public SemanticVersionImpl(int major, int minor, int patch, string tag = null) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.Tag = this.GetNormalisedTag(tag); + } + + /// <summary>Construct an instance.</summary> + /// <param name="version">The semantic version string.</param> + /// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception> + /// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception> + public SemanticVersionImpl(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersionImpl.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.Major = int.Parse(match.Groups["major"].Value); + this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> + /// <param name="other">The version to compare with this instance.</param> + /// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception> + public int CompareTo(SemanticVersionImpl other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + + /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> + /// <param name="otherMajor">The major version to compare with this instance.</param> + /// <param name="otherMinor">The minor version to compare with this instance.</param> + /// <param name="otherPatch">The patch version to compare with this instance.</param> + /// <param name="otherTag">The prerelease tag to compare with this instance.</param> + public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.Major != otherMajor) + return this.Major.CompareTo(otherMajor); + if (this.Minor != otherMinor) + return this.Minor.CompareTo(otherMinor); + if (this.Patch != otherPatch) + return this.Patch.CompareTo(otherPatch); + if (this.Tag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Tag.Split('.', '-'); + string[] otherParts = otherTag.Split('.', '-'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// <summary>Get a string representation of the version.</summary> + public override string ToString() + { + // version + string result = this.Patch != 0 + ? $"{this.Major}.{this.Minor}.{this.Patch}" + : $"{this.Major}.{this.Minor}"; + + // tag + string tag = this.Tag; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// <summary>Parse a version string without throwing an exception if it fails.</summary> + /// <param name="version">The version string.</param> + /// <param name="parsed">The parsed representation.</param> + /// <returns>Returns whether parsing the version succeeded.</returns> + internal static bool TryParse(string version, out SemanticVersionImpl parsed) + { + try + { + parsed = new SemanticVersionImpl(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Get a normalised build tag.</summary> + /// <param name="tag">The tag to normalise.</param> + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation + return null; + return tag; + } + } +} diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems new file mode 100644 index 00000000..223b0d5c --- /dev/null +++ b/src/SMAPI.Common/StardewModdingAPI.Common.projitems @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <HasSharedItems>true</HasSharedItems> + <SharedGUID>2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc</SharedGUID> + </PropertyGroup> + <PropertyGroup Label="Configuration"> + <Import_RootNamespace>StardewModdingAPI.Common</Import_RootNamespace> + </PropertyGroup> + <ItemGroup> + <Compile Include="$(MSBuildThisFileDirectory)Models\ModSeachModel.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Models\ModInfoModel.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)SemanticVersionImpl.cs" /> + </ItemGroup> + <ItemGroup> + <Folder Include="$(MSBuildThisFileDirectory)Models\" /> + </ItemGroup> +</Project>
\ No newline at end of file diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.shproj b/src/SMAPI.Common/StardewModdingAPI.Common.shproj new file mode 100644 index 00000000..0ef29144 --- /dev/null +++ b/src/SMAPI.Common/StardewModdingAPI.Common.shproj @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <ProjectGuid>2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc</ProjectGuid> + <MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion> + </PropertyGroup> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" /> + <PropertyGroup /> + <Import Project="StardewModdingAPI.Common.projitems" Label="Shared" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" /> +</Project> diff --git a/src/StardewModdingAPI.Installer/Enums/Platform.cs b/src/SMAPI.Installer/Enums/Platform.cs index 9bcaa3c3..9bcaa3c3 100644 --- a/src/StardewModdingAPI.Installer/Enums/Platform.cs +++ b/src/SMAPI.Installer/Enums/Platform.cs diff --git a/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs b/src/SMAPI.Installer/Enums/ScriptAction.cs index e62b2a7c..e62b2a7c 100644 --- a/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs +++ b/src/SMAPI.Installer/Enums/ScriptAction.cs diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 01288f33..1a132e54 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -135,11 +135,6 @@ namespace StardewModdingApi.Installer /// </remarks> public void Run(string[] args) { -#if SMAPI_1_x - bool installArg = false; - bool uninstallArg = false; - string gamePathArg = null; -#else /**** ** read command-line arguments ****/ @@ -160,7 +155,6 @@ namespace StardewModdingApi.Installer if (pathIndex >= 1 && args.Length >= pathIndex) gamePathArg = args[pathIndex]; } -#endif /**** ** collect details diff --git a/src/StardewModdingAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index 8f328ecf..8f328ecf 100644 --- a/src/StardewModdingAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs diff --git a/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs b/src/SMAPI.Installer/Properties/AssemblyInfo.cs index 3a6a4648..3a6a4648 100644 --- a/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs +++ b/src/SMAPI.Installer/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 58ce519c..f8e368a4 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> @@ -36,7 +36,7 @@ <Reference Include="System" /> </ItemGroup> <ItemGroup> - <Compile Include="..\GlobalAssemblyInfo.cs"> + <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> <Compile Include="Enums\ScriptAction.cs" /> @@ -51,6 +51,6 @@ </Content> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\common.targets" /> - <Import Project="$(SolutionDir)\prepare-install-package.targets" /> + <Import Project="..\..\build\common.targets" /> + <Import Project="..\..\build\prepare-install-package.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt index eb27ac52..eb27ac52 100644 --- a/src/StardewModdingAPI.Installer/readme.txt +++ b/src/SMAPI.Installer/readme.txt diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs new file mode 100644 index 00000000..a5725a81 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using StardewModdingAPI.ModBuildConfig.Framework; + +namespace StardewModdingAPI.ModBuildConfig +{ + /// <summary>A build task which deploys the mod files and prepares a release zip.</summary> + public class DeployModTask : Task + { + /********* + ** Accessors + *********/ + /// <summary>The name of the mod folder.</summary> + [Required] + public string ModFolderName { get; set; } + + /// <summary>The absolute or relative path to the folder which should contain the generated zip file.</summary> + [Required] + public string ModZipPath { get; set; } + + /// <summary>The folder containing the project files.</summary> + [Required] + public string ProjectDir { get; set; } + + /// <summary>The folder containing the build output.</summary> + [Required] + public string TargetDir { get; set; } + + /// <summary>The folder containing the game files.</summary> + [Required] + public string GameDir { get; set; } + + /// <summary>Whether to enable copying the mod files into the game's Mods folder.</summary> + [Required] + public bool EnableModDeploy { get; set; } + + /// <summary>Whether to enable the release zip.</summary> + [Required] + public bool EnableModZip { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>When overridden in a derived class, executes the task.</summary> + /// <returns>true if the task successfully executed; otherwise, false.</returns> + public override bool Execute() + { + if (!this.EnableModDeploy && !this.EnableModZip) + return true; // nothing to do + + try + { + // get mod info + ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir); + + // deploy mod files + if (this.EnableModDeploy) + { + string outputPath = Path.Combine(this.GameDir, "Mods", this.EscapeInvalidFilenameCharacters(this.ModFolderName)); + this.Log.LogMessage(MessageImportance.High, $"The mod build package is copying the mod files to {outputPath}..."); + this.CreateModFolder(package.GetFiles(), outputPath); + } + + // create release zip + if (this.EnableModZip) + { + this.Log.LogMessage(MessageImportance.High, $"The mod build package is generating a release zip at {this.ModZipPath} for {this.ModFolderName}..."); + this.CreateReleaseZip(package.GetFiles(), this.ModFolderName, package.GetManifestVersion(), this.ModZipPath); + } + + return true; + } + catch (UserErrorException ex) + { + this.Log.LogErrorFromException(ex); + return false; + } + catch (Exception ex) + { + this.Log.LogError($"The mod build package failed trying to deploy the mod.\n{ex}"); + return false; + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Copy the mod files into the game's mod folder.</summary> + /// <param name="files">The files to include.</param> + /// <param name="modFolderPath">The folder path to create with the mod files.</param> + private void CreateModFolder(IDictionary<string, FileInfo> files, string modFolderPath) + { + foreach (var entry in files) + { + string fromPath = entry.Value.FullName; + string toPath = Path.Combine(modFolderPath, entry.Key); + + // ReSharper disable once AssignNullToNotNullAttribute -- not applicable in this context + Directory.CreateDirectory(Path.GetDirectoryName(toPath)); + + File.Copy(fromPath, toPath, overwrite: true); + } + } + + /// <summary>Create a release zip in the recommended format for uploading to mod sites.</summary> + /// <param name="files">The files to include.</param> + /// <param name="modName">The name of the mod.</param> + /// <param name="modVersion">The mod version string.</param> + /// <param name="outputFolderPath">The absolute or relative path to the folder which should contain the generated zip file.</param> + private void CreateReleaseZip(IDictionary<string, FileInfo> files, string modName, string modVersion, string outputFolderPath) + { + // get names + string zipName = this.EscapeInvalidFilenameCharacters($"{modName} {modVersion}.zip"); + string folderName = this.EscapeInvalidFilenameCharacters(modName); + string zipPath = Path.Combine(outputFolderPath, zipName); + + // create zip file + Directory.CreateDirectory(outputFolderPath); + using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) + using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + { + foreach (var fileEntry in files) + { + string relativePath = fileEntry.Key; + FileInfo file = fileEntry.Value; + + // get file info + string filePath = file.FullName; + string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/'); + + // add to zip + using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) + fileStream.CopyTo(fileStreamInZip); + } + } + } + + /// <summary>Get a copy of a filename with all invalid filename characters substituted.</summary> + /// <param name="name">The filename.</param> + private string EscapeInvalidFilenameCharacters(string name) + { + foreach (char invalidChar in Path.GetInvalidFileNameChars()) + name = name.Replace(invalidChar, '.'); + return name; + } + } +} diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs new file mode 100644 index 00000000..64262dc2 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web.Script.Serialization; +using StardewModdingAPI.Common; + +namespace StardewModdingAPI.ModBuildConfig.Framework +{ + /// <summary>Manages the files that are part of a mod package.</summary> + internal class ModFileManager + { + /********* + ** Properties + *********/ + /// <summary>The name of the manifest file.</summary> + private readonly string ManifestFileName = "manifest.json"; + + /// <summary>The files that are part of the package.</summary> + private readonly IDictionary<string, FileInfo> Files; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="projectDir">The folder containing the project files.</param> + /// <param name="targetDir">The folder containing the build output.</param> + /// <exception cref="UserErrorException">The mod package isn't valid.</exception> + public ModFileManager(string projectDir, string targetDir) + { + this.Files = new Dictionary<string, FileInfo>(StringComparer.InvariantCultureIgnoreCase); + + // validate paths + if (!Directory.Exists(projectDir)) + throw new UserErrorException("Could not create mod package because the project folder wasn't found."); + if (!Directory.Exists(targetDir)) + throw new UserErrorException("Could not create mod package because no build output was found."); + + // project manifest + bool hasProjectManifest = false; + { + FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json")); + if (manifest.Exists) + { + this.Files[this.ManifestFileName] = manifest; + hasProjectManifest = true; + } + } + + // project i18n files + bool hasProjectTranslations = false; + DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n")); + if (translationsFolder.Exists) + { + foreach (FileInfo file in translationsFolder.EnumerateFiles()) + this.Files[Path.Combine("i18n", file.Name)] = file; + hasProjectTranslations = true; + } + + // build output + DirectoryInfo buildFolder = new DirectoryInfo(targetDir); + foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories)) + { + // get relative paths + string relativePath = file.FullName.Replace(buildFolder.FullName, ""); + string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, ""); + + // prefer project manifest/i18n files + if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName)) + continue; + if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n")) + continue; + + // ignore release zips + if (this.EqualsInvariant(file.Extension, ".zip")) + continue; + + // ignore Json.NET (bundled into SMAPI) + if (this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")) + continue; + + // add file + this.Files[relativePath] = file; + } + + // check for missing manifest + if (!this.Files.ContainsKey(this.ManifestFileName)) + throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); + + // check for missing DLL + // ReSharper disable once SimplifyLinqExpression + if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) + throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); + } + + /// <summary>Get the files in the mod package.</summary> + public IDictionary<string, FileInfo> GetFiles() + { + return new Dictionary<string, FileInfo>(this.Files, StringComparer.InvariantCultureIgnoreCase); + } + + /// <summary>Get a semantic version from the mod manifest.</summary> + /// <exception cref="UserErrorException">The manifest is missing or invalid.</exception> + public string GetManifestVersion() + { + // get manifest file + if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile)) + throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor + + // read content + string json = File.ReadAllText(manifestFile.FullName); + if (string.IsNullOrWhiteSpace(json)) + throw new UserErrorException("The mod's manifest must not be empty."); + + // parse JSON + IDictionary<string, object> data; + try + { + data = this.Parse(json); + } + catch (Exception ex) + { + throw new UserErrorException($"The mod's manifest couldn't be parsed. It doesn't seem to be valid JSON.\n{ex}"); + } + + // get version field + object versionObj = data.ContainsKey("Version") ? data["Version"] : null; + if (versionObj == null) + throw new UserErrorException("The mod's manifest must have a version field."); + + // get version string + if (versionObj is IDictionary<string, object> versionFields) // SMAPI 1.x + { + int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0; + int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; + int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; + string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; + return new SemanticVersionImpl(major, minor, patch, tag).ToString(); + } + return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ + } + + + /********* + ** Private methods + *********/ + /// <summary>Get a case-insensitive dictionary matching the given JSON.</summary> + /// <param name="json">The JSON to parse.</param> + private IDictionary<string, object> Parse(string json) + { + IDictionary<string, object> MakeCaseInsensitive(IDictionary<string, object> dict) + { + foreach (var field in dict.ToArray()) + { + if (field.Value is IDictionary<string, object> value) + dict[field.Key] = MakeCaseInsensitive(value); + } + return new Dictionary<string, object>(dict, StringComparer.InvariantCultureIgnoreCase); + } + + IDictionary<string, object> data = (IDictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json); + return MakeCaseInsensitive(data); + } + + /// <summary>Get whether a string is equal to another case-insensitively.</summary> + /// <param name="str">The string value.</param> + /// <param name="other">The string to compare with.</param> + private bool EqualsInvariant(string str, string other) + { + return str.Equals(other, StringComparison.InvariantCultureIgnoreCase); + } + } +} diff --git a/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs b/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs new file mode 100644 index 00000000..64e31c29 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.ModBuildConfig.Framework +{ + /// <summary>A user error whose message can be displayed to the user.</summary> + internal class UserErrorException : Exception + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="message">The error message.</param> + public UserErrorException(string message) + : base(message) { } + } +} diff --git a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..6ef2d568 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.ModBuildConfig")] +[assembly: AssemblyDescription("")] +[assembly: Guid("ea4f1e80-743f-4a1d-9757-ae66904a196a")] +[assembly: ComVisible(false)] +[assembly: AssemblyVersion("2.0.1.0")] +[assembly: AssemblyFileVersion("2.0.1.0")] diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj new file mode 100644 index 00000000..e04f09a7 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">x86</Platform> + <ProjectGuid>{EA4F1E80-743F-4A1D-9757-AE66904A196A}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace> + <AssemblyName>StardewModdingAPI.ModBuildConfig</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="Microsoft.Build" /> + <Reference Include="Microsoft.Build.Framework" /> + <Reference Include="Microsoft.Build.Utilities.v4.0" /> + <Reference Include="System" /> + <Reference Include="System.IO.Compression" /> + <Reference Include="System.Web.Extensions" /> + </ItemGroup> + <ItemGroup> + <Compile Include="DeployModTask.cs" /> + <Compile Include="Framework\UserErrorException.cs" /> + <Compile Include="Framework\ModFileManager.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="assets\nuget-icon.pdn" /> + <None Include="build\smapi.targets"> + <SubType>Designer</SubType> + </None> + <None Include="package.nuspec"> + <SubType>Designer</SubType> + </None> + </ItemGroup> + <ItemGroup> + <Content Include="assets\nuget-icon.png" /> + </ItemGroup> + <Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" /> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> +</Project>
\ No newline at end of file diff --git a/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn b/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn Binary files differnew file mode 100644 index 00000000..7bd5c0c5 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn diff --git a/src/SMAPI.ModBuildConfig/assets/nuget-icon.png b/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Binary files differnew file mode 100644 index 00000000..611cdf88 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/assets/nuget-icon.png diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets new file mode 100644 index 00000000..c0319e22 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -0,0 +1,144 @@ +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <!--********************************************* + ** Import build tasks + **********************************************--> + <UsingTask TaskName="DeployModTask" AssemblyFile="StardewModdingAPI.ModBuildConfig.dll" /> + + <!--********************************************* + ** Find the basic mod metadata + **********************************************--> + <!-- import developer's custom settings (if any) --> + <Import Condition="$(OS) != 'Windows_NT' AND Exists('$(HOME)\stardewvalley.targets')" Project="$(HOME)\stardewvalley.targets" /> + <Import Condition="$(OS) == 'Windows_NT' AND Exists('$(USERPROFILE)\stardewvalley.targets')" Project="$(USERPROFILE)\stardewvalley.targets" /> + + <!-- set setting defaults --> + <PropertyGroup> + <!-- map legacy settings --> + <ModFolderName Condition="'$(ModFolderName)' == '' AND '$(DeployModFolderName)' != ''">$(DeployModFolderName)</ModFolderName> + <ModZipPath Condition="'$(ModZipPath)' == '' AND '$(DeployModZipTo)' != ''">$(DeployModZipTo)</ModZipPath> + + <!-- set default settings --> + <ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName> + <ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath> + <EnableModDeploy Condition="'$(EnableModDeploy)' == ''">True</EnableModDeploy> + <EnableModZip Condition="'$(EnableModZip)' == ''">True</EnableModZip> + </PropertyGroup> + + <!-- find platform + game path --> + <Choose> + <When Condition="$(OS) == 'Unix' OR $(OS) == 'OSX'"> + <PropertyGroup> + <!-- Linux --> + <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/GOG Games/Stardew Valley/game</GamePath> + <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.local/share/Steam/steamapps/common/Stardew Valley</GamePath> + + <!-- Mac (may be 'Unix' or 'OSX') --> + <GamePath Condition="!Exists('$(GamePath)')">/Applications/Stardew Valley.app/Contents/MacOS</GamePath> + <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS</GamePath> + </PropertyGroup> + </When> + <When Condition="$(OS) == 'Windows_NT'"> + <PropertyGroup> + <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath> + <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath> + <GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath> + <GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath> + </PropertyGroup> + </When> + </Choose> + + + <!--********************************************* + ** Inject the assembly references and debugging configuration + **********************************************--> + <Choose> + <When Condition="$(OS) == 'Windows_NT'"> + <!-- references --> + <ItemGroup> + <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> + <Private>false</Private> + </Reference> + <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> + <Private>false</Private> + </Reference> + <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> + <Private>false</Private> + </Reference> + <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> + <Private>false</Private> + </Reference> + <Reference Include="Stardew Valley"> + <HintPath>$(GamePath)\Stardew Valley.exe</HintPath> + <Private>false</Private> + </Reference> + <Reference Include="StardewModdingAPI"> + <HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath> + <Private>false</Private> + </Reference> + <Reference Include="xTile, Version=2.0.4.0, Culture=neutral, processorArchitecture=x86"> + <HintPath>$(GamePath)\xTile.dll</HintPath> + <Private>false</Private> + <SpecificVersion>False</SpecificVersion> + </Reference> + </ItemGroup> + + <!-- launch game for debugging --> + <PropertyGroup> + <StartAction>Program</StartAction> + <StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram> + <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory> + </PropertyGroup> + </When> + <Otherwise> + <!-- references --> + <ItemGroup> + <Reference Include="MonoGame.Framework"> + <HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath> + <Private>false</Private> + <SpecificVersion>False</SpecificVersion> + </Reference> + <Reference Include="StardewValley"> + <HintPath>$(GamePath)\StardewValley.exe</HintPath> + <Private>false</Private> + </Reference> + <Reference Include="StardewModdingAPI"> + <HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath> + <Private>false</Private> + </Reference> + <Reference Include="xTile"> + <HintPath>$(GamePath)\xTile.dll</HintPath> + <Private>false</Private> + </Reference> + </ItemGroup> + </Otherwise> + </Choose> + + + <!--********************************************* + ** Deploy mod files & create release zip after build + **********************************************--> + <!-- if game path or OS is invalid, show one user-friendly error instead of a slew of reference errors --> + <Target Name="BeforeBuild"> + <Error Condition="'$(OS)' != 'OSX' AND '$(OS)' != 'Unix' AND '$(OS)' != 'Windows_NT'" Text="The mod build package doesn't recognise OS type '$(OS)'." /> + + <Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see details at https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#game-path." /> + <Error Condition="'$(OS)' == 'Windows_NT' AND !Exists('$(GamePath)\Stardew Valley.exe')" Text="The mod build package found a a game folder at $(GamePath), but it doesn't contain the Stardew Valley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." /> + <Error Condition="'$(OS)' != 'Windows_NT' AND !Exists('$(GamePath)\StardewValley.exe')" Text="The mod build package found a a game folder at $(GamePath), but it doesn't contain the StardewValley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." /> + <Error Condition="!Exists('$(GamePath)\StardewModdingAPI.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." /> + </Target> + + <!-- deploy mod files & create release zip --> + <Target Name="AfterBuild"> + <DeployModTask + ModFolderName="$(ModFolderName)" + ModZipPath="$(ModZipPath)" + + EnableModDeploy="$(EnableModDeploy)" + EnableModZip="$(EnableModZip)" + + ProjectDir="$(ProjectDir)" + TargetDir="$(TargetDir)" + GameDir="$(GamePath)" + /> + </Target> +</Project> diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec new file mode 100644 index 00000000..b1b228de --- /dev/null +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata> + <id>Pathoschild.Stardew.ModBuildConfig</id> + <version>2.0.1</version> + <title>Build package for SMAPI mods</title> + <authors>Pathoschild</authors> + <owners>Pathoschild</owners> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <licenseUrl>https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt</licenseUrl> + <projectUrl>https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme</projectUrl> + <iconUrl>https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png</iconUrl> + <description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods.</description> + <releaseNotes> + 2.0: + - Added: mods are now copied into the `Mods` folder automatically (configurable). + - Added: release zips are now created automatically in your build output folder (configurable). + - Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. + - Added mod's version to release zip filename. + - Improved errors to simplify troubleshooting. + - Fixed release zip not having a mod folder. + - Fixed release zip failing if mod name contains characters that aren't valid in a filename. + + 2.0.1: + - Fixed mod deploy failing to create subfolders if they don't already exist. + </releaseNotes> + </metadata> + <files> + <file src="build/smapi.targets" target="build/Pathoschild.Stardew.ModBuildConfig.targets" /> + <file src="bin/StardewModdingAPI.ModBuildConfig.dll" target="build/StardewModdingAPI.ModBuildConfig.dll" /> + </files> +</package> diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index fc84ca29..051ffe99 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(rootFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray(); + 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."); @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(modFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -84,13 +84,13 @@ namespace StardewModdingAPI.Tests.Core File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray(); + 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.Compatibility, "The compatibility record should be null since we didn't provide one."); + 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."); @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Tests.Core [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")); + new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -130,21 +130,25 @@ namespace StardewModdingAPI.Tests.Core mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); // 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' compatibility.")] - public void ValidateManifests_ModCompatibility_AssumeBroken_Fails() + [Test(Description = "Assert that validation fails if the mod has 'assume broken' status.")] + public void ValidateManifests_ModStatus_AssumeBroken_Fails() { // arrange Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" }}); + 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")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata."); @@ -159,7 +163,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata."); @@ -173,13 +177,12 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata."); } -#if !SMAPI_1_x [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] public void ValidateManifests_DuplicateUniqueID_Fails() { @@ -191,13 +194,12 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mod); // act - new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); // assert modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the first mod with a unique ID."); modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the second mod with a unique ID."); } -#endif [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] public void ValidateManifests_Valid_Passes() @@ -213,12 +215,12 @@ namespace StardewModdingAPI.Tests.Core // arrange Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mock.Setup(p => p.Compatibility).Returns(() => null); + 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")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>()); // assert // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. @@ -423,7 +425,6 @@ namespace StardewModdingAPI.Tests.Core Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); } -#if !SMAPI_1_x [Test(Description = "Assert that optional dependencies are sorted correctly if present.")] public void ProcessDependencies_IfOptional() { @@ -455,7 +456,6 @@ namespace StardewModdingAPI.Tests.Core 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."); } -#endif /********* @@ -527,7 +527,7 @@ namespace StardewModdingAPI.Tests.Core private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false) { Mock<IModMetadata> mod = new Mock<IModMetadata>(MockBehavior.Strict); - mod.Setup(p => p.Compatibility).Returns(() => null); + 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); @@ -543,14 +543,14 @@ namespace StardewModdingAPI.Tests.Core /// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary> /// <param name="mod">The mock mod metadata.</param> - /// <param name="compatibility">The compatibility record to set.</param> - private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModCompatibility compatibility = null) + /// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param> + private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecord modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.Compatibility).Returns(() => null); + 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.Compatibility).Returns(compatibility); + mod.Setup(p => p.DataRecord).Returns(modRecord); } } } diff --git a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs index 8511e765..63404a41 100644 --- a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModHelpers; using StardewValley; diff --git a/src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs b/src/SMAPI.Tests/Properties/AssemblyInfo.cs index ee09145b..ee09145b 100644 --- a/src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs +++ b/src/SMAPI.Tests/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs index 99835d92..99835d92 100644 --- a/src/StardewModdingAPI.Tests/Sample.cs +++ b/src/SMAPI.Tests/Sample.cs diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index f3dbcdd4..c7a67306 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -30,22 +30,23 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> - <Reference Include="Castle.Core, Version=4.1.1.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> - <HintPath>..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll</HintPath> + <Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> + <HintPath>..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll</HintPath> </Reference> - <Reference Include="Moq, Version=4.7.99.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> - <HintPath>..\packages\Moq.4.7.99\lib\net45\Moq.dll</HintPath> + <Reference Include="Moq, Version=4.7.142.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> + <HintPath>..\packages\Moq.4.7.142\lib\net45\Moq.dll</HintPath> </Reference> - <Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> + <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> - <Reference Include="nunit.framework, Version=3.7.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll</HintPath> + <Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> + <HintPath>..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="System" /> + <Reference Include="System.Configuration" /> </ItemGroup> <ItemGroup> - <Compile Include="..\GlobalAssemblyInfo.cs"> + <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> <Compile Include="Utilities\SemanticVersionTests.cs" /> @@ -59,11 +60,11 @@ <None Include="packages.config" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\StardewModdingAPI\StardewModdingAPI.csproj"> + <ProjectReference Include="..\SMAPI\StardewModdingAPI.csproj"> <Project>{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}</Project> <Name>StardewModdingAPI</Name> </ProjectReference> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\common.targets" /> + <Import Project="..\..\build\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index 25acbaf3..86a0d3d0 100644 --- a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -69,6 +69,8 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase(01, "Spring", 1)] // seasons are case-sensitive [TestCase(01, "springs", 1)] // invalid season name [TestCase(-1, "spring", 1)] // day < 0 + [TestCase(0, "spring", 1)] // day zero + [TestCase(0, "spring", 2)] // day zero [TestCase(29, "spring", 1)] // day > 28 [TestCase(01, "spring", -1)] // year < 1 [TestCase(01, "spring", 0)] // year < 1 diff --git a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 03cd26c9..03cd26c9 100644 --- a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config new file mode 100644 index 00000000..7a91e807 --- /dev/null +++ b/src/SMAPI.Tests/packages.config @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="Castle.Core" version="4.2.1" targetFramework="net45" /> + <package id="Moq" version="4.7.142" targetFramework="net45" /> + <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" /> + <package id="NUnit" version="3.8.1" targetFramework="net45" /> +</packages>
\ No newline at end of file diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs new file mode 100644 index 00000000..a671ddca --- /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.Common.Models; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Controllers +{ + /// <summary>Provides an API to perform mod update checks.</summary> + [Produces("application/json")] + [Route("api/{version:semanticVersion}/[controller]")] + internal class ModsController : Controller + { + /********* + ** Properties + *********/ + /// <summary>The mod repositories which provide mod metadata.</summary> + private readonly IDictionary<string, IModRepository> Repositories; + + /// <summary>The cache in which to store mod metadata.</summary> + private readonly IMemoryCache Cache; + + /// <summary>The number of minutes update checks should be cached before refetching them.</summary> + private readonly int CacheMinutes; + + /// <summary>A regex which matches SMAPI-style semantic version.</summary> + private readonly string VersionRegex; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="cache">The cache in which to store mod metadata.</param> + /// <param name="configProvider">The config settings for mod update checks.</param> + public ModsController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> 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); + } + + /// <summary>Fetch version metadata for the given mods.</summary> + /// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param> + [HttpGet] + public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary<string, ModInfoModel>(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// <summary>Fetch version metadata for the given mods.</summary> + /// <param name="search">The mod search criteria.</param> + [HttpPost] + public async Task<IDictionary<string, ModInfoModel>> 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<string, ModInfoModel> result = new Dictionary<string, ModInfoModel>(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 + *********/ + /// <summary>Parse a namespaced mod ID.</summary> + /// <param name="raw">The raw mod ID to parse.</param> + /// <param name="vendorKey">The parsed vendor key.</param> + /// <param name="modID">The parsed mod ID.</param> + /// <returns>Returns whether the value could be parsed.</returns> + 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 +{ + /// <summary>The config settings for mod update checks.</summary> + public class ModUpdateCheckConfig + { + /********* + ** Accessors + *********/ + /**** + ** General + ****/ + /// <summary>The number of minutes update checks should be cached before refetching them.</summary> + public int CacheMinutes { get; set; } + + /// <summary>A regex which matches SMAPI-style semantic version.</summary> + /// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks> + public string SemanticVersionRegex { get; set; } + + /**** + ** Chucklefish mod site + ****/ + /// <summary>The repository key for the Chucklefish mod site.</summary> + public string ChucklefishKey { get; set; } + + /// <summary>The user agent for the Chucklefish API client, where {0} is the SMAPI version.</summary> + public string ChucklefishUserAgent { get; set; } + + /// <summary>The base URL for the Chucklefish mod site.</summary> + public string ChucklefishBaseUrl { get; set; } + + /// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary> + public string ChucklefishModPageUrlFormat { get; set; } + + + /**** + ** GitHub + ****/ + /// <summary>The repository key for Nexus Mods.</summary> + public string GitHubKey { get; set; } + + /// <summary>The user agent for the GitHub API client, where {0} is the SMAPI version.</summary> + public string GitHubUserAgent { get; set; } + + /// <summary>The base URL for the GitHub API.</summary> + public string GitHubBaseUrl { get; set; } + + /// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary> + public string GitHubReleaseUrlFormat { get; set; } + + /// <summary>The Accept header value expected by the GitHub API.</summary> + public string GitHubAcceptHeader { get; set; } + + /// <summary>The username with which to authenticate to the GitHub API (if any).</summary> + public string GitHubUsername { get; set; } + + /// <summary>The password with which to authenticate to the GitHub API (if any).</summary> + public string GitHubPassword { get; set; } + + /**** + ** Nexus Mods + ****/ + /// <summary>The repository key for Nexus Mods.</summary> + public string NexusKey { get; set; } + + /// <summary>The user agent for the Nexus Mods API client.</summary> + public string NexusUserAgent { get; set; } + + /// <summary>The base URL for the Nexus Mods API.</summary> + public string NexusBaseUrl { get; set; } + + /// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary> + 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 +{ + /// <summary>Discovers controllers with support for non-public controllers.</summary> + internal class InternalControllerFeatureProvider : ControllerFeatureProvider + { + /********* + ** Public methods + *********/ + /// <summary>Determines if a given type is a controller.</summary> + /// <param name="type">The <see cref="T:System.Reflection.TypeInfo" /> candidate.</param> + /// <returns><code>true</code> if the type is a controller; otherwise <code>false</code>.</returns> + 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..edb00454 --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -0,0 +1,51 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using StardewModdingAPI.Common.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + internal abstract class RepositoryBase : IModRepository + { + /********* + ** Accessors + *********/ + /// <summary>The unique key for this vendor.</summary> + public string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public abstract void Dispose(); + + /// <summary>Get metadata about a mod in the repository.</summary> + /// <param name="id">The mod ID in this repository.</param> + public abstract Task<ModInfoModel> GetModInfoAsync(string id); + + + /********* + ** Protected methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="vendorKey">The unique key for this vendor.</param> + protected RepositoryBase(string vendorKey) + { + this.VendorKey = vendorKey; + } + + /// <summary>Normalise a version string.</summary> + /// <param name="version">The version to normalise.</param> + 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..06ec58ed --- /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.Common.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary> + internal class ChucklefishRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// <summary>The base URL for the Chucklefish mod site.</summary> + private readonly string BaseUrl; + + /// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary> + private readonly string ModPageUrlFormat; + + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="vendorKey">The unique key for this vendor.</param> + /// <param name="userAgent">The user agent for the API client.</param> + /// <param name="baseUrl">The base URL for the Chucklefish mod site.</param> + /// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> + 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); + } + + /// <summary>Get metadata about a mod in the repository.</summary> + /// <param name="id">The mod ID in this repository.</param> + public override async Task<ModInfoModel> 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()); + } + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + 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..9d43adf0 --- /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.Common.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// <summary>An HTTP client for fetching mod metadata from GitHub project releases.</summary> + internal class GitHubRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary> + private readonly string ReleaseUrlFormat; + + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="vendorKey">The unique key for this vendor.</param> + /// <param name="baseUrl">The base URL for the Nexus Mods API.</param> + /// <param name="releaseUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> + /// <param name="userAgent">The user agent for the API client.</param> + /// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param> + /// <param name="username">The username with which to authenticate to the GitHub API.</param> + /// <param name="password">The password with which to authenticate to the GitHub API.</param> + 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); + } + + /// <summary>Get metadata about a mod in the repository.</summary> + /// <param name="id">The mod ID in this repository.</param> + public override async Task<ModInfoModel> 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<GitRelease>(); + 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()); + } + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public override void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// <summary>Metadata about a GitHub release tag.</summary> + private class GitRelease + { + /********* + ** Accessors + *********/ + /// <summary>The display name.</summary> + [JsonProperty("name")] + public string Name { get; set; } + + /// <summary>The semantic version string.</summary> + [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..4496400c --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using StardewModdingAPI.Common.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// <summary>A repository which provides mod metadata.</summary> + internal interface IModRepository : IDisposable + { + /********* + ** Accessors + *********/ + /// <summary>The unique key for this vendor.</summary> + string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Get metadata about a mod in the repository.</summary> + /// <param name="id">The mod ID in this repository.</param> + Task<ModInfoModel> 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..8a4bb0d8 --- /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.Common.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary> + internal class NexusRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary> + private readonly string ModUrlFormat; + + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="vendorKey">The unique key for this vendor.</param> + /// <param name="userAgent">The user agent for the Nexus Mods API client.</param> + /// <param name="baseUrl">The base URL for the Nexus Mods API.</param> + /// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> + public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) + : base(vendorKey) + { + this.ModUrlFormat = modUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// <summary>Get metadata about a mod in the repository.</summary> + /// <param name="id">The mod ID in this repository.</param> + public override async Task<ModInfoModel> 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<NexusResponseModel>(); + + 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()); + } + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public override void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// <summary>A mod metadata response from Nexus Mods.</summary> + private class NexusResponseModel + { + /********* + ** Accessors + *********/ + /// <summary>The mod name.</summary> + public string Name { get; set; } + + /// <summary>The mod's semantic version number.</summary> + public string Version { get; set; } + + /// <summary>The mod's web URL.</summary> + [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 +{ + /// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary> + /// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks> + internal class RewriteSubdomainRule : IRule + { + /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary> + /// <param name="context">The rewrite context.</param> + 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..cffb1092 --- /dev/null +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Routing.Constraints; +using StardewModdingAPI.Common; + +namespace StardewModdingAPI.Web.Framework +{ + /// <summary>Constrains a route value to a valid semantic version.</summary> + internal class VersionConstraint : RegexRouteConstraint + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public VersionConstraint() + : base(SemanticVersionImpl.Regex) { } + } +} 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 +{ + /// <summary>The main app entry point.</summary> + public class Program + { + /********* + ** Public methods + *********/ + /// <summary>The main app entry point.</summary> + /// <param name="args">The command-line arguments.</param> + public static void Main(string[] args) + { + // configure web server + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup<Startup>() + .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..a0760365 --- /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/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Dewdrop": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-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..b5b0ff07 --- /dev/null +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -0,0 +1,26 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>netcoreapp2.0</TargetFramework> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + </PropertyGroup> + + <ItemGroup> + <Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="HtmlAgilityPack" Version="1.6.0" /> + <PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.1.0" /> + </ItemGroup> + <ItemGroup> + <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" /> + </ItemGroup> + <Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" /> + +</Project> 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 +{ + /// <summary>The web app startup configuration.</summary> + internal class Startup + { + /********* + ** Accessors + *********/ + /// <summary>The web app configuration.</summary> + public IConfigurationRoot Configuration { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="env">The hosting environment.</param> + 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(); + } + + /// <summary>The method called by the runtime to add services to the container.</summary> + /// <param name="services">The service injection container.</param> + public void ConfigureServices(IServiceCollection services) + { + services + .Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck")) + .Configure<RouteOptions>(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; + }); + } + + /// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary> + /// <param name="app">The application builder.</param> + /// <param name="env">The hosting environment.</param> + /// <param name="loggerFactory">The logger factory.</param> + 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": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[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/SMAPI.sln index 9c3f18f8..89a8d45c 100644 --- a/src/StardewModdingAPI.sln +++ b/src/SMAPI.sln @@ -1,37 +1,59 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig + ..\.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 + ..\LICENSE.txt = ..\LICENSE.txt EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "StardewModdingAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" +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", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +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", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" +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.Common", "SMAPI.Common\StardewModdingAPI.Common.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\mod-build-config.md = ..\docs\mod-build-config.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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{EA4F1E80-743F-4A1D-9757-AE66904A196A}" EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + SMAPI.Common\StardewModdingAPI.Common.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 + SMAPI.Common\StardewModdingAPI.Common.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4 + SMAPI.Common\StardewModdingAPI.Common.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms @@ -91,8 +113,40 @@ Global {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 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.Build.0 = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86 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/StardewModdingAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 06cc66ef..d16ef684 100644 --- a/src/StardewModdingAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -1,4 +1,5 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InheritdocConsiderUsage/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNameQualifier/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeArgumentsOfMethod/@EntryIndexedValue">HINT</s:String> diff --git a/src/StardewModdingAPI/App.config b/src/SMAPI/App.config index 27cdf0f7..27cdf0f7 100644 --- a/src/StardewModdingAPI/App.config +++ b/src/SMAPI/App.config diff --git a/src/StardewModdingAPI/Constants.cs b/src/SMAPI/Constants.cs index fea9377a..7721fd5e 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -3,13 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.AssemblyRewriters; -using StardewModdingAPI.AssemblyRewriters.Finders; -using StardewModdingAPI.AssemblyRewriters.Rewriters; -using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.ModLoading; using StardewValley; namespace StardewModdingAPI @@ -34,12 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = -#if SMAPI_1_x - new SemanticVersion(1, 15, 4); -#else - new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); -#endif + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); @@ -97,6 +87,14 @@ namespace StardewModdingAPI Platform.Mono; #endif + /// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API.</summary> + internal static readonly IDictionary<string, string> VendorModUrls = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", + ["GitHub"] = "https://github.com/{0}/releases" + }; + /********* ** Internal methods @@ -147,79 +145,6 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } - /// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary> - internal static IEnumerable<IInstructionRewriter> GetRewriters() - { - return new IInstructionRewriter[] - { - /**** - ** Finders throw an exception when incompatible code is found. - ****/ - // changes in Stardew Valley 1.2 (with no rewriters) - new FieldFinder("StardewValley.Item", "set_Name"), - - // APIs removed in SMAPI 1.9 - new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"), - new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"), - new TypeFinder("StardewModdingAPI.Entities.SPlayer"), - new TypeFinder("StardewModdingAPI.Extensions"), - new TypeFinder("StardewModdingAPI.Inheritance.SGame"), - new TypeFinder("StardewModdingAPI.Inheritance.SObject"), - new TypeFinder("StardewModdingAPI.LogWriter"), - new TypeFinder("StardewModdingAPI.Manifest"), - new TypeFinder("StardewModdingAPI.Version"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), - - // APIs removed in SMAPI 2.0 -#if !SMAPI_1_x - new TypeFinder("StardewModdingAPI.Command"), - new TypeFinder("StardewModdingAPI.Config"), - new TypeFinder("StardewModdingAPI.Log"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), -#endif - - /**** - ** Rewriters change CIL as needed to fix incompatible code - ****/ - // crossplatform - new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), 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)) - }; - } - /********* ** Private methods diff --git a/src/StardewModdingAPI/ContentSource.cs b/src/SMAPI/ContentSource.cs index 35c8bc21..35c8bc21 100644 --- a/src/StardewModdingAPI/ContentSource.cs +++ b/src/SMAPI/ContentSource.cs diff --git a/src/StardewModdingAPI/Context.cs b/src/SMAPI/Context.cs index 119e14c8..119e14c8 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/SMAPI/Context.cs diff --git a/src/StardewModdingAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs index 4b207f08..4b207f08 100644 --- a/src/StardewModdingAPI/Events/ChangeType.cs +++ b/src/SMAPI/Events/ChangeType.cs diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs index 4b4e2ad0..4b4e2ad0 100644 --- a/src/StardewModdingAPI/Events/ContentEvents.cs +++ b/src/SMAPI/Events/ContentEvents.cs diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 80d0f547..80d0f547 100644 --- a/src/StardewModdingAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs index 2a2aa163..2a2aa163 100644 --- a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs +++ b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs index 5e6585f0..5e6585f0 100644 --- a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs +++ b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs index 3243b80b..3243b80b 100644 --- a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs +++ b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs index e05a080b..e05a080b 100644 --- a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs +++ b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs index a2087733..a2087733 100644 --- a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs +++ b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs index d2eecbec..d2eecbec 100644 --- a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs +++ b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs diff --git a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs index 25d3ebf3..25d3ebf3 100644 --- a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs +++ b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs index fb8c821e..fb8c821e 100644 --- a/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs +++ b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 31368555..66cb19f2 100644 --- a/src/StardewModdingAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using System; using System.Linq; using Microsoft.Xna.Framework; @@ -123,4 +122,3 @@ namespace StardewModdingAPI.Events } } } -#endif diff --git a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs index 0c742d12..0c742d12 100644 --- a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs +++ b/src/SMAPI/Events/EventArgsIntChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs index 1ee02842..1ee02842 100644 --- a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs +++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs b/src/SMAPI/Events/EventArgsKeyPressed.cs index d9d81e10..d9d81e10 100644 --- a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs +++ b/src/SMAPI/Events/EventArgsKeyPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs index 14e397ce..14e397ce 100644 --- a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs +++ b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs index fe6696d4..fe6696d4 100644 --- a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs +++ b/src/SMAPI/Events/EventArgsLevelUp.cs diff --git a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index 058999e9..058999e9 100644 --- a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs b/src/SMAPI/Events/EventArgsMineLevelChanged.cs index c82fed35..c82fed35 100644 --- a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs +++ b/src/SMAPI/Events/EventArgsMineLevelChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs b/src/SMAPI/Events/EventArgsMouseStateChanged.cs index 57298164..57298164 100644 --- a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs +++ b/src/SMAPI/Events/EventArgsMouseStateChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs b/src/SMAPI/Events/EventArgsValueChanged.cs index 1d25af49..1d25af49 100644 --- a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs +++ b/src/SMAPI/Events/EventArgsValueChanged.cs 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 +{ + /// <summary>Events raised when the game changes state.</summary> + public static class GameEvents + { + /********* + ** Events + *********/ + /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> + internal static event EventHandler InitializeInternal; + + /// <summary>Raised when the game updates its state (≈60 times per second).</summary> + public static event EventHandler UpdateTick; + + /// <summary>Raised every other tick (≈30 times per second).</summary> + public static event EventHandler SecondUpdateTick; + + /// <summary>Raised every fourth tick (≈15 times per second).</summary> + public static event EventHandler FourthUpdateTick; + + /// <summary>Raised every eighth tick (≈8 times per second).</summary> + public static event EventHandler EighthUpdateTick; + + /// <summary>Raised every 15th tick (≈4 times per second).</summary> + public static event EventHandler QuarterSecondTick; + + /// <summary>Raised every 30th tick (≈twice per second).</summary> + public static event EventHandler HalfSecondTick; + + /// <summary>Raised every 60th tick (≈once per second).</summary> + public static event EventHandler OneSecondTick; + + + /********* + ** Internal methods + *********/ + /// <summary>Raise an <see cref="InitializeInternal"/> event.</summary> + /// <param name="monitor">Encapsulates logging and monitoring.</param> + internal static void InvokeInitialize(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); + } + + /// <summary>Raise an <see cref="UpdateTick"/> event.</summary> + /// <param name="monitor">Encapsulates logging and monitoring.</param> + internal static void InvokeUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); + } + + /// <summary>Raise a <see cref="SecondUpdateTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeSecondUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); + } + + /// <summary>Raise a <see cref="FourthUpdateTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeFourthUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); + } + + /// <summary>Raise a <see cref="EighthUpdateTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeEighthUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); + } + + /// <summary>Raise a <see cref="QuarterSecondTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeQuarterSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); + } + + /// <summary>Raise a <see cref="HalfSecondTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeHalfSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); + } + + /// <summary>Raise a <see cref="OneSecondTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + 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/SMAPI/Events/GraphicsEvents.cs index fff51bed..fff51bed 100644 --- a/src/StardewModdingAPI/Events/GraphicsEvents.cs +++ b/src/SMAPI/Events/GraphicsEvents.cs diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index b99b49e0..c31eb698 100644 --- a/src/StardewModdingAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using System; using StardewModdingAPI.Framework; using StardewModdingAPI.Utilities; @@ -42,4 +41,3 @@ namespace StardewModdingAPI.Events } } } -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs index f9ae6df6..f9ae6df6 100644 --- a/src/StardewModdingAPI/Events/ItemStackChange.cs +++ b/src/SMAPI/Events/ItemStackChange.cs diff --git a/src/StardewModdingAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index b834bc1c..b834bc1c 100644 --- a/src/StardewModdingAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs diff --git a/src/StardewModdingAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs index bd8d897e..bd8d897e 100644 --- a/src/StardewModdingAPI/Events/MenuEvents.cs +++ b/src/SMAPI/Events/MenuEvents.cs diff --git a/src/StardewModdingAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs index 9cf7edac..9cf7edac 100644 --- a/src/StardewModdingAPI/Events/MineEvents.cs +++ b/src/SMAPI/Events/MineEvents.cs 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 +{ + /// <summary>Events raised when the player data changes.</summary> + public static class PlayerEvents + { + /********* + ** Events + *********/ + /// <summary>Raised after the player's inventory changes in any way (added or removed item, sorted, etc).</summary> + public static event EventHandler<EventArgsInventoryChanged> InventoryChanged; + + /// <summary> 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.</summary> + public static event EventHandler<EventArgsLevelUp> LeveledUp; + + + /********* + ** Internal methods + *********/ + /// <summary>Raise an <see cref="InventoryChanged"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + /// <param name="inventory">The player's inventory.</param> + /// <param name="changedItems">The inventory changes.</param> + internal static void InvokeInventoryChanged(IMonitor monitor, List<Item> inventory, IEnumerable<ItemStackChange> changedItems) + { + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); + } + + /// <summary>Rase a <see cref="LeveledUp"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + /// <param name="type">The player skill that leveled up.</param> + /// <param name="newLevel">The new skill level.</param> + 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/SMAPI/Events/SaveEvents.cs index 50e6d729..50e6d729 100644 --- a/src/StardewModdingAPI/Events/SaveEvents.cs +++ b/src/SMAPI/Events/SaveEvents.cs 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 +{ + /// <summary>Events raised when the in-game date or time changes.</summary> + public static class TimeEvents + { + /********* + ** Events + *********/ + /// <summary>Raised after the game begins a new day, including when loading a save.</summary> + public static event EventHandler AfterDayStarted; + + /// <summary>Raised after the in-game clock changes.</summary> + public static event EventHandler<EventArgsIntChanged> TimeOfDayChanged; + + /********* + ** Internal methods + *********/ + /// <summary>Raise an <see cref="AfterDayStarted"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeAfterDayStarted(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); + } + + /// <summary>Raise a <see cref="TimeOfDayChanged"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + /// <param name="priorTime">The previous time in military time format (e.g. 6:00pm is 1800).</param> + /// <param name="newTime">The current time in military time format (e.g. 6:10pm is 1810).</param> + 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/SMAPI/Framework/Command.cs index 943e018d..943e018d 100644 --- a/src/StardewModdingAPI/Framework/Command.cs +++ b/src/SMAPI/Framework/Command.cs diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 9af3d27a..79a23d03 100644 --- a/src/StardewModdingAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -52,8 +52,7 @@ namespace StardewModdingAPI.Framework public Command Get(string name) { name = this.GetNormalisedName(name); - Command command; - this.Commands.TryGetValue(name, out command); + this.Commands.TryGetValue(name, out Command command); return command; } @@ -92,8 +91,7 @@ namespace StardewModdingAPI.Framework return false; // get command - Command command; - if (this.Commands.TryGetValue(name, out command)) + if (this.Commands.TryGetValue(name, out Command command)) { command.Callback.Invoke(name, arguments); return true; @@ -101,6 +99,7 @@ namespace StardewModdingAPI.Framework return false; } + /********* ** Private methods *********/ @@ -114,4 +113,4 @@ namespace StardewModdingAPI.Framework : null; } } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs index 1ab9eebd..1ab9eebd 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetData.cs +++ b/src/SMAPI/Framework/Content/AssetData.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs index e9b29b12..e9b29b12 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs +++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 45c5588b..45c5588b 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs index f30003e4..f30003e4 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index d580dc06..d580dc06 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs diff --git a/src/StardewModdingAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs index d46f23a3..d46f23a3 100644 --- a/src/StardewModdingAPI/Framework/ContentManagerShim.cs +++ b/src/SMAPI/Framework/ContentManagerShim.cs diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index 0fb2309b..db02b3d1 100644 --- a/src/StardewModdingAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using Microsoft.Xna.Framework; namespace StardewModdingAPI.Framework @@ -34,4 +33,3 @@ namespace StardewModdingAPI.Framework } } } -#endif diff --git a/src/StardewModdingAPI/Framework/DeprecationLevel.cs b/src/SMAPI/Framework/DeprecationLevel.cs index c0044053..c0044053 100644 --- a/src/StardewModdingAPI/Framework/DeprecationLevel.cs +++ b/src/SMAPI/Framework/DeprecationLevel.cs diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index 43e82d74..b07c6c7d 100644 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Reflection; namespace StardewModdingAPI.Framework { @@ -52,10 +51,6 @@ namespace StardewModdingAPI.Framework if (!this.MarkWarned(source ?? "<unknown>", nounPhrase, version)) return; - // show SMAPI 2.0 meta-warning - if(this.MarkWarned("SMAPI", "SMAPI 2.0 meta-warning", "2.0")) - this.Monitor.Log("Some mods may stop working in SMAPI 2.0 (but they'll work fine for now). Try updating mods with 'deprecated code' warnings; if that doesn't remove the warnings, let the mod authors know about this message or see http://stardewvalleywiki.com/Modding:SMAPI_2.0 for details.", LogLevel.Warn); - // build message string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; if (source == null) @@ -106,16 +101,5 @@ namespace StardewModdingAPI.Framework this.LoggedDeprecations.Add(key); return true; } - - /// <summary>Get whether a type implements the given virtual method.</summary> - /// <param name="subtype">The type to check.</param> - /// <param name="baseType">The base type which declares the virtual method.</param> - /// <param name="name">The method name.</param> - /// <param name="argumentTypes">The expected argument types.</param> - internal bool IsVirtualMethodImplemented(Type subtype, Type baseType, string name, Type[] argumentTypes) - { - MethodInfo method = subtype.GetMethod(nameof(Mod.Entry), argumentTypes); - return method.DeclaringType != baseType; - } } } diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs index ec9279f1..ec9279f1 100644 --- a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs +++ b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs index 85d85e3d..85d85e3d 100644 --- a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs +++ b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/SMAPI/Framework/Exceptions/SParseException.cs index f7133ee7..f7133ee7 100644 --- a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs +++ b/src/SMAPI/Framework/Exceptions/SParseException.cs diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 48159f61..48159f61 100644 --- a/src/StardewModdingAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs diff --git a/src/StardewModdingAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 56ac25f1..c21734a7 100644 --- a/src/StardewModdingAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; namespace StardewModdingAPI.Framework @@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework /// <summary>The mod manifest.</summary> IManifest Manifest { get; } - /// <summary>Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary> - ModCompatibility Compatibility { get; } + /// <summary>>Metadata about the mod from SMAPI's internal data (if any).</summary> + ModDataRecord DataRecord { get; } /// <summary>The metadata resolution status.</summary> ModMetadataStatus Status { get; } diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 2842bc83..3709e05d 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -71,6 +71,20 @@ namespace StardewModdingAPI.Framework } } + /// <summary>Log a message for the player or developer the first time it occurs.</summary> + /// <param name="monitor">The monitor through which to log the message.</param> + /// <param name="hash">The hash of logged messages.</param> + /// <param name="message">The message to log.</param> + /// <param name="level">The log severity level.</param> + public static void LogOnce(this IMonitor monitor, HashSet<string> hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + /**** ** Exceptions ****/ diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs index b8f2c34e..b8f2c34e 100644 --- a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs index 9ca61b59..9ca61b59 100644 --- a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs +++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs index 8cfe0527..8cfe0527 100644 --- a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs +++ b/src/SMAPI/Framework/Logging/LogFileManager.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs index 16032da1..16032da1 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs index bdedb07c..bdedb07c 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4440ae40..4f5bd2f0 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -239,6 +239,10 @@ namespace StardewModdingAPI.Framework.ModHelpers { string imageSource = tilesheet.ImageSource; + // validate + if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains("..")) + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); + // get seasonal name (if applicable) string seasonalImageSource = null; if (Game1.currentSeason != null) diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 665b9cf4..665b9cf4 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9e824694..9e824694 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 14a339da..8d435416 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -180,7 +180,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="type">The type being accessed.</param> private void AssertAccessAllowed(Type type) { -#if !SMAPI_1_x // validate type namespace if (type.Namespace != null) { @@ -188,7 +187,6 @@ namespace StardewModdingAPI.Framework.ModHelpers 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."); } -#endif } /// <summary>Assert that mods can use the reflection helper to access the given type.</summary> diff --git a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs index bbe3a81a..bbe3a81a 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index 4378798c..4378798c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs index 11be19fc..11be19fc 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 9c642bef..1e3c4a05 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading { @@ -25,6 +25,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; + /// <summary>Whether to enable developer mode logging.</summary> + private readonly bool IsDeveloperMode; + /********* ** Public methods @@ -32,9 +35,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Construct an instance.</summary> /// <param name="targetPlatform">The current game platform.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public AssemblyLoader(Platform targetPlatform, IMonitor monitor) + /// <param name="isDeveloperMode">Whether to enable developer mode logging.</param> + 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 @@ -54,11 +59,12 @@ namespace StardewModdingAPI.Framework.ModLoading } /// <summary>Preprocess and load an assembly.</summary> + /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assemblyPath">The assembly file path.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> - public Assembly Load(string assemblyPath, bool assumeCompatible) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -82,12 +88,13 @@ namespace StardewModdingAPI.Framework.ModLoading // rewrite & load assemblies in leaf-to-root order bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; + HashSet<string> loggedMessages = new HashSet<string>(); foreach (AssemblyParseResult assembly in assemblies) { if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); if (changed) { if (!oneAssembly) @@ -174,15 +181,16 @@ namespace StardewModdingAPI.Framework.ModLoading ** Assembly rewriting ****/ /// <summary>Rewrite the types referenced by an assembly.</summary> + /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assembly">The assembly to rewrite.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> + /// <param name="loggedMessages">The messages that have already been logged for this mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <returns>Returns whether the assembly was modified.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet<string> loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; - HashSet<string> loggedMessages = new HashSet<string>(); string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) @@ -192,7 +200,7 @@ namespace StardewModdingAPI.Framework.ModLoading // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; @@ -212,48 +220,28 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; - IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); + IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { // check method definition - foreach (IInstructionRewriter rewriter in rewriters) + foreach (IInstructionHandler handler in handlers) { - try - { - if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) - { - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } + 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 (IInstructionRewriter rewriter in rewriters) + foreach (IInstructionHandler handler in handlers) { - try - { - if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) - { - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } + 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; } } } @@ -261,6 +249,57 @@ namespace StardewModdingAPI.Framework.ModLoading return platformChanged || anyRewritten; } + /// <summary>Process the result from an instruction handler.</summary> + /// <param name="mod">The mod being analysed.</param> + /// <param name="handler">The instruction handler.</param> + /// <param name="result">The result returned by the handler.</param> + /// <param name="loggedMessages">The messages already logged for the current mod.</param> + /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> + /// <param name="logPrefix">A string to prefix to log messages.</param> + /// <param name="filename">The assembly filename for log messages.</param> + private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet<string> 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}'."); + } + } + /// <summary>Get the correct reference to use for compatibility with the current platform.</summary> /// <param name="type">The type reference to rewrite.</param> private void ChangeTypeScope(TypeReference type) @@ -270,8 +309,7 @@ namespace StardewModdingAPI.Framework.ModLoading return; // get assembly - Assembly assembly; - if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) + if (!this.TypeAssemblies.TryGetValue(type.FullName, out Assembly assembly)) return; // replace scope @@ -291,19 +329,5 @@ namespace StardewModdingAPI.Framework.ModLoading select method ); } - - /// <summary>Log a message for the player or developer the first time it occurs.</summary> - /// <param name="monitor">The monitor through which to log the message.</param> - /// <param name="hash">The hash of logged messages.</param> - /// <param name="message">The message to log.</param> - /// <param name="level">The log severity level.</param> - private void LogOnce(IMonitor monitor, HashSet<string> hash, string message, LogLevel level = LogLevel.Trace) - { - if (!hash.Contains(message)) - { - monitor.Log(message, level); - hash.Add(message); - } - } } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs index b56a776c..b56a776c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index c0051469..e4beb7a9 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,10 +1,10 @@ -using Mono.Cecil; +using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// <summary>Finds incompatible CIL instructions that reference a given event and throws an <see cref="IncompatibleInstructionException"/>.</summary> - public class EventFinder : IInstructionRewriter + /// <summary>Finds incompatible CIL instructions that reference a given event.</summary> + internal class EventFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>The event name for which to find references.</summary> private readonly string EventName; + /// <summary>The result to return for matching instructions.</summary> + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="eventName">The event name for which to find references.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public EventFinder(string fullTypeName, string eventName, string nounPhrase = null) + /// <param name="result">The result to return for matching instructions.</param> + public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.EventName = eventName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{eventName} event"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{eventName} event"; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index b44883e9..00805815 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,10 +1,10 @@ using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// <summary>Finds incompatible CIL instructions that reference a given field and throws an <see cref="IncompatibleInstructionException"/>.</summary> - public class FieldFinder : IInstructionRewriter + /// <summary>Finds incompatible CIL instructions that reference a given field.</summary> + internal class FieldFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>The field name for which to find references.</summary> private readonly string FieldName; + /// <summary>The result to return for matching instructions.</summary> + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="fieldName">The field name for which to find references.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public FieldFinder(string fullTypeName, string fieldName, string nounPhrase = null) + /// <param name="result">The result to return for matching instructions.</param> + public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.FieldName = fieldName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{fieldName} field"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{fieldName} field"; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs index 19dda58a..5358f181 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,10 +1,10 @@ -using Mono.Cecil; +using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// <summary>Finds incompatible CIL instructions that reference a given method and throws an <see cref="IncompatibleInstructionException"/>.</summary> - public class MethodFinder : IInstructionRewriter + /// <summary>Finds incompatible CIL instructions that reference a given method.</summary> + internal class MethodFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>The method name for which to find references.</summary> private readonly string MethodName; + /// <summary>The result to return for matching instructions.</summary> + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="methodName">The method name for which to find references.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public MethodFinder(string fullTypeName, string methodName, string nounPhrase = null) + /// <param name="result">The result to return for matching instructions.</param> + public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.MethodName = methodName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{methodName} method"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{methodName} method"; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs index 441f15f2..e54c86cf 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,10 +1,10 @@ using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// <summary>Finds incompatible CIL instructions that reference a given property and throws an <see cref="IncompatibleInstructionException"/>.</summary> - public class PropertyFinder : IInstructionRewriter + /// <summary>Finds incompatible CIL instructions that reference a given property.</summary> + internal class PropertyFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>The property name for which to find references.</summary> private readonly string PropertyName; + /// <summary>The result to return for matching instructions.</summary> + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name for which to find references.</param> /// <param name="propertyName">The property name for which to find references.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public PropertyFinder(string fullTypeName, string propertyName, string nounPhrase = null) + /// <param name="result">The result to return for matching instructions.</param> + public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{propertyName} property"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{propertyName} property"; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index 0560e38e..45349def 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -2,10 +2,10 @@ using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// <summary>Finds incompatible CIL instructions that reference a given type and throws an <see cref="IncompatibleInstructionException"/>.</summary> - public class TypeFinder : IInstructionRewriter + /// <summary>Finds incompatible CIL instructions that reference a given type.</summary> + internal class TypeFinder : IInstructionHandler { /********* ** Accessors @@ -13,6 +13,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// <summary>The full type name for which to find references.</summary> private readonly string FullTypeName; + /// <summary>The result to return for matching instructions.</summary> + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -26,42 +29,37 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders *********/ /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public TypeFinder(string fullTypeName, string nounPhrase = null) + /// <param name="result">The result to return for matching instructions.</param> + public TypeFinder(string fullTypeName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName} type"; + this.Result = result; + this.NounPhrase = $"{fullTypeName} type"; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(method)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(method) + ? this.Result + : InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } 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 +{ + /// <summary>Performs predefined logic for detected CIL instructions.</summary> + internal interface IInstructionHandler + { + /********* + ** Accessors + *********/ + /// <summary>A brief noun phrase indicating what the handler matches.</summary> + string NounPhrase { get; } + + + /********* + ** Methods + *********/ + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + } +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs index f7e6bd8f..17ec24b1 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs +++ b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs @@ -1,9 +1,9 @@ -using System; +using System; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// <summary>An exception raised when an incompatible instruction is found while loading a mod assembly.</summary> - public class IncompatibleInstructionException : Exception + internal class IncompatibleInstructionException : Exception { /********* ** Accessors 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 +{ + /// <summary>Indicates how an instruction was handled.</summary> + internal enum InstructionHandleResult + { + /// <summary>No special handling is needed.</summary> + None, + + /// <summary>The instruction was successfully rewritten for compatibility.</summary> + Rewritten, + + /// <summary>The instruction is not compatible and can't be rewritten for compatibility.</summary> + NotCompatible, + + /// <summary>The instruction is compatible, but patches the game in a way that may impact stability.</summary> + DetectedGamePatch, + + /// <summary>The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod.</summary> + DetectedSaveSerialiser, + + /// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary> + DetectedDynamic + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs index ab11272a..075e237a 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs +++ b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace StardewModdingAPI.Framework.ModLoading { /// <summary>An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright.</summary> - public class InvalidModStateException : Exception + internal class InvalidModStateException : Exception { /// <summary>Construct an instance.</summary> /// <param name="message">The error message.</param> diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs index 0774b487..0774b487 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs +++ b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index ab590e10..5055da75 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.ModLoading { @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The mod manifest.</summary> public IManifest Manifest { get; } - /// <summary>Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary> - public ModCompatibility Compatibility { get; } + /// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary> + public ModDataRecord DataRecord { get; } /// <summary>The metadata resolution status.</summary> public ModMetadataStatus Status { get; private set; } @@ -37,13 +37,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="displayName">The mod's display name.</param> /// <param name="directoryPath">The mod's full directory path.</param> /// <param name="manifest">The mod manifest.</param> - /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + /// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param> + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; this.Manifest = manifest; - this.Compatibility = compatibility; + this.DataRecord = dataRecord; } /// <summary>Set the mod status.</summary> diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs index 1b2b0b55..ab65f7b4 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.ModLoading +namespace StardewModdingAPI.Framework.ModLoading { /// <summary>Indicates the status of a mod's metadata resolution.</summary> internal enum ModMetadataStatus @@ -9,4 +9,4 @@ /// <summary>The mod cannot be loaded.</summary> Failed } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 6b19db5c..d0ef1b08 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -17,13 +17,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Get manifest metadata for each folder in the given root path.</summary> /// <param name="rootPath">The root path to search for mods.</param> /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> - /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param> - /// <param name="disabledMods">Metadata about mods that SMAPI should consider obsolete and not load.</param> + /// <param name="dataRecords">Metadata about mods from SMAPI's internal data.</param> /// <returns>Returns the manifests by relative folder.</returns> - public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModCompatibility> compatibilityRecords, IEnumerable<DisabledMod> disabledMods) + public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModDataRecord> dataRecords) { - compatibilityRecords = compatibilityRecords.ToArray(); - disabledMods = disabledMods.ToArray(); + dataRecords = dataRecords.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { @@ -55,29 +53,18 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // validate metadata - ModCompatibility compatibility = null; + // get internal data record (if any) + ModDataRecord dataRecord = null; if (manifest != null) { - // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - - // check if mod should be disabled - DisabledMod disabledMod = disabledMods.FirstOrDefault(mod => mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)); - if (disabledMod != null) - error = $"it's obsolete: {disabledMod.ReasonPhrase}"; - - // get compatibility record - compatibility = ( - from mod in compatibilityRecords - where - mod.ID.Any(p => p.Matches(key, manifest)) - && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) - && !manifest.Version.IsNewerThan(mod.UpperVersion) - select mod - ).FirstOrDefault(); + 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 @@ -86,14 +73,15 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error); + yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); } } /// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> - public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion) + /// <param name="vendorModUrls">Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID).</param> + public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, IDictionary<string, string> vendorModUrls) { mods = mods.ToArray(); @@ -105,33 +93,46 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // validate compatibility + ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); + switch (compatibility?.Status) { - ModCompatibility compatibility = mod.Compatibility; - if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) - { -#if SMAPI_1_x - bool hasOfficialUrl = mod.Compatibility.UpdateUrls.Length > 0; - bool hasUnofficialUrl = mod.Compatibility.UpdateUrls.Length > 1; - - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:"; - if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrls[0]}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrls[0]}"; - if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UpdateUrls[1]}"; -#else - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; - string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) - error += "newer version"; - else - error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; - error += " at " + string.Join(" or ", compatibility.UpdateUrls); -#endif - - mod.SetStatus(ModMetadataStatus.Failed, error); + 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<string> updateUrls = new List<string>(); + 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 @@ -150,7 +151,6 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate required fields -#if !SMAPI_1_x { List<string> missingFields = new List<string>(3); @@ -164,11 +164,9 @@ namespace StardewModdingAPI.Framework.ModLoading if (missingFields.Any()) mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); } -#endif } // validate IDs are unique -#if !SMAPI_1_x { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) @@ -183,7 +181,6 @@ namespace StardewModdingAPI.Framework.ModLoading } } } -#endif } /// <summary>Sort the given mods by the order they should be loaded.</summary> @@ -264,12 +261,7 @@ namespace StardewModdingAPI.Framework.ModLoading ID = entry.UniqueID, MinVersion = entry.MinimumVersion, Mod = dependencyMod, - IsRequired = -#if SMAPI_1_x - true -#else - entry.IsRequired -#endif + IsRequired = entry.IsRequired } ) .ToArray(); diff --git a/src/StardewModdingAPI.AssemblyRewriters/Platform.cs b/src/SMAPI/Framework/ModLoading/Platform.cs index 8888a9a8..45e881c4 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Platform.cs +++ b/src/SMAPI/Framework/ModLoading/Platform.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// <summary>The game's platform version.</summary> - public enum Platform + internal enum Platform { /// <summary>The Linux/Mac version of the game.</summary> Mono, @@ -9,4 +9,4 @@ namespace StardewModdingAPI.AssemblyRewriters /// <summary>The Windows version of the game.</summary> Windows } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index fce2b187..463f45e8 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -3,10 +3,10 @@ using System.Linq; using System.Reflection; using Mono.Cecil; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// <summary>Metadata for mapping assemblies to the current <see cref="Platform"/>.</summary> - public class PlatformAssemblyMap + internal class PlatformAssemblyMap { /********* ** Accessors diff --git a/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index cfb330dd..56a60a72 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -4,7 +4,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// <summary>Provides helper methods for field rewriters.</summary> internal static class RewriteHelper diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 73844073..63358b39 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -2,12 +2,12 @@ using System; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters.Finders; +using StardewModdingAPI.Framework.ModLoading.Finders; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites references to one field with another.</summary> - public class FieldReplaceRewriter : FieldFinder + internal class FieldReplaceRewriter : FieldFinder { /********* ** Properties @@ -23,31 +23,28 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// <param name="type">The type whose field to which references should be rewritten.</param> /// <param name="fromFieldName">The field name to rewrite.</param> /// <param name="toFieldName">The new field name to reference.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName, string nounPhrase = null) - : base(type.FullName, fromFieldName, nounPhrase) + 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."); } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) - return false; + return InstructionHandleResult.None; FieldReference newRef = module.Import(this.ToField); cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); - return true; + return InstructionHandleResult.Rewritten; } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 3f57042d..a20b8bee 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -1,12 +1,12 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters.Finders; +using StardewModdingAPI.Framework.ModLoading.Finders; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites field references into property references.</summary> - public class FieldToPropertyRewriter : FieldFinder + internal class FieldToPropertyRewriter : FieldFinder { /********* ** Properties @@ -24,31 +24,28 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// <summary>Construct an instance.</summary> /// <param name="type">The type whose field to which references should be rewritten.</param> /// <param name="fieldName">The field name to rewrite.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public FieldToPropertyRewriter(Type type, string fieldName, string nounPhrase = null) - : base(type.FullName, fieldName, nounPhrase) + public FieldToPropertyRewriter(Type type, string fieldName) + : base(type.FullName, fieldName, InstructionHandleResult.None) { this.Type = type; this.FieldName = fieldName; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) - return false; + 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 true; + return InstructionHandleResult.Rewritten; } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 035ef211..974fcf4c 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -2,10 +2,10 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites method references from one parent type to another if the signatures match.</summary> - public class MethodParentRewriter : IInstructionRewriter + internal class MethodParentRewriter : IInstructionHandler { /********* ** Properties @@ -34,43 +34,38 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// <param name="fromType">The type whose methods to remap.</param> /// <param name="toType">The type with methods to map to.</param> /// <param name="onlyIfPlatformChanged">Whether to only rewrite references if loading the assembly on a different platform than it was compiled on.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) + public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) { this.FromType = fromType; this.ToType = toType; - this.NounPhrase = nounPhrase ?? $"{fromType.Name} methods"; + this.NounPhrase = $"{fromType.Name} methods"; this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction, platformChanged)) - return false; + return InstructionHandleResult.None; MethodReference methodRef = (MethodReference)instruction.Operand; methodRef.DeclaringType = module.Import(this.ToType); - return true; + return InstructionHandleResult.Rewritten; } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index da6d9bc9..74f2fcdd 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -1,12 +1,12 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters.Finders; +using StardewModdingAPI.Framework.ModLoading.Finders; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites all references to a type.</summary> - public class TypeReferenceRewriter : TypeFinder + internal class TypeReferenceRewriter : TypeFinder { /********* ** Properties @@ -24,22 +24,19 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// <summary>Construct an instance.</summary> /// <param name="fromTypeFullName">The full type name to which to find references.</param> /// <param name="toType">The new type to reference.</param> - /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> - public TypeReferenceRewriter(string fromTypeFullName, Type toType, string nounPhrase = null) - : base(fromTypeFullName, nounPhrase) + public TypeReferenceRewriter(string fromTypeFullName, Type toType) + : base(fromTypeFullName, InstructionHandleResult.None) { this.FromTypeName = fromTypeFullName; this.ToType = toType; } - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public override bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { bool rewritten = false; @@ -83,21 +80,21 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters } } - return rewritten; + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; } - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) - return false; + return InstructionHandleResult.None; // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -125,14 +122,14 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); } - return true; + return InstructionHandleResult.Rewritten; } /********* ** Private methods *********/ /// <summary>Get the adjusted type reference if it matches, else the same value.</summary> - /// <param name="module">The module being rewritten.</param> + /// <param name="module">The assembly module containing the instruction.</param> /// <param name="type">The type to replace if it matches.</param> private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference 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 +{ + /// <summary>Rewrites virtual calls to the <see cref="Mod.Entry"/> method.</summary> + internal class VirtualEntryCallRemover : IInstructionHandler + { + /********* + ** Properties + *********/ + /// <summary>The type containing the method.</summary> + private readonly Type ToType; + + /// <summary>The name of the method.</summary> + private readonly string MethodName; + + + /********* + ** Accessors + *********/ + /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public VirtualEntryCallRemover() + { + this.ToType = typeof(Mod); + this.MethodName = nameof(Mod.Entry); + this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; + } + + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + 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 + *********/ + /// <summary>Get whether a CIL instruction matches.</summary> + /// <param name="instruction">The IL instruction.</param> + 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/SMAPI/Framework/ModRegistry.cs index 8f30d813..9dde7a20 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -42,11 +42,7 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => -#if SMAPI_1_x - p.UniqueID != null && -#endif - p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); } /// <summary>Get whether a mod has been loaded.</summary> diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index 29c3517e..b85787e5 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; @@ -28,22 +27,19 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public ISemanticVersion MinimumApiVersion { get; set; } - /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> + /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method.</summary> public string EntryDll { get; set; } /// <summary>The other mods that must be loaded before this mod.</summary> [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } + /// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary> + public string[] UpdateKeys { get; set; } + /// <summary>The unique mod ID.</summary> public string UniqueID { get; set; } -#if SMAPI_1_x - /// <summary>Whether the mod uses per-save config files.</summary> - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public bool PerSaveConfigs { get; set; } -#endif - /// <summary>Any manifest fields which didn't match a valid field.</summary> [JsonExtensionData] public IDictionary<string, object> ExtraFields { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs index 67f906e3..5646b335 100644 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ b/src/SMAPI/Framework/Models/ManifestDependency.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.Models +namespace StardewModdingAPI.Framework.Models { /// <summary>A mod dependency listed in a mod manifest.</summary> internal class ManifestDependency : IManifestDependency @@ -12,10 +12,8 @@ /// <summary>The minimum required version (if any).</summary> public ISemanticVersion MinimumVersion { get; set; } -#if !SMAPI_1_x /// <summary>Whether the dependency must be installed to use the mod.</summary> public bool IsRequired { get; set; } -#endif /********* ** Public methods @@ -24,19 +22,13 @@ /// <param name="uniqueID">The unique mod ID to require.</param> /// <param name="minimumVersion">The minimum required version (if any).</param> /// <param name="required">Whether the dependency must be installed to use the mod.</param> - public ManifestDependency(string uniqueID, string minimumVersion -#if !SMAPI_1_x - , bool required = true -#endif - ) + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) { this.UniqueID = uniqueID; this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) ? new SemanticVersion(minimumVersion) : null; -#if !SMAPI_1_x this.IsRequired = required; -#endif } } } 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 +{ + /// <summary>Specifies the compatibility of a given mod version range.</summary> + internal class ModCompatibility + { + /********* + ** Accessors + *********/ + /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> + public ISemanticVersion LowerVersion { get; } + + /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> + public ISemanticVersion UpperVersion { get; } + + /// <summary>The mod compatibility.</summary> + public ModStatus Status { get; } + + /// <summary>The reason phrase to show in log output, or <c>null</c> to use the default value.</summary> + /// <example>For example, "this version is incompatible with the latest version of the game".</example> + public string ReasonPhrase { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="versionRange">A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range.</param> + /// <param name="status">The mod compatibility.</param> + /// <param name="reasonPhrase">The reason phrase to show in log output, or <c>null</c> to use the default value.</param> + 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; + } + + /// <summary>Get whether a given version is contained within this compatibility range.</summary> + /// <param name="version">The version to check.</param> + 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 +{ + /// <summary>Uniquely identifies a mod in SMAPI's internal data.</summary> + /// <remarks> + /// 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 <c>|</c> + /// 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. + /// </remarks> + internal class ModDataID + { + /********* + ** Properties + *********/ + /// <summary>The unique sets of field values which identify this mod.</summary> + private readonly FieldSnapshot[] Snapshots; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ModDataID() { } + + /// <summary>Construct an instance.</summary> + /// <param name="data">The mod identifier string (see remarks on <see cref="ModDataID"/>).</param> + public ModDataID(string data) + { + this.Snapshots = + ( + from string part in data.Split('|') + let str = part.Trim() + select str.StartsWith("{") + ? JsonConvert.DeserializeObject<FieldSnapshot>(str) + : new FieldSnapshot { ID = str } + ) + .ToArray(); + } + + /// <summary>Get whether this ID matches a given mod manifest.</summary> + /// <param name="id">The mod's unique ID, or a substitute ID if it isn't set in the manifest.</param> + /// <param name="manifest">The manifest to check.</param> + 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 + *********/ + /// <summary>A unique set of fields which identifies the mod.</summary> + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod ID.</summary> + public string ID { get; set; } + + /// <summary>The mod name, or <c>null</c> to ignore the mod name.</summary> + public string Name { get; set; } + + /// <summary>The author name, or <c>null</c> to ignore the author.</summary> + 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 +{ + /// <summary>Metadata about a mod from SMAPI's internal data.</summary> + internal class ModDataRecord + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod identifier.</summary> + [JsonConverter(typeof(SFieldConverter))] + public ModDataID ID { get; set; } + + /// <summary>A value to inject into <see cref="IManifest.UpdateKeys"/> field if it's not already set.</summary> + public string[] UpdateKeys { get; set; } + + /// <summary>The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible.</summary> + public string AlternativeUrl { get; set; } + + /// <summary>The compatibility of given mod versions (if any).</summary> + [JsonConverter(typeof(SFieldConverter))] + public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; + + /// <summary>Map local versions to a semantic version for update checks.</summary> + public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>(); + + /// <summary>Map remote versions to a semantic version for update checks.</summary> + public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>(); + + + /********* + ** Public methods + *********/ + /// <summary>Get the compatibility record for a given version, if any.</summary> + /// <param name="version">The mod version to check.</param> + public ModCompatibility GetCompatibility(ISemanticVersion version) + { + return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); + } + + /// <summary>Get a semantic local version for update checks.</summary> + /// <param name="version">The local version to normalise.</param> + public string GetLocalVersionForUpdateChecks(string version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + /// <summary>Get a semantic remote version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetRemoteVersionForUpdateChecks(string version) + { + 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 +{ + /// <summary>Indicates how SMAPI should treat a mod.</summary> + internal enum ModStatus + { + /// <summary>Don't override the status.</summary> + None, + + /// <summary>The mod is obsolete and shouldn't be used, regardless of version.</summary> + Obsolete, + + /// <summary>Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code.</summary> + AssumeBroken, + + /// <summary>Assume the mod is compatible, even if SMAPI detects incompatible code.</summary> + 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 +{ + /// <summary>The SMAPI configuration settings.</summary> + internal class SConfig + { + /******** + ** Accessors + ********/ + /// <summary>Whether to enable development features.</summary> + public bool DeveloperMode { get; set; } + + /// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary> + public bool CheckForUpdates { get; set; } + + /// <summary>SMAPI's GitHub project name, used to perform update checks.</summary> + public string GitHubProjectName { get; set; } + + /// <summary>The base URL for SMAPI's web API, used to perform update checks.</summary> + public string WebApiBaseUrl { get; set; } + + /// <summary>Whether SMAPI should log more information about the game context.</summary> + public bool VerboseLogging { get; set; } + + /// <summary>Extra metadata about mods.</summary> + public ModDataRecord[] ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index c2c3a689..bf338386 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -37,10 +37,8 @@ namespace StardewModdingAPI.Framework /// <summary>Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks.</summary> public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; -#if !SMAPI_1_x /// <summary>Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger.</summary> internal bool ShowFullStampInConsole { get; set; } -#endif /// <summary>Whether to show trace messages in the console.</summary> internal bool ShowTraceInConsole { get; set; } @@ -89,7 +87,6 @@ namespace StardewModdingAPI.Framework this.ExitTokenSource.Cancel(); } -#if !SMAPI_1_x /// <summary>Write a newline to the console and log file.</summary> internal void Newline() { @@ -98,20 +95,6 @@ namespace StardewModdingAPI.Framework if (this.WriteToFile) this.LogFile.WriteLine(""); } -#endif - -#if SMAPI_1_x - /// <summary>Log a message for the player or developer, using the specified console color.</summary> - /// <param name="source">The name of the mod logging the message.</param> - /// <param name="message">The message to log.</param> - /// <param name="color">The console color.</param> - /// <param name="level">The log level.</param> - [Obsolete("This method is provided for backwards compatibility and otherwise should not be used. Use " + nameof(Monitor) + "." + nameof(Monitor.Log) + " instead.")] - internal void LegacyLog(string source, string message, ConsoleColor color, LogLevel level = LogLevel.Debug) - { - this.LogImpl(source, message, level, color); - } -#endif /********* @@ -136,11 +119,7 @@ namespace StardewModdingAPI.Framework string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; -#if !SMAPI_1_x string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; -#else - string consoleMessage = fullMessage; -#endif // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) @@ -168,11 +147,9 @@ namespace StardewModdingAPI.Framework /// <summary>Get the color scheme to use for the current console.</summary> private static IDictionary<LogLevel, ConsoleColor> GetConsoleColorScheme() { -#if !SMAPI_1_x // scheme for dark console background if (Monitor.IsDark(Console.BackgroundColor)) { -#endif return new Dictionary<LogLevel, ConsoleColor> { [LogLevel.Trace] = ConsoleColor.DarkGray, @@ -182,7 +159,6 @@ namespace StardewModdingAPI.Framework [LogLevel.Error] = ConsoleColor.Red, [LogLevel.Alert] = ConsoleColor.Magenta }; -#if !SMAPI_1_x } // scheme for light console background @@ -195,7 +171,6 @@ namespace StardewModdingAPI.Framework [LogLevel.Error] = ConsoleColor.Red, [LogLevel.Alert] = ConsoleColor.DarkMagenta }; -#endif } /// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary> diff --git a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs index 30faca37..30faca37 100644 --- a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs +++ b/src/SMAPI/Framework/Reflection/CacheEntry.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs b/src/SMAPI/Framework/Reflection/PrivateField.cs index 0bf45969..0bf45969 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs +++ b/src/SMAPI/Framework/Reflection/PrivateField.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs b/src/SMAPI/Framework/Reflection/PrivateMethod.cs index ba2374f4..ba2374f4 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs +++ b/src/SMAPI/Framework/Reflection/PrivateMethod.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index 08204b7e..08204b7e 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 5c2d90fa..5c2d90fa 100644 --- a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/SMAPI/Framework/RequestExitDelegate.cs index 12d0ea0c..12d0ea0c 100644 --- a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs +++ b/src/SMAPI/Framework/RequestExitDelegate.cs diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 9553e79f..db202567 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -6,8 +6,8 @@ using System.Linq; using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; @@ -21,9 +21,6 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ - /// <summary>The possible directory separator characters in an asset key.</summary> - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// <summary>The preferred directory separator chaeacter in an asset key.</summary> private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); @@ -51,6 +48,9 @@ namespace StardewModdingAPI.Framework /// <summary>A lookup of the content managers which loaded each asset.</summary> private readonly IDictionary<string, HashSet<ContentManager>> AssetLoaders = new Dictionary<string, HashSet<ContentManager>>(); + /// <summary>An object locked to prevent concurrent changes to the underlying assets.</summary> + private readonly object Lock = new object(); + /********* ** Accessors @@ -61,8 +61,11 @@ namespace StardewModdingAPI.Framework /// <summary>Interceptors which edit matching assets after they're loaded.</summary> internal IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>(); + /// <summary>The possible directory separator characters in an asset key.</summary> + internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary> - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); /********* @@ -128,8 +131,11 @@ namespace StardewModdingAPI.Framework /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> public bool IsLoaded(string assetName) { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); + lock (this.Lock) + { + assetName = this.NormaliseAssetName(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } } /// <summary>Load an asset that has been processed by the content pipeline.</summary> @@ -146,38 +152,41 @@ namespace StardewModdingAPI.Framework /// <param name="instance">The content manager instance for which to load the asset.</param> public T LoadFor<T>(string assetName, ContentManager instance) { - assetName = this.NormaliseAssetName(assetName); - - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) + lock (this.Lock) { - this.TrackAssetLoader(assetName, instance); - return base.Load<T>(assetName); - } + assetName = this.NormaliseAssetName(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<T>(assetName); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors<T>(info, asset); - return (T)asset.Data; - }); - } + this.TrackAssetLoader(assetName, instance); + return base.Load<T>(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<T>(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors<T>(info, asset); + return (T)asset.Data; + }); + } - // update cache & return data - this.Cache[assetName] = data; - this.TrackAssetLoader(assetName, instance); - return data; + // update cache & return data + this.Cache[assetName] = data; + this.TrackAssetLoader(assetName, instance); + return data; + } } /// <summary>Inject an asset into the cache.</summary> @@ -186,9 +195,12 @@ namespace StardewModdingAPI.Framework /// <param name="value">The asset value.</param> public void Inject<T>(string assetName, T value) { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - this.TrackAssetLoader(assetName, this); + lock (this.Lock) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + this.TrackAssetLoader(assetName, this); + } } /// <summary>Get the current content locale.</summary> @@ -200,16 +212,19 @@ namespace StardewModdingAPI.Framework /// <summary>Get the cached asset keys.</summary> public IEnumerable<string> GetAssetKeys() { - IEnumerable<string> GetAllAssetKeys() + lock (this.Lock) { - foreach (string cacheKey in this.Cache.Keys) + IEnumerable<string> GetAllAssetKeys() { - this.ParseCacheKey(cacheKey, out string assetKey, out string _); - yield return assetKey; + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string _); + yield return assetKey; + } } - } - return GetAllAssetKeys().Distinct(); + return GetAllAssetKeys().Distinct(); + } } /// <summary>Purge assets from the cache that match one of the interceptors.</summary> @@ -248,45 +263,48 @@ namespace StardewModdingAPI.Framework /// <returns>Returns whether any cache entries were invalidated.</returns> public bool InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false) { - // find matching asset keys - HashSet<string> purgeCacheKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); - HashSet<string> purgeAssetKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); - foreach (string cacheKey in this.Cache.Keys) + lock (this.Lock) { - this.ParseCacheKey(cacheKey, out string assetKey, out _); - Type type = this.Cache[cacheKey].GetType(); - if (predicate(assetKey, type)) + // find matching asset keys + HashSet<string> purgeCacheKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + HashSet<string> purgeAssetKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + foreach (string cacheKey in this.Cache.Keys) { - purgeAssetKeys.Add(assetKey); - purgeCacheKeys.Add(cacheKey); + 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); - } + // 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++; - } + // 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; + // 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; } - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return false; } /// <summary>Dispose assets for the given content manager shim.</summary> diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 76c106d7..6f8f7cef 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -20,9 +20,6 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; -#if SMAPI_1_x -using SFarmer = StardewValley.Farmer; -#endif namespace StardewModdingAPI.Framework { @@ -117,23 +114,6 @@ namespace StardewModdingAPI.Framework /// <summary>The time of day (in 24-hour military format) at last check.</summary> private int PreviousTime; -#if SMAPI_1_x - /// <summary>The day of month (1–28) at last check.</summary> - private int PreviousDay; - - /// <summary>The season name (winter, spring, summer, or fall) at last check.</summary> - private string PreviousSeason; - - /// <summary>The year number at last check.</summary> - private int PreviousYear; - - /// <summary>Whether the game was transitioning to a new day at last check.</summary> - private bool PreviousIsNewDay; - - /// <summary>The player character at last check.</summary> - private SFarmer PreviousFarmer; -#endif - /// <summary>The previous content locale.</summary> private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -164,10 +144,10 @@ namespace StardewModdingAPI.Framework private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue(); public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop public BlendState lightingBlend => SGame.Reflection.GetPrivateField<BlendState>(this, nameof(lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(new object[0]); - private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(new object[0]); - private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(new object[0]); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); + 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 @@ -295,10 +275,6 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) { GameEvents.InvokeInitialize(this.Monitor); -#if SMAPI_1_x - GameEvents.InvokeLoadContent(this.Monitor); -#endif - GameEvents.InvokeGameLoaded(this.Monitor); } /********* @@ -321,10 +297,8 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) { -#if !SMAPI_1_x if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) -#endif - this.AfterLoadTimer--; + this.AfterLoadTimer--; if (this.AfterLoadTimer == 0) { @@ -332,9 +306,6 @@ namespace StardewModdingAPI.Framework Context.IsWorldReady = true; SaveEvents.InvokeAfterLoad(this.Monitor); -#if SMAPI_1_x - PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); -#endif TimeEvents.InvokeAfterDayStarted(this.Monitor); } } @@ -403,25 +374,21 @@ namespace StardewModdingAPI.Framework bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); // get cursor position -#if !SMAPI_1_x 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 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((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); } -#endif // raise button pressed foreach (SButton button in framePressedKeys) { -#if !SMAPI_1_x InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); -#endif // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -441,12 +408,10 @@ namespace StardewModdingAPI.Framework // raise button released foreach (SButton button in frameReleasedKeys) { -#if !SMAPI_1_x 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); -#endif // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -524,12 +489,6 @@ namespace StardewModdingAPI.Framework if (this.GetHash(Game1.locations) != this.PreviousGameLocations) LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); -#if SMAPI_1_x - // raise player changed - if (Game1.player != this.PreviousFarmer) - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); -#endif - // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { @@ -559,14 +518,6 @@ namespace StardewModdingAPI.Framework // raise time changed if (Game1.timeOfDay != this.PreviousTime) TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); -#if SMAPI_1_x - if (Game1.dayOfMonth != this.PreviousDay) - TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); - if (Game1.currentSeason != this.PreviousSeason) - TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); - if (Game1.year != this.PreviousYear) - TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); -#endif // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) @@ -587,26 +538,9 @@ namespace StardewModdingAPI.Framework this.PreviousTime = Game1.timeOfDay; this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; this.PreviousSaveID = Game1.uniqueIDForThisGame; -#if SMAPI_1_x - this.PreviousFarmer = Game1.player; - this.PreviousDay = Game1.dayOfMonth; - this.PreviousSeason = Game1.currentSeason; - this.PreviousYear = Game1.year; -#endif } /********* - ** Game day transition event (obsolete) - *********/ -#if SMAPI_1_x - if (Game1.newDay != this.PreviousIsNewDay) - { - TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); - this.PreviousIsNewDay = Game1.newDay; - } -#endif - - /********* ** Game update *********/ try @@ -623,12 +557,7 @@ namespace StardewModdingAPI.Framework *********/ GameEvents.InvokeUpdateTick(this.Monitor); if (this.FirstUpdate) - { -#if SMAPI_1_x - GameEvents.InvokeFirstUpdateTick(this.Monitor); -#endif this.FirstUpdate = false; - } if (this.CurrentUpdateTick % 2 == 0) GameEvents.InvokeSecondUpdateTick(this.Monitor); if (this.CurrentUpdateTick % 4 == 0) diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 3193aa3c..3193aa3c 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs index 11ffdccb..917c950d 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -27,7 +27,8 @@ namespace StardewModdingAPI.Framework.Serialisation return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]) - || objectType == typeof(ModCompatibilityID[]); + || objectType == typeof(ModDataID) + || objectType == typeof(ModCompatibility[]); } /// <summary>Reads the JSON representation of the object.</summary> @@ -68,7 +69,7 @@ namespace StardewModdingAPI.Framework.Serialisation } } - // manifest dependency + // manifest dependencies if (objectType == typeof(IManifestDependency[])) { List<IManifestDependency> result = new List<IManifestDependency>(); @@ -76,26 +77,30 @@ namespace StardewModdingAPI.Framework.Serialisation { string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID)); string minVersion = obj.Value<string>(nameof(IManifestDependency.MinimumVersion)); -#if SMAPI_1_x - result.Add(new ManifestDependency(uniqueID, minVersion)); -#else bool required = obj.Value<bool?>(nameof(IManifestDependency.IsRequired)) ?? true; result.Add(new ManifestDependency(uniqueID, minVersion, required)); -#endif } return result.ToArray(); } - // mod compatibility ID - if (objectType == typeof(ModCompatibilityID[])) + // mod data ID + if (objectType == typeof(ModDataID)) { - List<ModCompatibilityID> result = new List<ModCompatibilityID>(); - foreach (JToken child in JArray.Load(reader).Children()) + JToken token = JToken.Load(reader); + return new ModDataID(token.Value<string>()); + } + + // mod compatibility records + if (objectType == typeof(ModCompatibility[])) + { + List<ModCompatibility> result = new List<ModCompatibility>(); + foreach (JProperty property in JObject.Load(reader).Properties()) { - result.Add(child is JValue value - ? new ModCompatibilityID(value.Value<string>()) - : child.ToObject<ModCompatibilityID>() - ); + string range = property.Name; + ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value<string>(nameof(ModCompatibility.Status))); + string reasonPhrase = property.Value.Value<string>(nameof(ModCompatibility.ReasonPhrase)); + + result.Add(new ModCompatibility(range, status, reasonPhrase)); } return result.ToArray(); } diff --git a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs index 37108556..37108556 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/SMAPI/Framework/Utilities/ContextHash.cs index 0d8487bb..6c0fdc90 100644 --- a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs +++ b/src/SMAPI/Framework/Utilities/ContextHash.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; namespace StardewModdingAPI.Framework.Utilities @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Framework.Utilities /// <exception cref="InvalidOperationException">The specified key is already added.</exception> public void Track(T key, Action action) { - if(this.Contains(key)) + if (this.Contains(key)) throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); this.Add(key); diff --git a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs b/src/SMAPI/Framework/Utilities/Countdown.cs index 921a35ce..921a35ce 100644 --- a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs +++ b/src/SMAPI/Framework/Utilities/Countdown.cs diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs new file mode 100644 index 00000000..e78ac14b --- /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.Common.Models; + +namespace StardewModdingAPI.Framework +{ + /// <summary>Provides methods for interacting with the SMAPI web API.</summary> + internal class WebApiClient + { + /********* + ** Properties + *********/ + /// <summary>The base URL for the web API.</summary> + private readonly Uri BaseUrl; + + /// <summary>The API version number.</summary> + private readonly ISemanticVersion Version; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="baseUrl">The base URL for the web API.</param> + /// <param name="version">The web API version.</param> + 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; + } + + /// <summary>Get the latest SMAPI version.</summary> + /// <param name="modKeys">The mod keys for which to fetch the latest version.</param> + public IDictionary<string, ModInfoModel> GetModInfo(params string[] modKeys) + { + return this.Post<ModSearchModel, Dictionary<string, ModInfoModel>>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys) + ); + } + + + /********* + ** Private methods + *********/ + /// <summary>Fetch the response from the backend API.</summary> + /// <typeparam name="TBody">The body content type.</typeparam> + /// <typeparam name="TResult">The expected response type.</typeparam> + /// <param name="url">The request URL, optionally excluding the base URL.</param> + /// <param name="content">The body content to post.</param> + private TResult Post<TBody, TResult>(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<TResult>(response); + } + } + } +} diff --git a/src/StardewModdingAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs index c3021144..c3021144 100644 --- a/src/StardewModdingAPI/IAssetData.cs +++ b/src/SMAPI/IAssetData.cs diff --git a/src/StardewModdingAPI/IAssetDataForDictionary.cs b/src/SMAPI/IAssetDataForDictionary.cs index 53c24346..53c24346 100644 --- a/src/StardewModdingAPI/IAssetDataForDictionary.cs +++ b/src/SMAPI/IAssetDataForDictionary.cs diff --git a/src/StardewModdingAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs index 4584a20e..4584a20e 100644 --- a/src/StardewModdingAPI/IAssetDataForImage.cs +++ b/src/SMAPI/IAssetDataForImage.cs diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs index d2c6f295..d2c6f295 100644 --- a/src/StardewModdingAPI/IAssetEditor.cs +++ b/src/SMAPI/IAssetEditor.cs diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index 5dd58e2e..5dd58e2e 100644 --- a/src/StardewModdingAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs index ad97b941..ad97b941 100644 --- a/src/StardewModdingAPI/IAssetLoader.cs +++ b/src/SMAPI/IAssetLoader.cs diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs index fb562e32..fb562e32 100644 --- a/src/StardewModdingAPI/ICommandHelper.cs +++ b/src/SMAPI/ICommandHelper.cs diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index b4557134..b78b165b 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; @@ -12,13 +12,11 @@ namespace StardewModdingAPI /********* ** Accessors *********/ -#if !SMAPI_1_x /// <summary>Interceptors which provide the initial versions of matching content assets.</summary> IList<IAssetLoader> AssetLoaders { get; } /// <summary>Interceptors which edit matching content assets after they're loaded.</summary> IList<IAssetEditor> AssetEditors { get; } -#endif /// <summary>The game's current locale code (like <c>pt-BR</c>).</summary> string CurrentLocale { get; } @@ -44,7 +42,6 @@ namespace StardewModdingAPI /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); -#if !SMAPI_1_x /// <summary>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.</summary> /// <param name="key">The asset key to invalidate in the content folder.</param> /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> @@ -55,6 +52,5 @@ namespace StardewModdingAPI /// <typeparam name="T">The asset type to remove from the cache.</typeparam> /// <returns>Returns whether any assets were invalidated.</returns> bool InvalidateCache<T>(); -#endif } } diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs index 8fbc115f..ddb8eb49 100644 --- a/src/StardewModdingAPI/ICursorPosition.cs +++ b/src/SMAPI/ICursorPosition.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using Microsoft.Xna.Framework; namespace StardewModdingAPI @@ -16,4 +15,3 @@ namespace StardewModdingAPI Vector2 GrabTile { get; } } } -#endif diff --git a/src/StardewModdingAPI/IManifest.cs b/src/SMAPI/IManifest.cs index 407db1ce..9db1d538 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/SMAPI/IManifest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace StardewModdingAPI { @@ -26,13 +26,16 @@ namespace StardewModdingAPI /// <summary>The unique mod ID.</summary> string UniqueID { get; } - /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> + /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method.</summary> string EntryDll { get; } /// <summary>The other mods that must be loaded before this mod.</summary> IManifestDependency[] Dependencies { get; } + /// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary> + string[] UpdateKeys { get; set; } + /// <summary>Any manifest fields which didn't match a valid field.</summary> IDictionary<string, object> ExtraFields { get; } } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs index 1fa6c812..e86cd1f4 100644 --- a/src/StardewModdingAPI/IManifestDependency.cs +++ b/src/SMAPI/IManifestDependency.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI +namespace StardewModdingAPI { /// <summary>A mod dependency listed in a mod manifest.</summary> public interface IManifestDependency @@ -12,9 +12,7 @@ /// <summary>The minimum required version (if any).</summary> ISemanticVersion MinimumVersion { get; } -#if !SMAPI_1_x /// <summary>Whether the dependency must be installed to use the mod.</summary> bool IsRequired { get; } -#endif } } diff --git a/src/StardewModdingAPI/IMod.cs b/src/SMAPI/IMod.cs index 35ac7c0f..35ac7c0f 100644 --- a/src/StardewModdingAPI/IMod.cs +++ b/src/SMAPI/IMod.cs diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 116e8508..116e8508 100644 --- a/src/StardewModdingAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs diff --git a/src/StardewModdingAPI/IModLinked.cs b/src/SMAPI/IModLinked.cs index 172ee30c..172ee30c 100644 --- a/src/StardewModdingAPI/IModLinked.cs +++ b/src/SMAPI/IModLinked.cs diff --git a/src/StardewModdingAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index 5ef3fd65..5ef3fd65 100644 --- a/src/StardewModdingAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs diff --git a/src/StardewModdingAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs index 62c479bc..62c479bc 100644 --- a/src/StardewModdingAPI/IMonitor.cs +++ b/src/SMAPI/IMonitor.cs diff --git a/src/StardewModdingAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs index 3e681c12..3e681c12 100644 --- a/src/StardewModdingAPI/IPrivateField.cs +++ b/src/SMAPI/IPrivateField.cs diff --git a/src/StardewModdingAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs index 67fc8b3c..67fc8b3c 100644 --- a/src/StardewModdingAPI/IPrivateMethod.cs +++ b/src/SMAPI/IPrivateMethod.cs diff --git a/src/StardewModdingAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs index 8d67fa7a..8d67fa7a 100644 --- a/src/StardewModdingAPI/IPrivateProperty.cs +++ b/src/SMAPI/IPrivateProperty.cs diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs index fb2c7861..fb2c7861 100644 --- a/src/StardewModdingAPI/IReflectionHelper.cs +++ b/src/SMAPI/IReflectionHelper.cs diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs index c1a4ca3a..0483c97b 100644 --- a/src/StardewModdingAPI/ISemanticVersion.cs +++ b/src/SMAPI/ISemanticVersion.cs @@ -1,12 +1,9 @@ -using System; +using System; namespace StardewModdingAPI { /// <summary>A semantic version with an optional release tag.</summary> - public interface ISemanticVersion : IComparable<ISemanticVersion> -#if !SMAPI_1_x - , IEquatable<ISemanticVersion> -#endif + public interface ISemanticVersion : IComparable<ISemanticVersion>, IEquatable<ISemanticVersion> { /********* ** Accessors @@ -59,4 +56,4 @@ namespace StardewModdingAPI /// <summary>Get a string representation of the version.</summary> string ToString(); } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs index c4b72444..c4b72444 100644 --- a/src/StardewModdingAPI/ITranslationHelper.cs +++ b/src/SMAPI/ITranslationHelper.cs diff --git a/src/StardewModdingAPI/LogLevel.cs b/src/SMAPI/LogLevel.cs index 89647876..89647876 100644 --- a/src/StardewModdingAPI/LogLevel.cs +++ b/src/SMAPI/LogLevel.cs diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs index 24f23af7..5a98da4b 100644 --- a/src/StardewModdingAPI/Metadata/CoreAssets.cs +++ b/src/SMAPI/Metadata/CoreAssets.cs @@ -37,6 +37,10 @@ namespace StardewModdingAPI.Metadata this.SingletonSetters = new Dictionary<string, Action<SContentManager, string>> { + // from CraftingRecipe.InitShared + ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key), + ["Data\\CookingRecipes"] = (content, key) => CraftingRecipe.cookingRecipes = content.Load<Dictionary<string, string>>(key), + // from Game1.loadContent ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load<Texture2D>(key), ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load<Texture2D>(key), 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 +{ + /// <summary>Provides CIL instruction handlers which rewrite mods for compatibility and throw exceptions for incompatible code.</summary> + internal class InstructionMetadata + { + /********* + ** Public methods + *********/ + /// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary> + public IEnumerable<IInstructionHandler> 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 +{ + /// <summary>The base class for a mod.</summary> + public abstract class Mod : IMod, IDisposable + { + /********* + ** Accessors + *********/ + /// <summary>Provides simplified APIs for writing mods.</summary> + public IModHelper Helper { get; internal set; } + + /// <summary>Writes messages to the console and log file.</summary> + public IMonitor Monitor { get; internal set; } + + /// <summary>The mod's manifest.</summary> + public IManifest ModManifest { get; internal set; } + + + /********* + ** Public methods + *********/ + /// <summary>The mod entry point, called after the mod is first loaded.</summary> + /// <param name="helper">Provides simplified APIs for writing mods.</param> + public abstract void Entry(IModHelper helper); + + /// <summary>Release or reset unmanaged resources.</summary> + 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 + *********/ + /// <summary>Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit.</summary> + /// <param name="disposing">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.</param> + protected virtual void Dispose(bool disposing) { } + + /// <summary>Destruct the instance.</summary> + ~Mod() + { + this.Dispose(false); + } + } +} diff --git a/src/StardewModdingAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs index b4286a89..b4286a89 100644 --- a/src/StardewModdingAPI/PatchMode.cs +++ b/src/SMAPI/PatchMode.cs diff --git a/src/StardewModdingAPI/Program.cs b/src/SMAPI/Program.cs index ad873598..fe306e24 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -12,7 +12,7 @@ using System.Management; using System.Windows.Forms; #endif using Newtonsoft.Json; -using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Exceptions; @@ -129,9 +129,6 @@ namespace StardewModdingAPI 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); -#if SMAPI_1_x - this.Monitor.Log("Preparing SMAPI..."); -#endif // validate paths this.VerifyPath(Constants.ModPath); @@ -188,7 +185,6 @@ namespace StardewModdingAPI #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); - GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles @@ -214,11 +210,7 @@ namespace StardewModdingAPI } // start game -#if SMAPI_1_x - this.Monitor.Log("Starting game..."); -#else this.Monitor.Log("Starting game...", LogLevel.Trace); -#endif try { this.IsGameRunning = true; @@ -235,16 +227,6 @@ namespace StardewModdingAPI } } -#if SMAPI_1_x - /// <summary>Get a monitor for legacy code which doesn't have one passed in.</summary> - [Obsolete("This method should only be used when needed for backwards compatibility.")] - internal IMonitor GetLegacyMonitorForMod() - { - string modName = this.ModRegistry.GetModFromStack() ?? "unknown"; - return this.GetSecondaryMonitor(modName); - } -#endif - /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> public void Dispose() { @@ -304,7 +286,7 @@ namespace StardewModdingAPI { PrintErrorAndExit( "Oops! SMAPI can't find the game. " - + (Assembly.GetCallingAssembly().Location?.Contains(Path.Combine("internal", "Windows")) == true || Assembly.GetCallingAssembly().Location?.Contains(Path.Combine("internal", "Mono")) == true + + (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. " ) @@ -334,19 +316,6 @@ namespace StardewModdingAPI this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); -#if SMAPI_1_x - // inject compatibility shims -#pragma warning disable 618 - Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); - Config.Shim(this.DeprecationManager); - Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); - Mod.Shim(this.DeprecationManager); - GameEvents.Shim(this.DeprecationManager); - PlayerEvents.Shim(this.DeprecationManager); - TimeEvents.Shim(this.DeprecationManager); -#pragma warning restore 618 -#endif - // redirect direct console output { Monitor monitor = this.GetSecondaryMonitor("Console.Out"); @@ -358,17 +327,14 @@ namespace StardewModdingAPI if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; -#if !SMAPI_1_x this.Monitor.ShowFullStampInConsole = true; -#endif 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); - if (this.Settings.VerboseLogging) - this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace); + this.VerboseLog("Verbose logging enabled."); // validate XNB integrity if (!this.ValidateContentIntegrity()) @@ -376,67 +342,21 @@ namespace StardewModdingAPI // load mods { -#if SMAPI_1_x - this.Monitor.Log("Loading mod metadata..."); -#else this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); -#endif ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility, this.Settings.DisabledMods).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion); - - // check for deprecated metadata -#if SMAPI_1_x - IList<Action> deprecationWarnings = new List<Action>(); - foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) - { - // missing fields that will be required in SMAPI 2.0 - { - List<string> missingFields = new List<string>(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()) - deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} is missing some manifest fields ({string.Join(", ", missingFields)}) which will be required in an upcoming SMAPI version.", LogLevel.Warn)); - } - - // per-save directories - if ((mod.Manifest as Manifest)?.PerSaveConfigs == true) - { - deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.DisplayName, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.PendingRemoval)); - try - { - string psDir = Path.Combine(mod.DirectoryPath, "psconfigs"); - Directory.CreateDirectory(psDir); - if (!Directory.Exists(psDir)) - mod.SetStatus(ModMetadataStatus.Failed, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); - } - catch (Exception ex) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); - } - } - } -#endif + 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 -#if SMAPI_1_x - this.LoadMods(mods, new JsonHelper(), this.ContentManager, deprecationWarnings); - foreach (Action warning in deprecationWarnings) - warning(); -#else this.LoadMods(mods, new JsonHelper(), this.ContentManager); -#endif + + // check for updates + this.CheckForUpdatesAsync(mods); } if (this.Monitor.IsExiting) { @@ -470,9 +390,6 @@ namespace StardewModdingAPI private void RunConsoleLoop() { // prepare console -#if SMAPI_1_x - this.Monitor.Log("Starting console..."); -#endif this.Monitor.Log("Type 'help' for help, or 'help <cmd>' 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 <cmd>\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); @@ -563,24 +480,125 @@ namespace StardewModdingAPI return !issuesFound; } - /// <summary>Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available.</summary> - private void CheckForUpdateAsync() + /// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> + /// <param name="mods">The mods to include in the update check (if eligible).</param> + 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 { - GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; - ISemanticVersion latestVersion = new SemanticVersion(release.Tag); - if (latestVersion.IsNewerThan(Constants.ApiVersion)) - this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); + // 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<string, IModMetadata[]> 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<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>(); + 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 a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); + this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } }).Start(); } @@ -604,24 +622,16 @@ namespace StardewModdingAPI /// <param name="mods">The mods to load.</param> /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param> /// <param name="contentManager">The content manager to use for mod content.</param> -#if SMAPI_1_x - /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param> - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList<Action> deprecationWarnings) -#else private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) -#endif { -#if SMAPI_1_x - this.Monitor.Log("Loading mods..."); -#else this.Monitor.Log("Loading mods...", LogLevel.Trace); -#endif + // load mod assemblies IDictionary<IModMetadata, string> skippedMods = new Dictionary<IModMetadata, string>(); { void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); + 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) { @@ -646,15 +656,11 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { -#if SMAPI_1_x - TrackSkip(metadata, $"it's not compatible with the latest version of the game or SMAPI (detected {ex.NounPhrase}). Please check for a newer version of the mod."); -#else TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); -#endif continue; } catch (SAssemblyLoadFailedException ex) @@ -701,16 +707,6 @@ namespace StardewModdingAPI continue; } -#if SMAPI_1_x - // prevent mods from using SMAPI 2.0 content interception before release - // ReSharper disable SuspiciousTypeConversion.Global - if (mod is IAssetEditor || mod is IAssetLoader) - { - TrackSkip(metadata, $"its entry class implements {nameof(IAssetEditor)} or {nameof(IAssetLoader)}. These are part of a prototype API that isn't available for mods to use yet."); - } - // ReSharper restore SuspiciousTypeConversion.Global -#endif - // inject data { IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); @@ -723,9 +719,6 @@ namespace StardewModdingAPI mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); mod.Monitor = monitor; -#if SMAPI_1_x - mod.PathOnDisk = metadata.DirectoryPath; -#endif } // track mod @@ -741,9 +734,7 @@ namespace StardewModdingAPI IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods -#if !SMAPI_1_x this.Monitor.Newline(); -#endif if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); @@ -757,9 +748,7 @@ namespace StardewModdingAPI else this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); } -#if !SMAPI_1_x this.Monitor.Newline(); -#endif } // log loaded mods @@ -774,9 +763,7 @@ namespace StardewModdingAPI LogLevel.Info ); } -#if !SMAPI_1_x this.Monitor.Newline(); -#endif // initialise translations this.ReloadTranslations(); @@ -787,6 +774,13 @@ namespace StardewModdingAPI // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) { + // ReSharper disable SuspiciousTypeConversion.Global + if (metadata.Mod is IAssetEditor editor) + helper.ObservableAssetEditors.Add(editor); + if (metadata.Mod is IAssetLoader loader) + helper.ObservableAssetLoaders.Add(loader); + // ReSharper restore SuspiciousTypeConversion.Global + this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; } @@ -796,16 +790,6 @@ namespace StardewModdingAPI { IMod mod = metadata.Mod; mod.Entry(mod.Helper); -#if SMAPI_1_x - (mod as Mod)?.Entry(); // deprecated since 1.0 - - // raise deprecation warning for old Entry() methods - if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.PendingRemoval)); -#else - if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(IModHelper) })) - this.Monitor.Log($"{metadata.DisplayName} doesn't implement Entry() and may not work correctly.", LogLevel.Error); -#endif } catch (Exception ex) { @@ -839,8 +823,8 @@ namespace StardewModdingAPI } // reset cache now if any editors or loaders were added during entry - IAssetEditor[] editors = loadedMods.SelectMany(p => ((ContentHelper)p.Mod.Helper.Content).AssetEditors).ToArray(); - IAssetLoader[] loaders = loadedMods.SelectMany(p => ((ContentHelper)p.Mod.Helper.Content).AssetLoaders).ToArray(); + 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); @@ -889,7 +873,7 @@ namespace StardewModdingAPI case "help": if (arguments.Any()) { - Framework.Command result = this.CommandManager.Get(arguments[0]); + Command result = this.CommandManager.Get(arguments[0]); if (result == null) this.Monitor.Log("There's no command with that name.", LogLevel.Error); else @@ -897,10 +881,6 @@ namespace StardewModdingAPI } else { -#if SMAPI_1_x - this.Monitor.Log("The following commands are registered: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)) + ".", LogLevel.Info); - this.Monitor.Log("For more information about a command, type 'help command_name'.", LogLevel.Info); -#else string message = "The following commands are registered:\n"; IGrouping<string, string>[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); foreach (var group in groups) @@ -912,7 +892,6 @@ namespace StardewModdingAPI message += "For more information about a command, type 'help command_name'."; this.Monitor.Log(message, LogLevel.Info); -#endif } break; @@ -961,9 +940,7 @@ namespace StardewModdingAPI { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, -#if !SMAPI_1_x ShowFullStampInConsole = this.Settings.DeveloperMode -#endif }; } @@ -984,5 +961,13 @@ namespace StardewModdingAPI #endif return Environment.OSVersion.ToString(); } + + /// <summary>Log a message if verbose mode is enabled.</summary> + /// <param name="message">The message to log.</param> + 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/SMAPI/Properties/AssemblyInfo.cs index b0a065f5..b0a065f5 100644 --- a/src/StardewModdingAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/SMAPI/SButton.cs index 33058a64..0ec799db 100644 --- a/src/StardewModdingAPI/Utilities/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -2,16 +2,11 @@ using System; using Microsoft.Xna.Framework.Input; using StardewValley; -namespace StardewModdingAPI.Utilities +namespace StardewModdingAPI { /// <summary>A unified button constant which includes all controller, keyboard, and mouse buttons.</summary> /// <remarks>Derived from <see cref="Keys"/>, <see cref="Buttons"/>, and <see cref="System.Windows.Forms.MouseButtons"/>.</remarks> -#if SMAPI_1_x - internal -#else - public -#endif - enum SButton + public enum SButton { /// <summary>No valid key.</summary> None = 0, @@ -594,12 +589,7 @@ namespace StardewModdingAPI.Utilities } /// <summary>Provides extension methods for <see cref="SButton"/>.</summary> -#if SMAPI_1_x - internal -#else - public -#endif - static class SButtonExtensions + public static class SButtonExtensions { /********* ** Accessors diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index e448eae1..ce86dceb 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,6 @@ -using System; -using System.Text.RegularExpressions; +using System; using Newtonsoft.Json; +using StardewModdingAPI.Common; namespace StardewModdingAPI { @@ -10,31 +10,24 @@ namespace StardewModdingAPI /********* ** Properties *********/ - /// <summary>A regular expression matching a semantic version string.</summary> - /// <remarks> - /// This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// </remarks> - private static readonly Regex Regex = new Regex(@"^(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + /// <summary>The underlying semantic version implementation.</summary> + private readonly SemanticVersionImpl Version; /********* ** Accessors *********/ /// <summary>The major version incremented for major API changes.</summary> - public int MajorVersion { get; } + public int MajorVersion => this.Version.Major; /// <summary>The minor version incremented for backwards-compatible changes.</summary> - public int MinorVersion { get; } + public int MinorVersion => this.Version.Minor; /// <summary>The patch version for backwards-compatible bug fixes.</summary> - public int PatchVersion { get; } + public int PatchVersion => this.Version.Patch; /// <summary>An optional build tag.</summary> - public string Build { get; } + public string Build => this.Version.Tag; /********* @@ -47,32 +40,14 @@ namespace StardewModdingAPI /// <param name="build">An optional build tag.</param> [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); - } + : this(new SemanticVersionImpl(majorVersion, minorVersion, patchVersion, build)) { } /// <summary>Construct an instance.</summary> /// <param name="version">The semantic version string.</param> /// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception> /// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception> public 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; - } + : this(new SemanticVersionImpl(version)) { } /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> /// <param name="other">The version to compare with this instance.</param> @@ -80,56 +55,7 @@ namespace StardewModdingAPI /// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks> 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); + return this.Version.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); } /// <summary>Get whether this version is older than the specified version.</summary> @@ -179,7 +105,6 @@ namespace StardewModdingAPI return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); } -#if !SMAPI_1_x /// <summary>Indicates whether the current object is equal to another object of the same type.</summary> /// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns> /// <param name="other">An object to compare with this object.</param> @@ -187,21 +112,11 @@ namespace StardewModdingAPI { return other != null && this.CompareTo(other) == 0; } -#endif /// <summary>Get a string representation of the version.</summary> 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; + return this.Version.ToString(); } /// <summary>Parse a version string without throwing an exception if it fails.</summary> @@ -210,30 +125,26 @@ namespace StardewModdingAPI /// <returns>Returns whether parsing the version succeeded.</returns> internal static bool TryParse(string version, out ISemanticVersion parsed) { - try + if (SemanticVersionImpl.TryParse(version, out SemanticVersionImpl versionImpl)) { - parsed = new SemanticVersion(version); + parsed = new SemanticVersion(versionImpl); return true; } - catch - { - parsed = null; - return false; - } + + parsed = null; + return false; } /********* ** Private methods *********/ - /// <summary>Get a normalised build tag.</summary> - /// <param name="tag">The tag to normalise.</param> - private string GetNormalisedTag(string tag) + /// <summary>Construct an instance.</summary> + /// <param name="version">The underlying semantic version implementation.</param> + private SemanticVersion(SemanticVersionImpl version) { - tag = tag?.Trim(); - if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation - return null; - return tag; + this.Version = version; } + } } diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json new file mode 100644 index 00000000..fa3bdcc9 --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -0,0 +1,2068 @@ +/* + + + +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" ] // added in 1.4.1 + }, + { + // 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" ] // added in 1.4.1 + }, + { + // 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" ], // added in 1.4.1 + "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" ], // added in 1.4.1 + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Butcher Mod + "ID": "DIGUS.BUTCHER", + "UpdateKeys": [ "Nexus:1538" ] + }, + { + // Buy Cooking Recipes + "ID": "Denifia.BuyRecipes", + "UpdateKeys": [ "Nexus:1126" ], // added in 1.0.1 (2017-10-04) + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Buy Back Collectables + "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 + "UpdateKeys": [ "Nexus:507" ], // added in 1.4.1 + "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" ] // added in 1.4.1 + }, + { + // 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" ] // added in 1.4.1 + }, + { + // 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" ], // added in 1.4.1 + "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 + "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" ], // added in 1.4.1 + "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" ], + "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" ], // added in 1.0.4-pathoschild-update + "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 + } + }, + { + // Jiggly Junimo Bundles + "ID": "JJB.dll | Greger.JigglyJunimoBundles", // changed in 1.1.2-pathoschild-update + "UpdateKeys": [ "GitHub:gr3ger/Stardew_JJB" ], // added in 1.0.4-pathoschild-update + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + { + // 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 + } + }, + { + // Mail Framework + "ID": "DIGUS.MailFrameworkMod", + "UpdateKeys": [ "Nexus:1536" ] + }, + { + // 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" ], // added in 1.5.1 + "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" ], // added in 1.4.1 + "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 + "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere + "UpdateKeys": [ "Nexus:433" ], // added in 1.4.1 + "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" ] // added in 1.4.1 + }, + { + // 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" ], + "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" ], + "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" ], // added in 2.6.1 + "Compatibility": { + "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // 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" ], // added in 1.3.1 + "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 + "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) + "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" ], // added in 1.0.1 + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", // can remove once 1.0.1 is published + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // 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" ], // added in 1.4.1 + "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" ], + "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" ], + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // TimeFreeze + "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe | Omegasis.TimeFreeze", // changed in 1.2 + "UpdateKeys": [ "Nexus:973" ] // added in 1.2.1 + }, + { + // 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 + "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/SMAPI/StardewModdingAPI.csproj index 8daf21b7..b8d5990e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -65,9 +65,8 @@ <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath> <Private>True</Private> </Reference> - <Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -87,10 +86,26 @@ <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> - <Compile Include="..\GlobalAssemblyInfo.cs"> + <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> - <Compile Include="Command.cs" /> + <Compile Include="Framework\Models\ModCompatibility.cs" /> + <Compile Include="Framework\ModLoading\Finders\EventFinder.cs" /> + <Compile Include="Framework\ModLoading\Finders\FieldFinder.cs" /> + <Compile Include="Framework\ModLoading\Finders\MethodFinder.cs" /> + <Compile Include="Framework\ModLoading\Finders\PropertyFinder.cs" /> + <Compile Include="Framework\ModLoading\Finders\TypeFinder.cs" /> + <Compile Include="Framework\ModLoading\IInstructionHandler.cs" /> + <Compile Include="Framework\ModLoading\IncompatibleInstructionException.cs" /> + <Compile Include="Framework\ModLoading\InstructionHandleResult.cs" /> + <Compile Include="Framework\ModLoading\Platform.cs" /> + <Compile Include="Framework\ModLoading\PlatformAssemblyMap.cs" /> + <Compile Include="Framework\ModLoading\RewriteHelper.cs" /> + <Compile Include="Framework\ModLoading\Rewriters\FieldReplaceRewriter.cs" /> + <Compile Include="Framework\ModLoading\Rewriters\FieldToPropertyRewriter.cs" /> + <Compile Include="Framework\ModLoading\Rewriters\VirtualEntryCallRemover.cs" /> + <Compile Include="Framework\ModLoading\Rewriters\MethodParentRewriter.cs" /> + <Compile Include="Framework\ModLoading\Rewriters\TypeReferenceRewriter.cs" /> <Compile Include="Framework\ContentManagerShim.cs" /> <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> @@ -104,10 +119,8 @@ <Compile Include="Framework\Content\AssetInfo.cs" /> <Compile Include="Framework\Exceptions\SContentLoadException.cs" /> <Compile Include="Framework\Command.cs" /> - <Compile Include="Config.cs" /> <Compile Include="Constants.cs" /> <Compile Include="Events\ControlEvents.cs" /> - <Compile Include="Events\EventArgsCommand.cs" /> <Compile Include="Events\EventArgsClickableMenuChanged.cs" /> <Compile Include="Events\EventArgsClickableMenuClosed.cs" /> <Compile Include="Events\EventArgsControllerButtonPressed.cs" /> @@ -115,26 +128,21 @@ <Compile Include="Events\EventArgsControllerTriggerPressed.cs" /> <Compile Include="Events\EventArgsControllerTriggerReleased.cs" /> <Compile Include="Events\EventArgsCurrentLocationChanged.cs" /> - <Compile Include="Events\EventArgsFarmerChanged.cs" /> <Compile Include="Events\EventArgsGameLocationsChanged.cs" /> <Compile Include="Events\EventArgsIntChanged.cs" /> <Compile Include="Events\EventArgsInventoryChanged.cs" /> <Compile Include="Events\EventArgsKeyboardStateChanged.cs" /> <Compile Include="Events\EventArgsKeyPressed.cs" /> <Compile Include="Events\EventArgsLevelUp.cs" /> - <Compile Include="Events\EventArgsLoadedGameChanged.cs" /> <Compile Include="Events\EventArgsLocationObjectsChanged.cs" /> <Compile Include="Events\EventArgsMineLevelChanged.cs" /> <Compile Include="Events\EventArgsMouseStateChanged.cs" /> - <Compile Include="Events\EventArgsNewDay.cs" /> - <Compile Include="Events\EventArgsStringChanged.cs" /> <Compile Include="Events\GameEvents.cs" /> <Compile Include="Events\GraphicsEvents.cs" /> <Compile Include="Framework\Utilities\Countdown.cs" /> <Compile Include="Framework\GameVersion.cs" /> <Compile Include="Framework\IModMetadata.cs" /> - <Compile Include="Framework\Models\DisabledMod.cs" /> - <Compile Include="Framework\Models\ModCompatibilityID.cs" /> + <Compile Include="Framework\Models\ModDataID.cs" /> <Compile Include="Framework\ModHelpers\BaseHelper.cs" /> <Compile Include="Framework\ModHelpers\CommandHelper.cs" /> <Compile Include="Framework\ModHelpers\ContentHelper.cs" /> @@ -157,7 +165,7 @@ <Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" /> <Compile Include="Framework\Logging\InterceptingTextWriter.cs" /> <Compile Include="Framework\Models\ManifestDependency.cs" /> - <Compile Include="Framework\Models\ModCompatibilityType.cs" /> + <Compile Include="Framework\Models\ModStatus.cs" /> <Compile Include="Framework\Models\SConfig.cs" /> <Compile Include="Framework\ModLoading\ModMetadata.cs" /> <Compile Include="Framework\Reflection\PrivateProperty.cs" /> @@ -186,7 +194,7 @@ <Compile Include="Framework\DeprecationLevel.cs" /> <Compile Include="Framework\DeprecationManager.cs" /> <Compile Include="Framework\InternalExtensions.cs" /> - <Compile Include="Framework\Models\ModCompatibility.cs" /> + <Compile Include="Framework\Models\ModDataRecord.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoader.cs" /> <Compile Include="Framework\Reflection\CacheEntry.cs" /> <Compile Include="Framework\Reflection\PrivateField.cs" /> @@ -202,14 +210,13 @@ <Compile Include="ITranslationHelper.cs" /> <Compile Include="LogLevel.cs" /> <Compile Include="Framework\ModRegistry.cs" /> - <Compile Include="Framework\UpdateHelper.cs" /> - <Compile Include="Framework\Models\GitRelease.cs" /> + <Compile Include="Framework\WebApiClient.cs" /> <Compile Include="IMonitor.cs" /> <Compile Include="Events\ChangeType.cs" /> <Compile Include="Events\ItemStackChange.cs" /> - <Compile Include="Log.cs" /> <Compile Include="Framework\Monitor.cs" /> <Compile Include="Framework\Models\Manifest.cs" /> + <Compile Include="Metadata\InstructionMetadata.cs" /> <Compile Include="Mod.cs" /> <Compile Include="PatchMode.cs" /> <Compile Include="Program.cs" /> @@ -222,7 +229,7 @@ <Compile Include="Translation.cs" /> <Compile Include="ICursorPosition.cs" /> <Compile Include="Utilities\SDate.cs" /> - <Compile Include="Utilities\SButton.cs" /> + <Compile Include="SButton.cs" /> <Compile Include="Framework\CursorPosition.cs" /> </ItemGroup> <ItemGroup> @@ -257,12 +264,13 @@ <Install>false</Install> </BootstrapperPackage> </ItemGroup> + <Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" /> <ItemGroup> - <ProjectReference Include="..\StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj"> + <ProjectReference Include="..\SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj"> <Project>{10db0676-9fc1-4771-a2c8-e2519f091e49}</Project> <Name>StardewModdingAPI.AssemblyRewriters</Name> </ProjectReference> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\common.targets" /> + <Import Project="..\..\build\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI/Translation.cs b/src/SMAPI/Translation.cs index ce344f81..ce344f81 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/SMAPI/Translation.cs diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 5073259d..2630731a 100644 --- a/src/StardewModdingAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -32,10 +32,8 @@ namespace StardewModdingAPI.Utilities /// <summary>The year.</summary> public int Year { get; } -#if !SMAPI_1_x /// <summary>The day of week.</summary> public DayOfWeek DayOfWeek { get; } -#endif /********* @@ -54,30 +52,12 @@ namespace StardewModdingAPI.Utilities /// <param name="year">The year.</param> /// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception> 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; -#if !SMAPI_1_x - this.DayOfWeek = this.GetDayOfWeek(); -#endif - } + : this(day, season, year, allowDayZero: false) { } /// <summary>Get the current in-game date.</summary> public static SDate Now() { - return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year); + return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } /// <summary>Get a new date with the given number of days added.</summary> @@ -199,6 +179,42 @@ namespace StardewModdingAPI.Utilities /********* ** Private methods *********/ + /// <summary>Construct an instance.</summary> + /// <param name="day">The day of month.</param> + /// <param name="season">The season name.</param> + /// <param name="year">The year.</param> + /// <param name="allowDayZero">Whether to allow 0 spring Y1 as a valid date.</param> + /// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception> + private SDate(int day, string season, int year, bool allowDayZero) + { + // 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 < 0 || day > this.DaysInSeason) + throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); + if(day == 0 && !(allowDayZero && this.IsDayZero(day, season, year))) + 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(); + } + + /// <summary>Get whether a date represents 0 spring Y1, which is the date during the in-game intro.</summary> + /// <param name="day">The day of month.</param> + /// <param name="season">The season name.</param> + /// <param name="year">The year.</param> + private bool IsDayZero(int day, string season, int year) + { + return day == 0 && season == "spring" && year == 1; + } + /// <summary>Get the day of week for the current date.</summary> private DayOfWeek GetDayOfWeek() { diff --git a/src/StardewModdingAPI/icon.ico b/src/SMAPI/icon.ico Binary files differindex 587a6e74..587a6e74 100644 --- a/src/StardewModdingAPI/icon.ico +++ b/src/SMAPI/icon.ico diff --git a/src/StardewModdingAPI/packages.config b/src/SMAPI/packages.config index e5fa3c3a..98d742c7 100644 --- a/src/StardewModdingAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <packages> <package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" /> - <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/src/StardewModdingAPI/steam_appid.txt b/src/SMAPI/steam_appid.txt index 9fe92b96..9fe92b96 100644 --- a/src/StardewModdingAPI/steam_appid.txt +++ b/src/SMAPI/steam_appid.txt diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh index 70f1873a..70f1873a 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/SMAPI/unix-launcher.sh diff --git a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs deleted file mode 100644 index 2f16b23d..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.AssemblyRewriters -{ - /// <summary>Rewrites CIL instructions for compatibility.</summary> - public interface IInstructionRewriter - { - /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the rewriter matches.</summary> - string NounPhrase { get; } - - - /********* - ** Methods - *********/ - /// <summary>Rewrite a method definition for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="method">The method definition to rewrite.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); - - /// <summary>Rewrite a CIL instruction for compatibility.</summary> - /// <param name="module">The module being rewritten.</param> - /// <param name="cil">The CIL rewriter.</param> - /// <param name="instruction">The instruction to rewrite.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - /// <returns>Returns whether the instruction was rewritten.</returns> - /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception> - bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); - } -} diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs deleted file mode 100644 index 25fbfef9..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 code for converting mods between Linux/Mac and Windows. This assembly is used by SMAPI internally and shouldn't be referenced directly by mods.")] -[assembly: AssemblyProduct("StardewModdingAPI.CrossplatformRewriters")] -[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")] diff --git a/src/StardewModdingAPI.AssemblyRewriters/packages.config b/src/StardewModdingAPI.AssemblyRewriters/packages.config deleted file mode 100644 index 88fbc79d..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/packages.config +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> - <package id="Mono.Cecil" version="0.9.6.4" targetFramework="net452" /> -</packages>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/StardewModdingAPI.Tests/packages.config deleted file mode 100644 index 6f04e625..00000000 --- a/src/StardewModdingAPI.Tests/packages.config +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> - <package id="Castle.Core" version="4.1.1" targetFramework="net45" /> - <package id="Moq" version="4.7.99" targetFramework="net45" /> - <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net45" /> - <package id="NUnit" version="3.7.1" targetFramework="net45" /> -</packages>
\ No newline at end of file diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs deleted file mode 100644 index 76c85287..00000000 --- a/src/StardewModdingAPI/Command.cs +++ /dev/null @@ -1,159 +0,0 @@ -#if SMAPI_1_x -using System; -using System.Collections.Generic; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI -{ - /// <summary>A command that can be submitted through the SMAPI console to interact with SMAPI.</summary> - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] - public class Command - { - /********* - ** Properties - *********/ - /// <summary>The commands registered with SMAPI.</summary> - private static readonly IDictionary<string, Command> LegacyCommands = new Dictionary<string, Command>(StringComparer.InvariantCultureIgnoreCase); - - /// <summary>Manages console commands.</summary> - private static CommandManager CommandManager; - - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - /// <summary>Tracks the installed mods.</summary> - private static ModRegistry ModRegistry; - - - /********* - ** Accessors - *********/ - /// <summary>The event raised when this command is submitted through the console.</summary> - public event EventHandler<EventArgsCommand> CommandFired; - - /**** - ** Command - ****/ - /// <summary>The name of the command.</summary> - public string CommandName; - - /// <summary>A human-readable description of what the command does.</summary> - public string CommandDesc; - - /// <summary>A human-readable list of accepted arguments.</summary> - public string[] CommandArgs; - - /// <summary>The actual submitted argument values.</summary> - public string[] CalledArgs; - - - /********* - ** Public methods - *********/ - /**** - ** Command - ****/ - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="commandManager">Manages console commands.</param> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - /// <param name="modRegistry">Tracks the installed mods.</param> - internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry) - { - Command.CommandManager = commandManager; - Command.DeprecationManager = deprecationManager; - Command.ModRegistry = modRegistry; - } - - /// <summary>Construct an instance.</summary> - /// <param name="name">The name of the command.</param> - /// <param name="description">A human-readable description of what the command does.</param> - /// <param name="args">A human-readable list of accepted arguments.</param> - public Command(string name, string description, string[] args = null) - { - this.CommandName = name; - this.CommandDesc = description; - if (args == null) - args = new string[0]; - this.CommandArgs = args; - } - - /// <summary>Trigger this command.</summary> - public void Fire() - { - if (this.CommandFired == null) - throw new InvalidOperationException($"Can't run command '{this.CommandName}' because it has no registered handler."); - this.CommandFired.Invoke(this, new EventArgsCommand(this)); - } - - - /**** - ** SMAPI - ****/ - /// <summary>Parse a command string and invoke it if valid.</summary> - /// <param name="input">The command to run, including the command name and any arguments.</param> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - public static void CallCommand(string input, IMonitor monitor) - { - Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.PendingRemoval); - Command.CommandManager.Trigger(input); - } - - /// <summary>Register a command with SMAPI.</summary> - /// <param name="name">The name of the command.</param> - /// <param name="description">A human-readable description of what the command does.</param> - /// <param name="args">A human-readable list of accepted arguments.</param> - public static Command RegisterCommand(string name, string description, string[] args = null) - { - name = name?.Trim().ToLower(); - - // raise deprecation warning - Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.PendingRemoval); - - // validate - if (Command.LegacyCommands.ContainsKey(name)) - throw new InvalidOperationException($"The '{name}' command is already registered!"); - - // add command - string modName = Command.ModRegistry.GetModFromStack() ?? "<unknown mod>"; - string documentation = args?.Length > 0 - ? $"{description} - {string.Join(", ", args)}" - : description; - Command.CommandManager.Add(modName, name, documentation, Command.Fire); - - // add legacy command - Command command = new Command(name, description, args); - Command.LegacyCommands.Add(name, command); - return command; - } - - /// <summary>Find a command with the given name.</summary> - /// <param name="name">The command name to find.</param> - public static Command FindCommand(string name) - { - Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.PendingRemoval); - if (name == null) - return null; - - Command command; - Command.LegacyCommands.TryGetValue(name.Trim(), out command); - return command; - } - - - /********* - ** Private methods - *********/ - /// <summary>Trigger this command.</summary> - /// <param name="name">The command name.</param> - /// <param name="args">The command arguments.</param> - private static void Fire(string name, string[] args) - { - Command command; - if (!Command.LegacyCommands.TryGetValue(name, out command)) - throw new InvalidOperationException($"Can't run command '{name}' because there's no such legacy command."); - command.Fire(); - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs deleted file mode 100644 index 734c04e8..00000000 --- a/src/StardewModdingAPI/Config.cs +++ /dev/null @@ -1,188 +0,0 @@ -#if SMAPI_1_x -using System; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI -{ - /// <summary>A dynamic configuration class for a mod.</summary> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public abstract class Config - { - /********* - ** Properties - *********/ - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - - /********* - ** Accessors - *********/ - /// <summary>The full path to the configuration file.</summary> - [JsonIgnore] - public virtual string ConfigLocation { get; protected internal set; } - - /// <summary>The directory path containing the configuration file.</summary> - [JsonIgnore] - public virtual string ConfigDir => Path.GetDirectoryName(this.ConfigLocation); - - - /********* - ** Public methods - *********/ - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - internal static void Shim(DeprecationManager deprecationManager) - { - Config.DeprecationManager = deprecationManager; - } - - /// <summary>Construct an instance of the config class.</summary> - /// <typeparam name="T">The config class type.</typeparam> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual Config Instance<T>() where T : Config => Activator.CreateInstance<T>(); - - /// <summary>Load the config from the JSON file, saving it to disk if needed.</summary> - /// <typeparam name="T">The config class type.</typeparam> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T LoadConfig<T>() where T : Config - { - // validate - if (string.IsNullOrEmpty(this.ConfigLocation)) - { - Log.Error("A config tried to load without specifying a location on the disk."); - return null; - } - - // read or generate config - T returnValue; - if (!File.Exists(this.ConfigLocation)) - { - T config = this.GenerateDefaultConfig<T>(); - config.ConfigLocation = this.ConfigLocation; - returnValue = config; - } - else - { - try - { - T config = JsonConvert.DeserializeObject<T>(File.ReadAllText(this.ConfigLocation)); - config.ConfigLocation = this.ConfigLocation; - returnValue = config.UpdateConfig<T>(); - } - catch (Exception ex) - { - Log.Error($"Invalid JSON ({this.GetType().Name}): {this.ConfigLocation} \n{ex}"); - return this.GenerateDefaultConfig<T>(); - } - } - - returnValue.WriteConfig(); - return returnValue; - } - - /// <summary>Get the default config values.</summary> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T GenerateDefaultConfig<T>() where T : Config - { - return null; - } - - /// <summary>Get the current configuration with missing values defaulted.</summary> - /// <typeparam name="T">The config class type.</typeparam> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T UpdateConfig<T>() where T : Config - { - try - { - // get default + user config - JObject defaultConfig = JObject.FromObject(this.Instance<T>().GenerateDefaultConfig<T>()); - JObject currentConfig = JObject.FromObject(this); - defaultConfig.Merge(currentConfig, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); - - // cast json object to config - T config = defaultConfig.ToObject<T>(); - - // update location - config.ConfigLocation = this.ConfigLocation; - - return config; - } - catch (Exception ex) - { - Log.Error($"An error occured when updating a config: {ex}"); - return this as T; - } - } - - - /********* - ** Protected methods - *********/ - /// <summary>Construct an instance.</summary> - protected Config() - { - Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.PendingRemoval); - Config.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings - } - } - - /// <summary>Provides extension methods for <see cref="Config"/> classes.</summary> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static class ConfigExtensions - { - /// <summary>Initialise the configuration. That includes loading, saving, and merging the config file and in memory at a default state. This method should not be used to reload or to resave a config. NOTE: You MUST set your config EQUAL to the return of this method!</summary> - /// <typeparam name="T">The config class type.</typeparam> - /// <param name="baseConfig">The base configuration to initialise.</param> - /// <param name="configLocation">The base configuration file path.</param> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static T InitializeConfig<T>(this T baseConfig, string configLocation) where T : Config - { - if (baseConfig == null) - baseConfig = Activator.CreateInstance<T>(); - - if (string.IsNullOrEmpty(configLocation)) - { - Log.Error("A config tried to initialize without specifying a location on the disk."); - return null; - } - - baseConfig.ConfigLocation = configLocation; - return baseConfig.LoadConfig<T>(); - } - - /// <summary>Writes the configuration to the JSON file.</summary> - /// <typeparam name="T">The config class type.</typeparam> - /// <param name="baseConfig">The base configuration to initialise.</param> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static void WriteConfig<T>(this T baseConfig) where T : Config - { - if (string.IsNullOrEmpty(baseConfig?.ConfigLocation) || string.IsNullOrEmpty(baseConfig.ConfigDir)) - { - Log.Error("A config attempted to save when it itself or it's location were null."); - return; - } - - string json = JsonConvert.SerializeObject(baseConfig, Formatting.Indented); - if (!Directory.Exists(baseConfig.ConfigDir)) - Directory.CreateDirectory(baseConfig.ConfigDir); - - if (!File.Exists(baseConfig.ConfigLocation) || !File.ReadAllText(baseConfig.ConfigLocation).SequenceEqual(json)) - File.WriteAllText(baseConfig.ConfigLocation, json); - } - - /// <summary>Rereads the JSON file and merges its values with a default config. NOTE: You MUST set your config EQUAL to the return of this method!</summary> - /// <typeparam name="T">The config class type.</typeparam> - /// <param name="baseConfig">The base configuration to initialise.</param> - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static T ReloadConfig<T>(this T baseConfig) where T : Config - { - return baseConfig.LoadConfig<T>(); - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsCommand.cs b/src/StardewModdingAPI/Events/EventArgsCommand.cs deleted file mode 100644 index 35370139..00000000 --- a/src/StardewModdingAPI/Events/EventArgsCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// <summary>Event arguments for a <see cref="StardewModdingAPI.Command.CommandFired"/> event.</summary> - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] - public class EventArgsCommand : EventArgs - { - /********* - ** Accessors - *********/ - /// <summary>The triggered command.</summary> - public Command Command { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="command">The triggered command.</param> - public EventArgsCommand(Command command) - { - this.Command = command; - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs deleted file mode 100644 index 4c359939..00000000 --- a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs +++ /dev/null @@ -1,33 +0,0 @@ -#if SMAPI_1_x -using System; -using SFarmer = StardewValley.Farmer; - -namespace StardewModdingAPI.Events -{ - /// <summary>Event arguments for a <see cref="PlayerEvents.FarmerChanged"/> event.</summary> - public class EventArgsFarmerChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// <summary>The previous player character.</summary> - public SFarmer NewFarmer { get; } - - /// <summary>The new player character.</summary> - public SFarmer PriorFarmer { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="priorFarmer">The previous player character.</param> - /// <param name="newFarmer">The new player character.</param> - public EventArgsFarmerChanged(SFarmer priorFarmer, SFarmer newFarmer) - { - this.PriorFarmer = priorFarmer; - this.NewFarmer = newFarmer; - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs b/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs deleted file mode 100644 index 688b4b3d..00000000 --- a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs +++ /dev/null @@ -1,27 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// <summary>Event arguments for a <see cref="PlayerEvents.LoadedGame"/> event.</summary> - public class EventArgsLoadedGameChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// <summary>Whether the save has been loaded. This is always true.</summary> - public bool LoadedGame { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="loaded">Whether the save has been loaded. This is always true.</param> - public EventArgsLoadedGameChanged(bool loaded) - { - this.LoadedGame = loaded; - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsNewDay.cs b/src/StardewModdingAPI/Events/EventArgsNewDay.cs deleted file mode 100644 index b8cbe9e3..00000000 --- a/src/StardewModdingAPI/Events/EventArgsNewDay.cs +++ /dev/null @@ -1,37 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// <summary>Event arguments for a <see cref="TimeEvents.OnNewDay"/> event.</summary> - public class EventArgsNewDay : EventArgs - { - /********* - ** Accessors - *********/ - /// <summary>The previous day value.</summary> - public int PreviousDay { get; } - - /// <summary>The current day value.</summary> - public int CurrentDay { get; } - - /// <summary>Whether the game just started the transition (<c>true</c>) or finished it (<c>false</c>).</summary> - public bool IsNewDay { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="priorDay">The previous day value.</param> - /// <param name="newDay">The current day value.</param> - /// <param name="isTransitioning">Whether the game just started the transition (<c>true</c>) or finished it (<c>false</c>).</param> - public EventArgsNewDay(int priorDay, int newDay, bool isTransitioning) - { - this.PreviousDay = priorDay; - this.CurrentDay = newDay; - this.IsNewDay = isTransitioning; - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs b/src/StardewModdingAPI/Events/EventArgsStringChanged.cs deleted file mode 100644 index f580a2ce..00000000 --- a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// <summary>Event arguments for a string field that changed value.</summary> - public class EventArgsStringChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// <summary>The previous value.</summary> - public string NewString { get; } - - /// <summary>The current value.</summary> - public string PriorString { get; } - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="priorString">The previous value.</param> - /// <param name="newString">The current value.</param> - public EventArgsStringChanged(string priorString, string newString) - { - this.NewString = newString; - this.PriorString = priorString; - } - } -} -#endif
\ 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 5610e67a..00000000 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using StardewModdingAPI.Framework; - -#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. -namespace StardewModdingAPI.Events -{ - /// <summary>Events raised when the game changes state.</summary> - public static class GameEvents - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - /// <summary>The backing field for <see cref="Initialize"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _Initialize; - - /// <summary>The backing field for <see cref="LoadContent"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _LoadContent; - - /// <summary>The backing field for <see cref="GameLoaded"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _GameLoaded; - - /// <summary>The backing field for <see cref="FirstUpdateTick"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _FirstUpdateTick; -#endif - - - /********* - ** Events - *********/ - /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> - internal static event EventHandler InitializeInternal; - - /// <summary>Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point.</summary> - internal static event EventHandler GameLoadedInternal; - -#if SMAPI_1_x - /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler Initialize - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", "1.10", DeprecationLevel.PendingRemoval); - GameEvents._Initialize += value; - } - remove => GameEvents._Initialize -= value; - } - - /// <summary>Raised before XNA loads or reloads graphics resources. Called during <see cref="Microsoft.Xna.Framework.Game.LoadContent"/>.</summary> - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.LoadContent) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler LoadContent - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", "1.10", DeprecationLevel.PendingRemoval); - GameEvents._LoadContent += value; - } - remove => GameEvents._LoadContent -= value; - } - - /// <summary>Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point.</summary> - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler GameLoaded - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", "1.12", DeprecationLevel.PendingRemoval); - GameEvents._GameLoaded += value; - } - remove => GameEvents._GameLoaded -= value; - } - - /// <summary>Raised during the first game update tick.</summary> - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler FirstUpdateTick - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", "1.12", DeprecationLevel.PendingRemoval); - GameEvents._FirstUpdateTick += value; - } - remove => GameEvents._FirstUpdateTick -= value; - } -#endif - - /// <summary>Raised when the game updates its state (≈60 times per second).</summary> - public static event EventHandler UpdateTick; - - /// <summary>Raised every other tick (≈30 times per second).</summary> - public static event EventHandler SecondUpdateTick; - - /// <summary>Raised every fourth tick (≈15 times per second).</summary> - public static event EventHandler FourthUpdateTick; - - /// <summary>Raised every eighth tick (≈8 times per second).</summary> - public static event EventHandler EighthUpdateTick; - - /// <summary>Raised every 15th tick (≈4 times per second).</summary> - public static event EventHandler QuarterSecondTick; - - /// <summary>Raised every 30th tick (≈twice per second).</summary> - public static event EventHandler HalfSecondTick; - - /// <summary>Raised every 60th tick (≈once per second).</summary> - public static event EventHandler OneSecondTick; - - - /********* - ** Internal methods - *********/ -#if SMAPI_1_x - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - internal static void Shim(DeprecationManager deprecationManager) - { - GameEvents.DeprecationManager = deprecationManager; - } -#endif - - /// <summary>Raise an <see cref="InitializeInternal"/> event.</summary> - /// <param name="monitor">Encapsulates logging and monitoring.</param> - internal static void InvokeInitialize(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); -#if SMAPI_1_x - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", GameEvents._Initialize?.GetInvocationList()); -#endif - } - -#if SMAPI_1_x - /// <summary>Raise a <see cref="LoadContent"/> event.</summary> - /// <param name="monitor">Encapsulates logging and monitoring.</param> - internal static void InvokeLoadContent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); - } -#endif - - /// <summary>Raise a <see cref="GameLoadedInternal"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeGameLoaded(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList()); -#if SMAPI_1_x - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); -#endif - } - -#if SMAPI_1_x - /// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeFirstUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents._FirstUpdateTick?.GetInvocationList()); - } -#endif - - /// <summary>Raise an <see cref="UpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates logging and monitoring.</param> - internal static void InvokeUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); - } - - /// <summary>Raise a <see cref="SecondUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeSecondUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); - } - - /// <summary>Raise a <see cref="FourthUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeFourthUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); - } - - /// <summary>Raise a <see cref="EighthUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeEighthUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); - } - - /// <summary>Raise a <see cref="QuarterSecondTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeQuarterSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); - } - - /// <summary>Raise a <see cref="HalfSecondTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeHalfSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); - } - - /// <summary>Raise a <see cref="OneSecondTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOneSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); - } - } -} diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs deleted file mode 100644 index 72826330..00000000 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using StardewModdingAPI.Framework; -using StardewValley; -using SFarmer = StardewValley.Farmer; - -#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. -namespace StardewModdingAPI.Events -{ - /// <summary>Events raised when the player data changes.</summary> - public static class PlayerEvents - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - /// <summary>The backing field for <see cref="LoadedGame"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler<EventArgsLoadedGameChanged> _LoadedGame; - - /// <summary>The backing field for <see cref="FarmerChanged"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler<EventArgsFarmerChanged> _FarmerChanged; -#endif - - - /********* - ** Events - *********/ -#if SMAPI_1_x - /// <summary>Raised after the player loads a saved game.</summary> - [Obsolete("Use " + nameof(SaveEvents) + "." + nameof(SaveEvents.AfterLoad) + " instead")] - public static event EventHandler<EventArgsLoadedGameChanged> LoadedGame - { - add - { - PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", "1.6", DeprecationLevel.PendingRemoval); - PlayerEvents._LoadedGame += value; - } - remove => PlayerEvents._LoadedGame -= value; - } - - /// <summary>Raised after the game assigns a new player character. This happens just before <see cref="LoadedGame"/>; it's unclear how this would happen any other time.</summary> - [Obsolete("should no longer be used")] - public static event EventHandler<EventArgsFarmerChanged> FarmerChanged - { - add - { - PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", "1.6", DeprecationLevel.PendingRemoval); - PlayerEvents._FarmerChanged += value; - } - remove => PlayerEvents._FarmerChanged -= value; - } -#endif - - /// <summary>Raised after the player's inventory changes in any way (added or removed item, sorted, etc).</summary> - public static event EventHandler<EventArgsInventoryChanged> InventoryChanged; - - /// <summary> 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.</summary> - public static event EventHandler<EventArgsLevelUp> LeveledUp; - - - /********* - ** Internal methods - *********/ -#if SMAPI_1_x - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - internal static void Shim(DeprecationManager deprecationManager) - { - PlayerEvents.DeprecationManager = deprecationManager; - } - - /// <summary>Raise a <see cref="LoadedGame"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="loaded">Whether the save has been loaded. This is always true.</param> - internal static void InvokeLoadedGame(IMonitor monitor, EventArgsLoadedGameChanged loaded) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", PlayerEvents._LoadedGame?.GetInvocationList(), null, loaded); - } - - /// <summary>Raise a <see cref="FarmerChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorFarmer">The previous player character.</param> - /// <param name="newFarmer">The new player character.</param> - internal static void InvokeFarmerChanged(IMonitor monitor, SFarmer priorFarmer, SFarmer newFarmer) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", PlayerEvents._FarmerChanged?.GetInvocationList(), null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); - } -#endif - - /// <summary>Raise an <see cref="InventoryChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="inventory">The player's inventory.</param> - /// <param name="changedItems">The inventory changes.</param> - internal static void InvokeInventoryChanged(IMonitor monitor, List<Item> inventory, IEnumerable<ItemStackChange> changedItems) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); - } - - /// <summary>Rase a <see cref="LeveledUp"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="type">The player skill that leveled up.</param> - /// <param name="newLevel">The new skill level.</param> - 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/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs deleted file mode 100644 index d5ab9fb7..00000000 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using StardewModdingAPI.Framework; - -#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. -namespace StardewModdingAPI.Events -{ - /// <summary>Events raised when the in-game date or time changes.</summary> - public static class TimeEvents - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - /// <summary>The backing field for <see cref="OnNewDay"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler<EventArgsNewDay> _OnNewDay; - - /// <summary>The backing field for <see cref="DayOfMonthChanged"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler<EventArgsIntChanged> _DayOfMonthChanged; - - /// <summary>The backing field for <see cref="SeasonOfYearChanged"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler<EventArgsStringChanged> _SeasonOfYearChanged; - - /// <summary>The backing field for <see cref="YearOfGameChanged"/>.</summary> - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler<EventArgsIntChanged> _YearOfGameChanged; -#endif - - - /********* - ** Events - *********/ - /// <summary>Raised after the game begins a new day, including when loading a save.</summary> - public static event EventHandler AfterDayStarted; - - /// <summary>Raised after the in-game clock changes.</summary> - public static event EventHandler<EventArgsIntChanged> TimeOfDayChanged; - -#if SMAPI_1_x - /// <summary>Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use <see cref="AfterDayStarted"/> instead.</summary> - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler<EventArgsIntChanged> DayOfMonthChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._DayOfMonthChanged += value; - } - remove => TimeEvents._DayOfMonthChanged -= value; - } - - /// <summary>Raised after the year value changes.</summary> - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler<EventArgsIntChanged> YearOfGameChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._YearOfGameChanged += value; - } - remove => TimeEvents._YearOfGameChanged -= value; - } - - /// <summary>Raised after the season value changes.</summary> - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler<EventArgsStringChanged> SeasonOfYearChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._SeasonOfYearChanged += value; - } - remove => TimeEvents._SeasonOfYearChanged -= value; - } - - /// <summary>Raised when the player is transitioning to a new day and the game is performing its day update logic. This event is triggered twice: once after the game starts transitioning, and again after it finishes.</summary> - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler<EventArgsNewDay> OnNewDay - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", "1.6", DeprecationLevel.PendingRemoval); - TimeEvents._OnNewDay += value; - } - remove => TimeEvents._OnNewDay -= value; - } -#endif - - - /********* - ** Internal methods - *********/ -#if SMAPI_1_x - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - internal static void Shim(DeprecationManager deprecationManager) - { - TimeEvents.DeprecationManager = deprecationManager; - } -#endif - - /// <summary>Raise an <see cref="AfterDayStarted"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeAfterDayStarted(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); - } - - /// <summary>Raise a <see cref="TimeOfDayChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorTime">The previous time in military time format (e.g. 6:00pm is 1800).</param> - /// <param name="newTime">The current time in military time format (e.g. 6:10pm is 1810).</param> - 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)); - } - -#if SMAPI_1_x - /// <summary>Raise a <see cref="DayOfMonthChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorDay">The previous day value.</param> - /// <param name="newDay">The current day value.</param> - internal static void InvokeDayOfMonthChanged(IMonitor monitor, int priorDay, int newDay) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", TimeEvents._DayOfMonthChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorDay, newDay)); - } - - /// <summary>Raise a <see cref="YearOfGameChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorYear">The previous year value.</param> - /// <param name="newYear">The current year value.</param> - internal static void InvokeYearOfGameChanged(IMonitor monitor, int priorYear, int newYear) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", TimeEvents._YearOfGameChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorYear, newYear)); - } - - /// <summary>Raise a <see cref="SeasonOfYearChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorSeason">The previous season name.</param> - /// <param name="newSeason">The current season name.</param> - internal static void InvokeSeasonOfYearChanged(IMonitor monitor, string priorSeason, string newSeason) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", TimeEvents._SeasonOfYearChanged?.GetInvocationList(), null, new EventArgsStringChanged(priorSeason, newSeason)); - } - - /// <summary>Raise a <see cref="OnNewDay"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorDay">The previous day value.</param> - /// <param name="newDay">The current day value.</param> - /// <param name="isTransitioning">Whether the game just started the transition (<c>true</c>) or finished it (<c>false</c>).</param> - internal static void InvokeOnNewDay(IMonitor monitor, int priorDay, int newDay, bool isTransitioning) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", TimeEvents._OnNewDay?.GetInvocationList(), null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); - } -#endif - } -} diff --git a/src/StardewModdingAPI/Framework/Models/DisabledMod.cs b/src/StardewModdingAPI/Framework/Models/DisabledMod.cs deleted file mode 100644 index 170fa760..00000000 --- a/src/StardewModdingAPI/Framework/Models/DisabledMod.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Metadata about for a mod that should never be loaded.</summary> - internal class DisabledMod - { - /********* - ** Accessors - *********/ - /**** - ** From config - ****/ - /// <summary>The unique mod IDs.</summary> - public string[] ID { get; set; } - - /// <summary>The mod name.</summary> - public string Name { get; set; } - - /// <summary>The reason phrase to show in the warning, or <c>null</c> to use the default value.</summary> - /// <example>"this mod is no longer supported or used"</example> - public string ReasonPhrase { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/GitRelease.cs b/src/StardewModdingAPI/Framework/Models/GitRelease.cs deleted file mode 100644 index bc53468f..00000000 --- a/src/StardewModdingAPI/Framework/Models/GitRelease.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Metadata about a GitHub release tag.</summary> - internal class GitRelease - { - /********* - ** Accessors - *********/ - /// <summary>The display name.</summary> - [JsonProperty("name")] - public string Name { get; set; } - - /// <summary>The semantic version string.</summary> - [JsonProperty("tag_name")] - public string Tag { get; set; } - } -}
\ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs deleted file mode 100644 index d3a9c533..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary> - internal class ModCompatibility - { - /********* - ** Accessors - *********/ - /// <summary>The unique mod IDs.</summary> - [JsonConverter(typeof(SFieldConverter))] - public ModCompatibilityID[] ID { get; set; } - - /// <summary>The mod name.</summary> - public string Name { get; set; } - - /// <summary>The oldest incompatible mod version, or <c>null</c> for all past versions.</summary> - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion LowerVersion { get; set; } - - /// <summary>The most recent incompatible mod version.</summary> - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion UpperVersion { get; set; } - - /// <summary>A label to show to the user instead of <see cref="UpperVersion"/>, when the manifest version differs from the user-facing version.</summary> - public string UpperVersionLabel { get; set; } - - /// <summary>The URLs the user can check for a newer version.</summary> - public string[] UpdateUrls { get; set; } - - /// <summary>The reason phrase to show in the warning, or <c>null</c> to use the default value.</summary> - /// <example>"this version is incompatible with the latest version of the game"</example> - public string ReasonPhrase { get; set; } - - /// <summary>Indicates how SMAPI should consider the mod.</summary> - public ModCompatibilityType Compatibility { get; set; } = ModCompatibilityType.AssumeBroken; - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs deleted file mode 100644 index 98e70116..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Uniquely identifies a mod for compatibility checks.</summary> - internal class ModCompatibilityID - { - /********* - ** Accessors - *********/ - /// <summary>The unique mod ID.</summary> - public string ID { get; set; } - - /// <summary>The mod name to disambiguate non-unique IDs, or <c>null</c> to ignore the mod name.</summary> - public string Name { get; set; } - - /// <summary>The author name to disambiguate non-unique IDs, or <c>null</c> to ignore the author.</summary> - public string Author { get; set; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public ModCompatibilityID() { } - - /// <summary>Construct an instance.</summary> - /// <param name="data">The mod ID or a JSON string matching the <see cref="ModCompatibilityID"/> fields.</param> - public ModCompatibilityID(string data) - { - // JSON can be stuffed into the ID string as a convenience hack to keep JSON mod lists - // formatted readably. The tradeoff is that the format is a bit more magical, but that's - // probably acceptable since players aren't meant to edit it. It's also fairly clear what - // the JSON strings do, if not necessarily how. - if (data.StartsWith("{")) - JsonConvert.PopulateObject(data, this); - else - this.ID = data; - } - - /// <summary>Get whether this ID matches a given mod manifest.</summary> - /// <param name="id">The mod's unique ID, or a substitute ID if it isn't set in the manifest.</param> - /// <param name="manifest">The manifest to check.</param> - public bool Matches(string id, IManifest manifest) - { - return - this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) - && ( - this.Author == null - || this.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && this.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (this.Name == null || this.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs deleted file mode 100644 index 35edec5e..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Indicates how SMAPI should consider a mod.</summary> - internal enum ModCompatibilityType - { - /// <summary>Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code.</summary> - AssumeBroken = 0, - - /// <summary>Assume the mod is compatible, even if SMAPI detects incompatible code.</summary> - AssumeCompatible = 1 - } -} diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs deleted file mode 100644 index b2ca4113..00000000 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>The SMAPI configuration settings.</summary> - internal class SConfig - { - /******** - ** Accessors - ********/ - /// <summary>Whether to enable development features.</summary> - public bool DeveloperMode { get; set; } - - /// <summary>Whether to check if a newer version of SMAPI is available on startup.</summary> - public bool CheckForUpdates { get; set; } = true; - - /// <summary>Whether SMAPI should log more information about the game context.</summary> - public bool VerboseLogging { get; set; } = false; - - /// <summary>A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code.</summary> - public ModCompatibility[] ModCompatibility { get; set; } - - /// <summary>A list of mods which should be considered obsolete and not loaded.</summary> - public DisabledMod[] DisabledMods { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/UpdateHelper.cs deleted file mode 100644 index e01e55c8..00000000 --- a/src/StardewModdingAPI/Framework/UpdateHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO; -using System.Net; -using System.Reflection; -using System.Threading.Tasks; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework -{ - /// <summary>Provides utility methods for mod updates.</summary> - internal class UpdateHelper - { - /********* - ** Public methods - *********/ - /// <summary>Get the latest release from a GitHub repository.</summary> - /// <param name="repository">The name of the repository from which to fetch releases (like "cjsu/SMAPI").</param> - public static async Task<GitRelease> GetLatestVersionAsync(string repository) - { - // build request - // (avoid HttpClient for Mac compatibility) - HttpWebRequest request = WebRequest.CreateHttp($"https://api.github.com/repos/{repository}/releases/latest"); - AssemblyName assembly = typeof(UpdateHelper).Assembly.GetName(); - request.UserAgent = $"{assembly.Name}/{assembly.Version}"; - request.Accept = "application/vnd.github.v3+json"; - - // fetch data - using (WebResponse response = await request.GetResponseAsync()) - using (Stream responseStream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(responseStream)) - { - string responseText = reader.ReadToEnd(); - return JsonConvert.DeserializeObject<GitRelease>(responseText); - } - } - } -} diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs deleted file mode 100644 index 60220ad8..00000000 --- a/src/StardewModdingAPI/Log.cs +++ /dev/null @@ -1,320 +0,0 @@ -#if SMAPI_1_x -using System; -using System.Threading; -using StardewModdingAPI.Framework; -using Monitor = StardewModdingAPI.Framework.Monitor; - -namespace StardewModdingAPI -{ - /// <summary>A singleton which logs messages to the SMAPI console and log file.</summary> - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Monitor))] - public static class Log - { - /********* - ** Properties - *********/ - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - /// <summary>The underlying logger.</summary> - private static Monitor Monitor; - - /// <summary>Tracks the installed mods.</summary> - private static ModRegistry ModRegistry; - - - /********* - ** Public methods - *********/ - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - /// <param name="monitor">The underlying logger.</param> - /// <param name="modRegistry">Tracks the installed mods.</param> - internal static void Shim(DeprecationManager deprecationManager, Monitor monitor, ModRegistry modRegistry) - { - Log.DeprecationManager = deprecationManager; - Log.Monitor = monitor; - Log.ModRegistry = modRegistry; - } - - /**** - ** Exceptions - ****/ - /// <summary>Log an exception event.</summary> - /// <param name="sender">The event sender.</param> - /// <param name="e">The event arguments.</param> - public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - Log.WarnDeprecated(); - Log.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - } - - /// <summary>Log a thread exception event.</summary> - /// <param name="sender">The event sender.</param> - /// <param name="e">The event arguments.</param> - public static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) - { - Log.WarnDeprecated(); - Log.Monitor.Log($"Critical thread exception: {e.Exception}", LogLevel.Error); - } - - /**** - ** Synchronous logging - ****/ - /// <summary>Synchronously log a message to the console. NOTE: synchronous logging is discouraged; use asynchronous methods instead.</summary> - /// <param name="message">The message to log.</param> - /// <param name="color">The message color.</param> - public static void SyncColour(object message, ConsoleColor color) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), color); - } - - /**** - ** Asynchronous logging - ****/ - /// <summary>Asynchronously log a message to the console with the specified color.</summary> - /// <param name="message">The message to log.</param> - /// <param name="color">The message color.</param> - public static void AsyncColour(object message, ConsoleColor color) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), color); - } - - /// <summary>Asynchronously log a message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Async(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Gray); - } - - /// <summary>Asynchronously log a red message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void AsyncR(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Red); - } - - /// <summary>Asynchronously log an orange message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void AsyncO(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.DarkYellow); - } - - /// <summary>Asynchronously log a yellow message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void AsyncY(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Yellow); - } - - /// <summary>Asynchronously log a green message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void AsyncG(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Green); - } - - /// <summary>Asynchronously log a cyan message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void AsyncC(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Cyan); - } - - /// <summary>Asynchronously log a magenta message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void AsyncM(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Magenta); - } - - /// <summary>Asynchronously log a warning to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Warning(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Yellow, LogLevel.Warn); - } - - /// <summary>Asynchronously log an error to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Error(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Red, LogLevel.Error); - } - - /// <summary>Asynchronously log a success message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Success(object message) - { - Log.WarnDeprecated(); - Log.AsyncG(message); - } - - /// <summary>Asynchronously log an info message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Info(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.White, LogLevel.Info); - } - - /// <summary>Asynchronously log an info message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Out(object message) - { - Log.WarnDeprecated(); - Log.Async($"[OUT] {message}"); - } - - /// <summary>Asynchronously log a debug message to the console.</summary> - /// <param name="message">The message to log.</param> - public static void Debug(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.DarkGray); - } - - /// <summary>Asynchronously log a message to the file that's not shown in the console.</summary> - /// <param name="message">The message to log.</param> - internal static void LogToFile(string message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message, ConsoleColor.DarkGray, LogLevel.Trace); - } - - /// <summary>Obsolete.</summary> - public static void LogValueNotSpecified() - { - Log.WarnDeprecated(); - Log.AsyncR("<value> must be specified"); - } - - /// <summary>Obsolete.</summary> - public static void LogObjectValueNotSpecified() - { - Log.WarnDeprecated(); - Log.AsyncR("<object> and <value> must be specified"); - } - - /// <summary>Obsolete.</summary> - public static void LogValueInvalid() - { - Log.WarnDeprecated(); - Log.AsyncR("<value> is invalid"); - } - - /// <summary>Obsolete.</summary> - public static void LogObjectInvalid() - { - Log.WarnDeprecated(); - Log.AsyncR("<object> is invalid"); - } - - /// <summary>Obsolete.</summary> - public static void LogValueNotInt32() - { - Log.WarnDeprecated(); - Log.AsyncR("<value> must be a whole number (Int32)"); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="disableLogging">Obsolete.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - private static void PrintLog(object message, bool disableLogging, params object[] values) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Gray); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Success(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Success(message); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Verbose(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Out(message); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Comment(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.AsyncC(message); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Info(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Info(message); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Error(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Error(message); - } - - /// <summary>Obsolete.</summary> - /// <param name="message">The message to log.</param> - /// <param name="values">Obsolete.</param> - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Debug(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Debug(message); - } - - - /********* - ** Private methods - *********/ - /// <summary>Raise a deprecation warning.</summary> - private static void WarnDeprecated() - { - Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.PendingRemoval); - } - - /// <summary>Get the name of the mod logging a message from the stack.</summary> - private static string GetModName() - { - return Log.ModRegistry.GetModFromStack() ?? "<unknown mod>"; - } - } -} -#endif
\ No newline at end of file diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs deleted file mode 100644 index b5607234..00000000 --- a/src/StardewModdingAPI/Mod.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.IO; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI -{ - /// <summary>The base class for a mod.</summary> - public class Mod : IMod, IDisposable - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// <summary>Manages deprecation warnings.</summary> - private static DeprecationManager DeprecationManager; - - - /// <summary>The backing field for <see cref="Mod.PathOnDisk"/>.</summary> - private string _pathOnDisk; -#endif - - - /********* - ** Accessors - *********/ - /// <summary>Provides simplified APIs for writing mods.</summary> - public IModHelper Helper { get; internal set; } - - /// <summary>Writes messages to the console and log file.</summary> - public IMonitor Monitor { get; internal set; } - - /// <summary>The mod's manifest.</summary> - public IManifest ModManifest { get; internal set; } - -#if SMAPI_1_x - /// <summary>The full path to the mod's directory on the disk.</summary> - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.DirectoryPath) + " instead")] - public string PathOnDisk - { - get - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.PendingRemoval); - return this._pathOnDisk; - } - internal set { this._pathOnDisk = value; } - } - - /// <summary>The full path to the mod's <c>config.json</c> file on the disk.</summary> - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public string BaseConfigPath - { - get - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.PendingRemoval); - Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings - return Path.Combine(this.PathOnDisk, "config.json"); - } - } - - /// <summary>The full path to the per-save configs folder (if <see cref="Manifest.PerSaveConfigs"/> is <c>true</c>).</summary> - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")] - public string PerSaveConfigFolder => this.GetPerSaveConfigFolder(); - - /// <summary>The full path to the per-save configuration file for the current save (if <see cref="Manifest.PerSaveConfigs"/> is <c>true</c>).</summary> - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")] - public string PerSaveConfigPath - { - get - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.PendingRemoval); - Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings - return Context.IsSaveLoaded ? Path.Combine(this.PerSaveConfigFolder, $"{Constants.SaveFolderName}.json") : ""; - } - } -#endif - - - /********* - ** Public methods - *********/ -#if SMAPI_1_x - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="deprecationManager">Manages deprecation warnings.</param> - internal static void Shim(DeprecationManager deprecationManager) - { - Mod.DeprecationManager = deprecationManager; - } - - /// <summary>The mod entry point, called after the mod is first loaded.</summary> - [Obsolete("This overload is obsolete since SMAPI 1.0.")] - public virtual void Entry(params object[] objects) { } -#endif - - /// <summary>The mod entry point, called after the mod is first loaded.</summary> - /// <param name="helper">Provides simplified APIs for writing mods.</param> - public virtual void Entry(IModHelper helper) { } - - /// <summary>Release or reset unmanaged resources.</summary> - 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 - *********/ -#if SMAPI_1_x - /// <summary>Get the full path to the per-save configuration file for the current save (if <see cref="Manifest.PerSaveConfigs"/> is <c>true</c>).</summary> - [Obsolete] - private string GetPerSaveConfigFolder() - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.PendingRemoval); - Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings - - if (!((Manifest)this.ModManifest).PerSaveConfigs) - { - this.Monitor.Log("Tried to fetch the per-save config folder, but this mod isn't configured to use per-save config files.", LogLevel.Error); - return ""; - } - return Path.Combine(this.PathOnDisk, "psconfigs"); - } -#endif - - /// <summary>Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit.</summary> - /// <param name="disposing">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.</param> - protected virtual void Dispose(bool disposing) { } - - /// <summary>Destruct the instance.</summary> - ~Mod() - { - this.Dispose(false); - } - } -} diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json deleted file mode 100644 index d393f5a9..00000000 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ /dev/null @@ -1,490 +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 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. - */ - "CheckForUpdates": true, - - /** - * Whether SMAPI should log more information about the game context. - */ - "VerboseLogging": false, - - /** - * A list of mods SMAPI should consider obsolete and not load. Changing this field is not - * recommended and may destabilise your game. - */ - "DisabledMods": [ - { - "Name": "Animal Mood Fix", - "ID": [ "GPeters-AnimalMoodFix" ], - "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - { - "Name": "Colored Chests", - "ID": [ "4befde5c-731c-4853-8e4b-c5cdf946805f" ], - "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - { - "Name": "Modder Serialization Utility", - "ID": [ "SerializerUtils-0-1" ], - "ReasonPhrase": "it's no longer maintained or used." - }, - { - "Name": "No Debug Mode", - "ID": [ "NoDebugMode" ], - "ReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - { - "Name": "StarDustCore", - "ID": [ "StarDustCore" ], - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - }, - { - "Name": "XmlSerializerRetool", - "ID": [ "XmlSerializerRetool.dll" ], - "ReasonPhrase": "it's no longer maintained or used." - } - ], - - /** - * A list of mod versions SMAPI should consider compatible or broken regardless of whether it - * detects incompatible code. The default for each record is to assume broken; to force SMAPI to - * load a mod regardless of compatibility checks, add a "Compatibility": "AssumeCompatible" field. - * Changing this field is not recommended and may destabilise your game. - */ - "ModCompatibility": [ - { - "Name": "AccessChestAnywhere", - "ID": [ "AccessChestAnywhere" ], - "UpperVersion": "1.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], - "Notes": "Broke in SDV 1.1." - }, - { - "Name": "AdjustArtisanPrices", - "ID": [ "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc" ], - "UpperVersion": "0.0", - "UpperVersionLabel": "0.01", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SMAPI 1.9." - }, - { - "Name": "Advanced Location Loader", - "ID": [ "Entoarox.AdvancedLocationLoader" ], - "UpperVersion": "1.2.10", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3619" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." - }, - { - "Name": "Almighty Tool", - "ID": [ "AlmightyTool.dll" ], - "UpperVersion": "1.1.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Better Sprinklers", - "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], - "UpperVersion": "2.3.1-pathoschild-update", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Birthday Mail", - "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], - "UpperVersion": "1.2.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Chest Label System", - "ID": [ "SPDChestLabel" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.1." - }, - { - "Name": "Chest Pooling", - "ID": [ "ChestPooling.dll" ], - "UpperVersion": "1.2", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Chests Anywhere", - "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ], - "UpperVersion": "1.9-beta", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "CJB Automation", - "ID": [ "CJBAutomation" ], - "UpperVersion": "1.4", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "CJB Cheats Menu", - "ID": [ "CJBCheatsMenu" ], - "UpperVersion": "1.12", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/4" ], - "Notes": "Broke in SDV 1.1." - }, - { - "Name": "CJB Item Spawner", - "ID": [ "CJBItemSpawner" ], - "UpperVersion": "1.5", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], - "Notes": "Broke in SDV 1.1." - }, - { - "Name": "CJB Show Item Sell Price", - "ID": [ "CJBShowItemSellPrice" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Cooking Skill", - "ID": [ "CookingSkill" ], - "UpperVersion": "1.0.3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Enemy Health Bars", - "ID": [ "SPDHealthBar" ], - "UpperVersion": "1.7", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Entoarox Framework", - "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], - "UpperVersion": "1.7.10", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." - }, - { - "Name": "Extended Fridge", - "ID": [ "Mystra007ExtendedFridge" ], - "UpperVersion": "1.0", - "UpperVersionLabel": "0.94", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485" ], - "Notes": "Broke in SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." - }, - { - "Name": "Farm Automation: Item Collector", - "ID": [ "FarmAutomation.ItemCollector.dll" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://community.playstarbound.com/threads/125172" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Farm Automation Unofficial: Item Collector", - "ID": [ "Maddy99.FarmAutomation.ItemCollector" ], - "UpperVersion": "0.4", - "UpdateUrls": [ "http://community.playstarbound.com/threads/125172" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Instant Geode", - "ID": [ "InstantGeode" ], - "UpperVersion": "1.12", - "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Gate Opener", - "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ], - "UpperVersion": "1.0.1", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Get Dressed", - "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], - "UpperVersion": "3.3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Gift Taste Helper", - "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ], - "UpperVersion": "2.3.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Lookup Anything", - "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], - "UpperVersion": "1.10.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Makeshift Multiplayer", - "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], - "UpperVersion": "0.3.3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "More Pets", - "ID": [ "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS", /* since 1.3 */ "Entoarox.MorePets" ], - "UpperVersion": "1.3.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4288" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." - }, - { - "Name": "NoSoilDecay", - "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], - "UpperVersion": "0.5", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." - }, - { - "Name": "NPC Map Locations", - "ID": [ "NPCMapLocationsMod" ], - "LowerVersion": "1.42", - "UpperVersion": "1.43", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], - "ReasonPhrase": "this version has an update check error which crashes the game." - }, - { - "Name": "Part of the Community", - "ID": [ "SB_PotC" ], - "UpperVersion": "1.0.8", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], - "ReasonPhrase": "Broke in SDV 1.2." - }, - { - "Name": "Persival's BundleMod", - "ID": [ "BundleMod.dll" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.1." - }, - { - "Name": "Point-and-Plant", - "ID": [ "PointAndPlant.dll" ], - "UpperVersion": "1.0.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "PrairieKingMadeEasy", - "ID": [ "PrairieKingMadeEasy.dll" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Rush Orders", - "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], - "UpperVersion": "1.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Save Anywhere", - "ID": [ "{'ID':'SaveAnywhere', 'Name':'Save Anywhere'}" ], // disambiguate from Night Owl - "UpperVersion": "2.3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Seasonal Immersion", - "ID": [ "EntoaroxSeasonalHouse", /*since 1.1.0*/ "EntoaroxSeasonalBuildings", /* since 1.6 or earlier*/ "EntoaroxSeasonalImmersion", /*since 1.7*/ "Entoarox.SeasonalImmersion" ], - "UpperVersion": "1.8.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4262" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." - }, - { - "Name": "Shop Expander", - "ID": [ /*since at least 1.4*/ "821ce8f6-e629-41ad-9fde-03b54f68b0b6", /*since 1.5*/ "EntoaroxShopExpander", /*since 1.5.2*/ "Entoarox.ShopExpander" ], - "UpperVersion": "1.5.3", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4381" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." - }, - { - "Name": "Simple Sprinklers", - "ID": [ "SimpleSprinkler.dll" ], - "UpperVersion": "1.4", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Siv's Marriage Mod", - "ID": [ "6266959802" ], - "UpperVersion": "1.2.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SMAPI 1.9 (has multiple Mod instances)." - }, - { - "Name": "Skill Prestige: Cooking Adapter", - "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], - "UpperVersion": "1.0.4", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Sprint and Dash", - "ID": [ "SPDSprintAndDash" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://community.playstarbound.com/resources/4201" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Sprint and Dash Redux", - "ID": [ "SPDSprintAndDash" ], - "UpperVersion": "1.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Sprinting Mod", - "ID": [ "a10d3097-b073-4185-98ba-76b586cba00c" ], - "UpperVersion": "2.1", - "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "StackSplitX", - "ID": [ "StackSplitX.dll" ], - "UpperVersion": "1.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Tainted Cellar", - "ID": [ "TaintedCellar.dll" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.1 or 1.11." - }, - { - "Name": "Teleporter", - "ID": [ "Teleporter" ], - "UpperVersion": "1.0.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4374" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Three-heart Dance Partner", - "ID": [ "ThreeHeartDancePartner" ], - "UpperVersion": "1.0.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "TimeSpeed", - "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'}", /* since 2.0.3 */ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'}", /*since 2.1*/ "community.TimeSpeed" ], // disambiguate other mods by Alpha_Omegasis - "UpperVersion": "2.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "UiModSuite", - "ID": [ "Demiacle.UiModSuite" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Weather Controller", - "ID": [ "WeatherController.dll" ], - "UpperVersion": "1.0.2", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Wonderful Farm Life", - "ID": [ "WonderfulFarmLife.dll" ], - "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.1 or 1.11." - }, - { - "Name": "Xnb Loader", - "ID": [ "Entoarox.XnbLoader" ], - "UpperVersion": "1.0.6", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4506" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." - }, - { - "Name": "zDailyIncrease", - "ID": [ "zdailyincrease" ], - "UpperVersion": "1.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoom Out Extreme", - "ID": [ "ZoomMod" ], - "UpperVersion": "0.1", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoryn's Better RNG", - "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoryn's Calendar Anywhere", - "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoryn's Health Bars", - "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoryn's Junimo Deposit Anywhere", - "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ], - "UpperVersion": "1.7", - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoryn's Movement Mod", - "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." - }, - { - "Name": "Zoryn's Regen Mod", - "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ], - "UpperVersion": "1.6", - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." - } - ] -} diff --git a/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs b/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs deleted file mode 100644 index ad79b4af..00000000 --- a/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if SMAPI_1_x -using StardewModdingAPI; -using StardewValley; -using StardewValley.Menus; - -namespace TrainerMod.Framework.Commands.Saves -{ - /// <summary>A command which shows the load screen.</summary> - internal class LoadCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public LoadCommand() - : base("load", "Shows the load screen.") { } - - /// <summary>Handle the command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="command">The command name.</param> - /// <param name="args">The command arguments.</param> - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - monitor.Log("Triggering load menu...", LogLevel.Info); - Game1.hasLoadedGame = false; - Game1.activeClickableMenu = new LoadGameMenu(); - } - } -} -#endif
\ No newline at end of file diff --git a/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs b/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs deleted file mode 100644 index ea2bd6a8..00000000 --- a/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -#if SMAPI_1_x -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Saves -{ - /// <summary>A command which saves the game.</summary> - internal class SaveCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public SaveCommand() - : base("save", "Saves the game? Doesn't seem to work.") { } - - - /// <summary>Handle the command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="command">The command name.</param> - /// <param name="args">The command arguments.</param> - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - monitor.Log("Saving the game...", LogLevel.Info); - SaveGame.Save(); - } - } -} -#endif
\ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 73b40050..cb5ec47e 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -36,9 +36,8 @@ <PlatformTarget>x86</PlatformTarget> </PropertyGroup> <ItemGroup> - <Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> - <Private>False</Private> + <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -48,7 +47,7 @@ <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> - <Compile Include="..\GlobalAssemblyInfo.cs"> + <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> <Compile Include="Framework\Commands\ArgumentParser.cs" /> @@ -69,8 +68,6 @@ <Compile Include="Framework\Commands\Player\SetStaminaCommand.cs" /> <Compile Include="Framework\Commands\Player\SetNameCommand.cs" /> <Compile Include="Framework\Commands\Player\SetMoneyCommand.cs" /> - <Compile Include="Framework\Commands\Saves\LoadCommand.cs" /> - <Compile Include="Framework\Commands\Saves\SaveCommand.cs" /> <Compile Include="Framework\Commands\TrainerCommand.cs" /> <Compile Include="Framework\Commands\World\SetMineLevelCommand.cs" /> <Compile Include="Framework\Commands\World\DownMineLevelCommand.cs" /> @@ -87,7 +84,7 @@ <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\StardewModdingAPI\StardewModdingAPI.csproj"> + <ProjectReference Include="..\SMAPI\StardewModdingAPI.csproj"> <Project>{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}</Project> <Name>StardewModdingAPI</Name> <Private>False</Private> @@ -100,5 +97,5 @@ <None Include="packages.config" /> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\common.targets" /> + <Import Project="..\..\build\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/TrainerMod/manifest.json b/src/TrainerMod/manifest.json index 20b40f8a..22e35bce 100644 --- a/src/TrainerMod/manifest.json +++ b/src/TrainerMod/manifest.json @@ -2,9 +2,9 @@ "Name": "Trainer Mod", "Author": "SMAPI", "Version": { - "MajorVersion": 1, - "MinorVersion": 15, - "PatchVersion": 4, + "MajorVersion": 2, + "MinorVersion": 0, + "PatchVersion": 0, "Build": null }, "Description": "Adds SMAPI console commands that let you manipulate the game.", diff --git a/src/TrainerMod/packages.config b/src/TrainerMod/packages.config index 75e68e71..ee51c237 100644 --- a/src/TrainerMod/packages.config +++ b/src/TrainerMod/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" /> </packages>
\ No newline at end of file |