diff --git a/src/.editorconfig b/.editorconfig similarity index 100% rename from src/.editorconfig rename to .editorconfig diff --git a/.gitignore b/.gitignore index 08d3fe47..f2d50778 100644 --- a/.gitignore +++ b/.gitignore @@ -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/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/src/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs similarity index 52% rename from src/GlobalAssemblyInfo.cs rename to 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 similarity index 100% rename from src/common.targets rename to build/common.targets diff --git a/src/prepare-install-package.targets b/build/prepare-install-package.targets similarity index 100% rename from src/prepare-install-package.targets rename to build/prepare-install-package.targets diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to 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 deleted file mode 100644 index 50f375b6..00000000 Binary files a/docs/imgs/SMAPI.png and /dev/null differ 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 `` in your +`.csproj`: +```xml +YourModName +``` + +If you don't want to deploy the mod automatically, you can add this: +```xml +False +``` + +### 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 +`` in your `.csproj`: +```xml +YourModName +``` + +You can change the folder path where the zip is created like this: +```xml +$(SolutionDir)\_releases +``` + +Finally, you can disable the zip creation with this: +```xml +False +``` + +Or only create it in release builds with this: +```xml +False +``` + +### 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 + + + PATH_HERE + + + ``` + + 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 ` + PATH_HERE + + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + + +## 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 similarity index 86% rename from release-notes.md rename to 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) - +# 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`. + +* **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. - _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)._ -* Added new input events. - _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._ +* 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` 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` 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 similarity index 60% rename from README.md rename to 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 | `-alpha.` | `1.0-alpha.20171230` - prerelease | `-prerelease.` | `1.0-prerelease.2` - release | `` | `1.0` + dev build | `-alpha.` | `2.0-alpha.20171230` + prerelease | `-prerelease.` | `2.0-prerelease.2` + release | `` | `2.0` 2. In Windows: 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 1.0`). + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). 2. Transfer the `SMAPI ` folder to Linux or Mac. _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, since we need to set Unix file permissions that Windows won't save._ @@ -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 .zip` and `SMAPI 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 similarity index 89% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs rename to 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 { - /// Wraps methods that are incompatible when converting compiled code between MonoGame and XNA. - public class SpriteBatchWrapper : SpriteBatch + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + public class SpriteBatchMethods : SpriteBatch { /********* ** Public methods *********/ /// Construct an instance. - public 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 similarity index 50% rename from src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj rename to 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 @@ - + @@ -30,44 +30,15 @@ 4 - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll - True - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll - True - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll - True - - + Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - - - - - + - + \ 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 +{ + /// Generic metadata about a mod. + internal class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + + /// Construct an instance. + /// The mod name. + /// The mod's semantic version number. + /// The mod's web URL. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel(string name, string version, string url, string error = null) + { + this.Name = name; + this.Version = version; + this.Url = url; + this.Error = error; // mainly initialised here for the JSON deserialiser + } + + /// Construct an instance. + /// The error message indicating why the mod is invalid. + 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 +{ + /// Specifies mods whose update-check info to fetch. + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The namespaced mod keys to search. + public ModSearchModel(IEnumerable 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 +{ + /// A low-level implementation of a semantic version with an optional release tag. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + internal class SemanticVersionImpl + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + public int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + public int Patch { get; } + + /// An optional prerelease tag. + public string Tag { get; } + + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + internal static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional prerelease tag. + 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); + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + 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; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + public int CompareTo(SemanticVersionImpl other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The major version to compare with this instance. + /// The minor version to compare with this instance. + /// The patch version to compare with this instance. + /// The prerelease tag to compare with this instance. + 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); + } + + /// Get a string representation of the version. + 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; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out SemanticVersionImpl parsed) + { + try + { + parsed = new SemanticVersionImpl(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation + return null; + return tag; + } + } +} diff --git a/src/SMAPI.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 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc + + + StardewModdingAPI.Common + + + + + + + + + + \ 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 @@ + + + + 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc + 14.0 + + + + + + + + diff --git a/src/StardewModdingAPI.Installer/Enums/Platform.cs b/src/SMAPI.Installer/Enums/Platform.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Enums/Platform.cs rename to src/SMAPI.Installer/Enums/Platform.cs diff --git a/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs b/src/SMAPI.Installer/Enums/ScriptAction.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Enums/ScriptAction.cs rename to src/SMAPI.Installer/Enums/ScriptAction.cs diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs similarity index 99% rename from src/StardewModdingAPI.Installer/InteractiveInstaller.cs rename to 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 /// 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 similarity index 100% rename from src/StardewModdingAPI.Installer/Program.cs rename to src/SMAPI.Installer/Program.cs diff --git a/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs b/src/SMAPI.Installer/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs rename to src/SMAPI.Installer/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj similarity index 91% rename from src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj rename to 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 @@ - + @@ -36,7 +36,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -51,6 +51,6 @@ - - + + \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt similarity index 100% rename from src/StardewModdingAPI.Installer/readme.txt rename to 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 +{ + /// A build task which deploys the mod files and prepares a release zip. + public class DeployModTask : Task + { + /********* + ** Accessors + *********/ + /// The name of the mod folder. + [Required] + public string ModFolderName { get; set; } + + /// The absolute or relative path to the folder which should contain the generated zip file. + [Required] + public string ModZipPath { get; set; } + + /// The folder containing the project files. + [Required] + public string ProjectDir { get; set; } + + /// The folder containing the build output. + [Required] + public string TargetDir { get; set; } + + /// The folder containing the game files. + [Required] + public string GameDir { get; set; } + + /// Whether to enable copying the mod files into the game's Mods folder. + [Required] + public bool EnableModDeploy { get; set; } + + /// Whether to enable the release zip. + [Required] + public bool EnableModZip { get; set; } + + + /********* + ** Public methods + *********/ + /// When overridden in a derived class, executes the task. + /// true if the task successfully executed; otherwise, false. + 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 + *********/ + /// Copy the mod files into the game's mod folder. + /// The files to include. + /// The folder path to create with the mod files. + private void CreateModFolder(IDictionary 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); + } + } + + /// Create a release zip in the recommended format for uploading to mod sites. + /// The files to include. + /// The name of the mod. + /// The mod version string. + /// The absolute or relative path to the folder which should contain the generated zip file. + private void CreateReleaseZip(IDictionary 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); + } + } + } + + /// Get a copy of a filename with all invalid filename characters substituted. + /// The filename. + 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 +{ + /// Manages the files that are part of a mod package. + internal class ModFileManager + { + /********* + ** Properties + *********/ + /// The name of the manifest file. + private readonly string ManifestFileName = "manifest.json"; + + /// The files that are part of the package. + private readonly IDictionary Files; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The folder containing the project files. + /// The folder containing the build output. + /// The mod package isn't valid. + public ModFileManager(string projectDir, string targetDir) + { + this.Files = new Dictionary(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."); + } + + /// Get the files in the mod package. + public IDictionary GetFiles() + { + return new Dictionary(this.Files, StringComparer.InvariantCultureIgnoreCase); + } + + /// Get a semantic version from the mod manifest. + /// The manifest is missing or invalid. + 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 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 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 + *********/ + /// Get a case-insensitive dictionary matching the given JSON. + /// The JSON to parse. + private IDictionary Parse(string json) + { + IDictionary MakeCaseInsensitive(IDictionary dict) + { + foreach (var field in dict.ToArray()) + { + if (field.Value is IDictionary value) + dict[field.Key] = MakeCaseInsensitive(value); + } + return new Dictionary(dict, StringComparer.InvariantCultureIgnoreCase); + } + + IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); + return MakeCaseInsensitive(data); + } + + /// Get whether a string is equal to another case-insensitively. + /// The string value. + /// The string to compare with. + 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 +{ + /// A user error whose message can be displayed to the user. + internal class UserErrorException : Exception + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + 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 @@ + + + + + Debug + x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A} + Library + Properties + StardewModdingAPI.ModBuildConfig + StardewModdingAPI.ModBuildConfig + v4.6.1 + 512 + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + Designer + + + Designer + + + + + + + + \ No newline at end of file diff --git a/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn b/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn new file mode 100644 index 00000000..7bd5c0c5 Binary files /dev/null and b/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn differ diff --git a/src/SMAPI.ModBuildConfig/assets/nuget-icon.png b/src/SMAPI.ModBuildConfig/assets/nuget-icon.png new file mode 100644 index 00000000..611cdf88 Binary files /dev/null and b/src/SMAPI.ModBuildConfig/assets/nuget-icon.png differ 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 @@ + + + + + + + + + + + + + $(DeployModFolderName) + $(DeployModZipTo) + + + $(MSBuildProjectName) + $(TargetDir) + True + True + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + + + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + + + + + + + + + + false + + + false + + + false + + + false + + + $(GamePath)\Stardew Valley.exe + false + + + $(GamePath)\StardewModdingAPI.exe + false + + + $(GamePath)\xTile.dll + false + False + + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + + + $(GamePath)\MonoGame.Framework.dll + false + False + + + $(GamePath)\StardewValley.exe + false + + + $(GamePath)\StardewModdingAPI.exe + false + + + $(GamePath)\xTile.dll + false + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + Pathoschild.Stardew.ModBuildConfig + 2.0.1 + Build package for SMAPI mods + Pathoschild + Pathoschild + false + https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt + https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme + https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png + Automates the build configuration for crossplatform Stardew Valley SMAPI mods. + + 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. + + + + + + + diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs similarity index 94% rename from src/StardewModdingAPI.Tests/Core/ModResolverTests.cs rename to 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()); } [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()); // 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 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()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), 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()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), 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()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), 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()); // assert modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); } -#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 mock = new Mock(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()); // 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 GetMetadata(IManifest manifest, bool allowStatusChange = false) { Mock mod = new Mock(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 /// Set up a mock mod metadata for . /// The mock mod metadata. - /// The compatibility record to set. - private void SetupMetadataForValidation(Mock mod, ModCompatibility compatibility = null) + /// The extra metadata about the mod from SMAPI's internal data (if any). + private void SetupMetadataForValidation(Mock mod, ModDataRecord modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.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 similarity index 99% rename from src/StardewModdingAPI.Tests/Core/TranslationTests.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs rename to src/SMAPI.Tests/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Sample.cs rename to src/SMAPI.Tests/Sample.cs diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj similarity index 73% rename from src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj rename to 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 @@ 4 - - ..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll + + ..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll - - ..\packages\Moq.4.7.99\lib\net45\Moq.dll + + ..\packages\Moq.4.7.142\lib\net45\Moq.dll - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - ..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + + ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll + - + Properties\GlobalAssemblyInfo.cs @@ -59,11 +60,11 @@ - + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI - + \ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs similarity index 99% rename from src/StardewModdingAPI.Tests/Utilities/SDateTests.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs rename to 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 @@ + + + + + + + \ 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 +{ + /// Provides an API to perform mod update checks. + [Produces("application/json")] + [Route("api/{version:semanticVersion}/[controller]")] + internal class ModsController : Controller + { + /********* + ** Properties + *********/ + /// The mod repositories which provide mod metadata. + private readonly IDictionary Repositories; + + /// The cache in which to store mod metadata. + private readonly IMemoryCache Cache; + + /// The number of minutes update checks should be cached before refetching them. + private readonly int CacheMinutes; + + /// A regex which matches SMAPI-style semantic version. + private readonly string VersionRegex; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cache in which to store mod metadata. + /// The config settings for mod update checks. + public ModsController(IMemoryCache cache, IOptions configProvider) + { + ModUpdateCheckConfig config = configProvider.Value; + + this.Cache = cache; + this.CacheMinutes = config.CacheMinutes; + this.VersionRegex = config.SemanticVersionRegex; + + string version = this.GetType().Assembly.GetName().Version.ToString(3); + this.Repositories = + new IModRepository[] + { + new ChucklefishRepository( + vendorKey: config.ChucklefishKey, + userAgent: string.Format(config.ChucklefishUserAgent, version), + baseUrl: config.ChucklefishBaseUrl, + modPageUrlFormat: config.ChucklefishModPageUrlFormat + ), + new GitHubRepository( + vendorKey: config.GitHubKey, + baseUrl: config.GitHubBaseUrl, + releaseUrlFormat: config.GitHubReleaseUrlFormat, + userAgent: string.Format(config.GitHubUserAgent, version), + acceptHeader: config.GitHubAcceptHeader, + username: config.GitHubUsername, + password: config.GitHubPassword + ), + new NexusRepository( + vendorKey: config.NexusKey, + userAgent: config.NexusUserAgent, + baseUrl: config.NexusBaseUrl, + modUrlFormat: config.NexusModUrlFormat + ) + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + } + + /// Fetch version metadata for the given mods. + /// The namespaced mod keys to search as a comma-delimited array. + [HttpGet] + public async Task> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// Fetch version metadata for the given mods. + /// The mod search criteria. + [HttpPost] + public async Task> PostAsync([FromBody] ModSearchModel search) + { + // sort & filter keys + string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) + .ToArray(); + + // fetch mod info + IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + foreach (string modKey in modKeys) + { + // parse mod key + if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) + { + result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + continue; + } + + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + { + result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + continue; + } + + // fetch mod info + result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + + ModInfoModel info = await repository.GetModInfoAsync(modID); + if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) + info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + + return info; + }); + } + + return result; + } + + + /********* + ** Private methods + *********/ + /// Parse a namespaced mod ID. + /// The raw mod ID to parse. + /// The parsed vendor key. + /// The parsed mod ID. + /// Returns whether the value could be parsed. + private bool TryParseModKey(string raw, out string vendorKey, out string modID) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + { + vendorKey = null; + modID = null; + return false; + } + + // parse + vendorKey = parts[0].Trim(); + modID = parts[1].Trim(); + return true; + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs new file mode 100644 index 00000000..03de639e --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -0,0 +1,74 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for mod update checks. + public class ModUpdateCheckConfig + { + /********* + ** Accessors + *********/ + /**** + ** General + ****/ + /// The number of minutes update checks should be cached before refetching them. + public int CacheMinutes { get; set; } + + /// A regex which matches SMAPI-style semantic version. + /// Derived from SMAPI's SemanticVersion implementation. + public string SemanticVersionRegex { get; set; } + + /**** + ** Chucklefish mod site + ****/ + /// The repository key for the Chucklefish mod site. + public string ChucklefishKey { get; set; } + + /// The user agent for the Chucklefish API client, where {0} is the SMAPI version. + public string ChucklefishUserAgent { get; set; } + + /// The base URL for the Chucklefish mod site. + public string ChucklefishBaseUrl { get; set; } + + /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. + public string ChucklefishModPageUrlFormat { get; set; } + + + /**** + ** GitHub + ****/ + /// The repository key for Nexus Mods. + public string GitHubKey { get; set; } + + /// The user agent for the GitHub API client, where {0} is the SMAPI version. + public string GitHubUserAgent { get; set; } + + /// The base URL for the GitHub API. + public string GitHubBaseUrl { get; set; } + + /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. + public string GitHubReleaseUrlFormat { get; set; } + + /// The Accept header value expected by the GitHub API. + public string GitHubAcceptHeader { get; set; } + + /// The username with which to authenticate to the GitHub API (if any). + public string GitHubUsername { get; set; } + + /// The password with which to authenticate to the GitHub API (if any). + public string GitHubPassword { get; set; } + + /**** + ** Nexus Mods + ****/ + /// The repository key for Nexus Mods. + public string NexusKey { get; set; } + + /// The user agent for the Nexus Mods API client. + public string NexusUserAgent { get; set; } + + /// The base URL for the Nexus Mods API. + public string NexusBaseUrl { get; set; } + + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public string NexusModUrlFormat { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs new file mode 100644 index 00000000..2c24c610 --- /dev/null +++ b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace StardewModdingAPI.Web.Framework +{ + /// Discovers controllers with support for non-public controllers. + internal class InternalControllerFeatureProvider : ControllerFeatureProvider + { + /********* + ** Public methods + *********/ + /// Determines if a given type is a controller. + /// The candidate. + /// true if the type is a controller; otherwise false. + protected override bool IsController(TypeInfo type) + { + return + type.IsClass + && !type.IsAbstract + && (/*type.IsPublic &&*/ !type.ContainsGenericParameters) + && (!type.IsDefined(typeof(NonControllerAttribute)) + && (type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || type.IsDefined(typeof(ControllerAttribute)))); + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs new file mode 100644 index 00000000..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 + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public abstract void Dispose(); + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public abstract Task GetModInfoAsync(string id); + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + protected RepositoryBase(string vendorKey) + { + this.VendorKey = vendorKey; + } + + /// Normalise a version string. + /// The version to normalise. + protected string NormaliseVersion(string version) + { + if (string.IsNullOrWhiteSpace(version)) + return null; + + version = version.Trim(); + if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix + version = version.Substring(1); + + return version; + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs new file mode 100644 index 00000000..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 +{ + /// An HTTP client for fetching mod metadata from the Chucklefish mod site. + internal class ChucklefishRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// The base URL for the Chucklefish mod site. + private readonly string BaseUrl; + + /// The URL for a mod page excluding the base URL, where {0} is the mod ID. + private readonly string ModPageUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The user agent for the API client. + /// The base URL for the Chucklefish mod site. + /// The URL for a mod page excluding the , where {0} is the mod ID. + public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) + : base(vendorKey) + { + this.BaseUrl = baseUrl; + this.ModPageUrlFormat = modPageUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!uint.TryParse(id, out uint _)) + return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + + // fetch info + try + { + // fetch HTML + string html; + try + { + html = await this.Client + .GetAsync(string.Format(this.ModPageUrlFormat, id)) + .AsString(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new ModInfoModel("Found no mod with this ID."); + } + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // extract mod info + string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); + string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; + if (name.StartsWith("[SMAPI] ")) + name = name.Substring("[SMAPI] ".Length); + string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; + + // create model + return new ModInfoModel(name, this.NormaliseVersion(version), url); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs new file mode 100644 index 00000000..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 +{ + /// An HTTP client for fetching mod metadata from GitHub project releases. + internal class GitHubRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ReleaseUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + /// The user agent for the API client. + /// The Accept header value expected by the GitHub API. + /// The username with which to authenticate to the GitHub API. + /// The password with which to authenticate to the GitHub API. + public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + : base(vendorKey) + { + this.ReleaseUrlFormat = releaseUrlFormat; + + this.Client = new FluentClient(baseUrl) + .SetUserAgent(userAgent) + .AddDefault(req => req.WithHeader("Accept", acceptHeader)); + if (!string.IsNullOrWhiteSpace(username)) + this.Client = this.Client.SetBasicAuthentication(username, password); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) + return new ModInfoModel($"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); + + // fetch info + try + { + GitRelease release = await this.Client + .GetAsync(string.Format(this.ReleaseUrlFormat, id)) + .As(); + return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new ModInfoModel("Found no mod with this ID."); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// Metadata about a GitHub release tag. + private class GitRelease + { + /********* + ** Accessors + *********/ + /// The display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The semantic version string. + [JsonProperty("tag_name")] + public string Tag { get; set; } + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs new file mode 100644 index 00000000..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 +{ + /// A repository which provides mod metadata. + internal interface IModRepository : IDisposable + { + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + Task GetModInfoAsync(string id); + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs new file mode 100644 index 00000000..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 +{ + /// An HTTP client for fetching mod metadata from Nexus Mods. + internal class NexusRepository : RepositoryBase + { + /********* + ** Properties + *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ModUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The user agent for the Nexus Mods API client. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) + : base(vendorKey) + { + this.ModUrlFormat = modUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!uint.TryParse(id, out uint _)) + return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + + // fetch info + try + { + NexusResponseModel response = await this.Client + .GetAsync(string.Format(this.ModUrlFormat, id)) + .As(); + + return response != null + ? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url) + : new ModInfoModel("Found no mod with this ID."); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// A mod metadata response from Nexus Mods. + private class NexusResponseModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + } + } +} diff --git a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs new file mode 100644 index 00000000..5a56844f --- /dev/null +++ b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework +{ + /// Rewrite requests to prepend the subdomain portion (if any) to the path. + /// Derived from . + internal class RewriteSubdomainRule : IRule + { + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + context.Result = RuleResult.ContinueRules; + + // get host parts + string host = context.HttpContext.Request.Host.Host; + string[] parts = host.Split('.'); + + // validate + if (parts.Length < 2) + return; + if (parts.Length < 3 && !"localhost".Equals(parts[1], StringComparison.InvariantCultureIgnoreCase)) + return; + + // prepend to path + context.HttpContext.Request.Path = $"/{parts[0]}{context.HttpContext.Request.Path}"; + } + } +} diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs new file mode 100644 index 00000000..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 +{ + /// Constrains a route value to a valid semantic version. + internal class VersionConstraint : RegexRouteConstraint + { + /********* + ** Public methods + *********/ + /// Construct an instance. + 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 +{ + /// The main app entry point. + public class Program + { + /********* + ** Public methods + *********/ + /// The main app entry point. + /// The command-line arguments. + public static void Main(string[] args) + { + // configure web server + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build() + .Run(); + } + } +} diff --git a/src/SMAPI.Web/Properties/AssemblyInfo.cs b/src/SMAPI.Web/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..63f787a4 --- /dev/null +++ b/src/SMAPI.Web/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("StardewModdingAPI.Web")] +[assembly: AssemblyProduct("StardewModdingAPI.Web")] diff --git a/src/SMAPI.Web/Properties/launchSettings.json b/src/SMAPI.Web/Properties/launchSettings.json new file mode 100644 index 00000000..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 @@ + + + + netcoreapp2.0 + false + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs new file mode 100644 index 00000000..eaf14983 --- /dev/null +++ b/src/SMAPI.Web/Startup.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.ConfigModels; + +namespace StardewModdingAPI.Web +{ + /// The web app startup configuration. + internal class Startup + { + /********* + ** Accessors + *********/ + /// The web app configuration. + public IConfigurationRoot Configuration { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The hosting environment. + public Startup(IHostingEnvironment env) + { + this.Configuration = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddEnvironmentVariables() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables() + .Build(); + } + + /// The method called by the runtime to add services to the container. + /// The service injection container. + public void ConfigureServices(IServiceCollection services) + { + services + .Configure(this.Configuration.GetSection("ModUpdateCheck")) + .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) + .AddMemoryCache() + .AddMvc() + .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) + .AddJsonOptions(options => + { + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + }); + } + + /// The method called by the runtime to configure the HTTP request pipeline. + /// The application builder. + /// The hosting environment. + /// The logger factory. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + app + .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseMvc(); + } + } +} diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json new file mode 100644 index 00000000..fa8ce71a --- /dev/null +++ b/src/SMAPI.Web/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json new file mode 100644 index 00000000..852f6f71 --- /dev/null +++ b/src/SMAPI.Web/appsettings.json @@ -0,0 +1,30 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "ModUpdateCheck": { + "CacheMinutes": 60, + "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", + + "ChucklefishKey": "Chucklefish", + "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "ChucklefishBaseUrl": "https://community.playstarbound.com", + "ChucklefishModPageUrlFormat": "resources/{0}", + + "GitHubKey": "GitHub", + "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "GitHubBaseUrl": "https://api.github.com", + "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubAcceptHeader": "application/vnd.github.v3+json", + "GitHubUsername": null, /* set via environment properties */ + "GitHubPassword": null, /* set via environment properties */ + + "NexusKey": "Nexus", + "NexusUserAgent": "Nexus Client v0.63.15", + "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", + "NexusModUrlFormat": "mods/{0}" + } +} diff --git a/src/StardewModdingAPI.sln b/src/SMAPI.sln similarity index 52% rename from src/StardewModdingAPI.sln rename to 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 similarity index 94% rename from src/StardewModdingAPI.sln.DotSettings rename to src/SMAPI.sln.DotSettings index 06cc66ef..d16ef684 100644 --- a/src/StardewModdingAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -1,4 +1,5 @@  + DO_NOT_SHOW DO_NOT_SHOW HINT HINT diff --git a/src/StardewModdingAPI/App.config b/src/SMAPI/App.config similarity index 100% rename from src/StardewModdingAPI/App.config rename to src/SMAPI/App.config diff --git a/src/StardewModdingAPI/Constants.cs b/src/SMAPI/Constants.cs similarity index 58% rename from src/StardewModdingAPI/Constants.cs rename to 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 ****/ /// SMAPI's current semantic version. - 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); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); @@ -97,6 +87,14 @@ namespace StardewModdingAPI Platform.Mono; #endif + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API. + internal static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", + ["GitHub"] = "https://github.com/{0}/releases" + }; + /********* ** Internal methods @@ -147,79 +145,6 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } - /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. - internal static IEnumerable 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 similarity index 100% rename from src/StardewModdingAPI/ContentSource.cs rename to src/SMAPI/ContentSource.cs diff --git a/src/StardewModdingAPI/Context.cs b/src/SMAPI/Context.cs similarity index 100% rename from src/StardewModdingAPI/Context.cs rename to src/SMAPI/Context.cs diff --git a/src/StardewModdingAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs similarity index 100% rename from src/StardewModdingAPI/Events/ChangeType.cs rename to src/SMAPI/Events/ChangeType.cs diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/ContentEvents.cs rename to src/SMAPI/Events/ContentEvents.cs diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/ControlEvents.cs rename to src/SMAPI/Events/ControlEvents.cs diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs rename to src/SMAPI/Events/EventArgsClickableMenuChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs rename to src/SMAPI/Events/EventArgsClickableMenuClosed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs rename to src/SMAPI/Events/EventArgsControllerButtonPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs rename to src/SMAPI/Events/EventArgsControllerButtonReleased.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs rename to src/SMAPI/Events/EventArgsControllerTriggerPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs rename to src/SMAPI/Events/EventArgsControllerTriggerReleased.cs diff --git a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs rename to src/SMAPI/Events/EventArgsCurrentLocationChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs rename to src/SMAPI/Events/EventArgsGameLocationsChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs similarity index 99% rename from src/StardewModdingAPI/Events/EventArgsInput.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsIntChanged.cs rename to src/SMAPI/Events/EventArgsIntChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs rename to src/SMAPI/Events/EventArgsInventoryChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs b/src/SMAPI/Events/EventArgsKeyPressed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsKeyPressed.cs rename to src/SMAPI/Events/EventArgsKeyPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs rename to src/SMAPI/Events/EventArgsKeyboardStateChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsLevelUp.cs rename to src/SMAPI/Events/EventArgsLevelUp.cs diff --git a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs rename to src/SMAPI/Events/EventArgsLocationObjectsChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs b/src/SMAPI/Events/EventArgsMineLevelChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs rename to src/SMAPI/Events/EventArgsMineLevelChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs b/src/SMAPI/Events/EventArgsMouseStateChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs rename to src/SMAPI/Events/EventArgsMouseStateChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs b/src/SMAPI/Events/EventArgsValueChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsValueChanged.cs rename to 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 +{ + /// Events raised when the game changes state. + public static class GameEvents + { + /********* + ** Events + *********/ + /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . + internal static event EventHandler InitializeInternal; + + /// Raised when the game updates its state (≈60 times per second). + public static event EventHandler UpdateTick; + + /// Raised every other tick (≈30 times per second). + public static event EventHandler SecondUpdateTick; + + /// Raised every fourth tick (≈15 times per second). + public static event EventHandler FourthUpdateTick; + + /// Raised every eighth tick (≈8 times per second). + public static event EventHandler EighthUpdateTick; + + /// Raised every 15th tick (≈4 times per second). + public static event EventHandler QuarterSecondTick; + + /// Raised every 30th tick (≈twice per second). + public static event EventHandler HalfSecondTick; + + /// Raised every 60th tick (≈once per second). + public static event EventHandler OneSecondTick; + + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeInitialize(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); + } + + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeSecondUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeFourthUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeEighthUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeQuarterSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeHalfSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeOneSecondTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); + } + } +} diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/GraphicsEvents.cs rename to src/SMAPI/Events/GraphicsEvents.cs diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs similarity index 98% rename from src/StardewModdingAPI/Events/InputEvents.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI/Events/ItemStackChange.cs rename to src/SMAPI/Events/ItemStackChange.cs diff --git a/src/StardewModdingAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/LocationEvents.cs rename to src/SMAPI/Events/LocationEvents.cs diff --git a/src/StardewModdingAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/MenuEvents.cs rename to src/SMAPI/Events/MenuEvents.cs diff --git a/src/StardewModdingAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/MineEvents.cs rename to 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 +{ + /// Events raised when the player data changes. + public static class PlayerEvents + { + /********* + ** Events + *********/ + /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). + public static event EventHandler InventoryChanged; + + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public static event EventHandler LeveledUp; + + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates monitoring and logging. + /// The player's inventory. + /// The inventory changes. + internal static void InvokeInventoryChanged(IMonitor monitor, List inventory, IEnumerable changedItems) + { + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); + } + + /// Rase a event. + /// Encapsulates monitoring and logging. + /// The player skill that leveled up. + /// The new skill level. + internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel) + { + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel)); + } + } +} diff --git a/src/StardewModdingAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/SaveEvents.cs rename to 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 +{ + /// Events raised when the in-game date or time changes. + public static class TimeEvents + { + /********* + ** Events + *********/ + /// Raised after the game begins a new day, including when loading a save. + public static event EventHandler AfterDayStarted; + + /// Raised after the in-game clock changes. + public static event EventHandler TimeOfDayChanged; + + /********* + ** Internal methods + *********/ + /// Raise an event. + /// Encapsulates monitoring and logging. + internal static void InvokeAfterDayStarted(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The previous time in military time format (e.g. 6:00pm is 1800). + /// The current time in military time format (e.g. 6:10pm is 1810). + internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime) + { + monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Command.cs b/src/SMAPI/Framework/Command.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Command.cs rename to src/SMAPI/Framework/Command.cs diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs similarity index 96% rename from src/StardewModdingAPI/Framework/CommandManager.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetData.cs rename to src/SMAPI/Framework/Content/AssetData.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs rename to src/SMAPI/Framework/Content/AssetDataForDictionary.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs rename to src/SMAPI/Framework/Content/AssetDataForImage.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs rename to src/SMAPI/Framework/Content/AssetDataForObject.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetInfo.cs rename to src/SMAPI/Framework/Content/AssetInfo.cs diff --git a/src/StardewModdingAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ContentManagerShim.cs rename to src/SMAPI/Framework/ContentManagerShim.cs diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs similarity index 98% rename from src/StardewModdingAPI/Framework/CursorPosition.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI/Framework/DeprecationLevel.cs rename to src/SMAPI/Framework/DeprecationLevel.cs diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs similarity index 81% rename from src/StardewModdingAPI/Framework/DeprecationManager.cs rename to 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 ?? "", 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; } - - /// Get whether a type implements the given virtual method. - /// The type to check. - /// The base type which declares the virtual method. - /// The method name. - /// The expected argument types. - 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 similarity index 100% rename from src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs rename to src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs rename to src/SMAPI/Framework/Exceptions/SContentLoadException.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/SMAPI/Framework/Exceptions/SParseException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Exceptions/SParseException.cs rename to src/SMAPI/Framework/Exceptions/SParseException.cs diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs similarity index 100% rename from src/StardewModdingAPI/Framework/GameVersion.cs rename to src/SMAPI/Framework/GameVersion.cs diff --git a/src/StardewModdingAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs similarity index 84% rename from src/StardewModdingAPI/Framework/IModMetadata.cs rename to 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 /// The mod manifest. IManifest Manifest { get; } - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - ModCompatibility Compatibility { get; } + /// >Metadata about the mod from SMAPI's internal data (if any). + ModDataRecord DataRecord { get; } /// The metadata resolution status. ModMetadataStatus Status { get; } diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs similarity index 87% rename from src/StardewModdingAPI/Framework/InternalExtensions.cs rename to 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 } } + /// Log a message for the player or developer the first time it occurs. + /// The monitor through which to log the message. + /// The hash of logged messages. + /// The message to log. + /// The log severity level. + public static void LogOnce(this IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + /**** ** Exceptions ****/ diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs rename to src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs rename to src/SMAPI/Framework/Logging/InterceptingTextWriter.cs diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Logging/LogFileManager.cs rename to src/SMAPI/Framework/Logging/LogFileManager.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs rename to src/SMAPI/Framework/ModHelpers/BaseHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs rename to src/SMAPI/Framework/ModHelpers/CommandHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs similarity index 98% rename from src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs rename to 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 similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs rename to src/SMAPI/Framework/ModHelpers/ModHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs rename to src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs similarity index 99% rename from src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs rename to 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 /// The type being accessed. 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 } /// Assert that mods can use the reflection helper to access the given type. diff --git a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs rename to src/SMAPI/Framework/ModHelpers/TranslationHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs rename to src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs rename to src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs similarity index 68% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs rename to 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 /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// Whether to enable developer mode logging. + private readonly bool IsDeveloperMode; + /********* ** Public methods @@ -32,9 +35,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The current game platform. /// Encapsulates monitoring and logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor) + /// Whether to enable developer mode logging. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) { this.Monitor = monitor; + this.IsDeveloperMode = isDeveloperMode; this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); // generate type => assembly lookup for types which should be rewritten @@ -54,11 +59,12 @@ namespace StardewModdingAPI.Framework.ModLoading } /// Preprocess and load an assembly. + /// The mod for which the assembly is being loaded. /// The assembly file path. /// Assume the mod is compatible, even if incompatible code is detected. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(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 loggedMessages = new HashSet(); 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 ****/ /// Rewrite the types referenced by an assembly. + /// The mod for which the assembly is being loaded. /// The assembly to rewrite. /// Assume the mod is compatible, even if incompatible code is detected. + /// The messages that have already been logged for this mod. /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; - HashSet loggedMessages = new HashSet(); 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; } + /// Process the result from an instruction handler. + /// The mod being analysed. + /// The instruction handler. + /// The result returned by the handler. + /// The messages already logged for the current mod. + /// Assume the mod is compatible, even if incompatible code is detected. + /// A string to prefix to log messages. + /// The assembly filename for log messages. + private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) + { + switch (result) + { + case InstructionHandleResult.Rewritten: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); + break; + + case InstructionHandleResult.NotCompatible: + if (!assumeCompatible) + throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedGamePatch: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedSaveSerialiser: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedDynamic: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", +#if SMAPI_FOR_WINDOWS + this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug +#else + LogLevel.Warn +#endif + ); + break; + + case InstructionHandleResult.None: + break; + + default: + throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); + } + } + /// Get the correct reference to use for compatibility with the current platform. /// The type reference to rewrite. private void ChangeTypeScope(TypeReference type) @@ -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 ); } - - /// Log a message for the player or developer the first time it occurs. - /// The monitor through which to log the message. - /// The hash of logged messages. - /// The message to log. - /// The log severity level. - private void LogOnce(IMonitor monitor, HashSet 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 similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs rename to src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs similarity index 53% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs rename to 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 { - /// Finds incompatible CIL instructions that reference a given event and throws an . - public class EventFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given event. + internal class EventFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// The event name for which to find references. private readonly string EventName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Construct an instance. /// The full type name for which to find references. /// The event name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public EventFinder(string fullTypeName, string eventName, string nounPhrase = null) + /// The result to return for matching instructions. + 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"; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 53% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs rename to 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 { - /// Finds incompatible CIL instructions that reference a given field and throws an . - public class FieldFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given field. + internal class FieldFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// The field name for which to find references. private readonly string FieldName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Construct an instance. /// The full type name for which to find references. /// The field name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public FieldFinder(string fullTypeName, string fieldName, string nounPhrase = null) + /// The result to return for matching instructions. + 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"; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 52% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs rename to 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 { - /// Finds incompatible CIL instructions that reference a given method and throws an . - public class MethodFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given method. + internal class MethodFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// The method name for which to find references. private readonly string MethodName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Construct an instance. /// The full type name for which to find references. /// The method name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public MethodFinder(string fullTypeName, string methodName, string nounPhrase = null) + /// The result to return for matching instructions. + 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"; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 53% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs rename to 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 { - /// Finds incompatible CIL instructions that reference a given property and throws an . - public class PropertyFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given property. + internal class PropertyFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// The property name for which to find references. private readonly string PropertyName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,40 +32,36 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Construct an instance. /// The full type name for which to find references. /// The property name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public PropertyFinder(string fullTypeName, string propertyName, string nounPhrase = null) + /// The result to return for matching instructions. + 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"; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 64% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs rename to 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 { - /// Finds incompatible CIL instructions that reference a given type and throws an . - public class TypeFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given type. + internal class TypeFinder : IInstructionHandler { /********* ** Accessors @@ -13,6 +13,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// The full type name for which to find references. private readonly string FullTypeName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -26,42 +29,37 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders *********/ /// Construct an instance. /// The full type name to match. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public TypeFinder(string fullTypeName, string nounPhrase = null) + /// The result to return for matching instructions. + public TypeFinder(string fullTypeName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName} type"; + this.Result = result; + this.NounPhrase = $"{fullTypeName} type"; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 +{ + /// Performs predefined logic for detected CIL instructions. + internal interface IInstructionHandler + { + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the handler matches. + string NounPhrase { get; } + + + /********* + ** Methods + *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + } +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs similarity index 90% rename from src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs rename to 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 { /// An exception raised when an incompatible instruction is found while loading a mod assembly. - 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 +{ + /// Indicates how an instruction was handled. + internal enum InstructionHandleResult + { + /// No special handling is needed. + None, + + /// The instruction was successfully rewritten for compatibility. + Rewritten, + + /// The instruction is not compatible and can't be rewritten for compatibility. + NotCompatible, + + /// The instruction is compatible, but patches the game in a way that may impact stability. + DetectedGamePatch, + + /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. + DetectedSaveSerialiser, + + /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. + DetectedDynamic + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs similarity index 87% rename from src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs rename to 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 { /// An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright. - public class InvalidModStateException : Exception + internal class InvalidModStateException : Exception { /// Construct an instance. /// The error message. diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs rename to src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs similarity index 79% rename from src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs rename to 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 /// The mod manifest. public IManifest Manifest { get; } - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModCompatibility Compatibility { get; } + /// Metadata about the mod from SMAPI's internal data (if any). + public ModDataRecord DataRecord { get; } /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } @@ -37,13 +37,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's display name. /// The mod's full directory path. /// The mod manifest. - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + /// Metadata about the mod from SMAPI's internal data (if any). + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; this.Manifest = manifest; - this.Compatibility = compatibility; + this.DataRecord = dataRecord; } /// Set the mod status. diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs similarity index 85% rename from src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs rename to 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 { /// Indicates the status of a mod's metadata resolution. internal enum ModMetadataStatus @@ -9,4 +9,4 @@ /// The mod cannot be loaded. Failed } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs similarity index 80% rename from src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs rename to 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 /// Get manifest metadata for each folder in the given root path. /// The root path to search for mods. /// The JSON helper with which to read manifests. - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - /// Metadata about mods that SMAPI should consider obsolete and not load. + /// Metadata about mods from SMAPI's internal data. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords, IEnumerable disabledMods) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable 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); } } /// Validate manifest metadata. /// The mod manifests to validate. /// The current SMAPI version. - public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion) + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, IDictionary vendorModUrls) { mods = mods.ToArray(); @@ -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 updateUrls = new List(); + foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) + { + string[] parts = key.Split(new[] { ':' }, 2); + if (parts.Length != 2) + continue; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + updateUrls.Add(string.Format(urlTemplate, modID)); + } + if (mod.DataRecord.AlternativeUrl != null) + updateUrls.Add(mod.DataRecord.AlternativeUrl); + + // build error + string error = $"{reasonPhrase}. Please check for a "; + if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + error += "newer version"; + else + error += $"version newer than {compatibility.UpperVersion}"; + error += " at " + string.Join(" or ", updateUrls); + + mod.SetStatus(ModMetadataStatus.Failed, error); + } continue; - } } // validate SMAPI version @@ -150,7 +151,6 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate required fields -#if !SMAPI_1_x { List missingFields = new List(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 } /// Sort the given mods by the order they should be loaded. @@ -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 similarity index 74% rename from src/StardewModdingAPI.AssemblyRewriters/Platform.cs rename to 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 { /// The game's platform version. - public enum Platform + internal enum Platform { /// The Linux/Mac version of the game. Mono, @@ -9,4 +9,4 @@ namespace StardewModdingAPI.AssemblyRewriters /// The Windows version of the game. Windows } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs similarity index 96% rename from src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs rename to 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 { /// Metadata for mapping assemblies to the current . - public class PlatformAssemblyMap + internal class PlatformAssemblyMap { /********* ** Accessors diff --git a/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs similarity index 98% rename from src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs rename to 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 { /// Provides helper methods for field rewriters. internal static class RewriteHelper diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs similarity index 55% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs rename to 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 { /// Rewrites references to one field with another. - public class FieldReplaceRewriter : FieldFinder + internal class FieldReplaceRewriter : FieldFinder { /********* ** Properties @@ -23,31 +23,28 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// The type whose field to which references should be rewritten. /// The field name to rewrite. /// The new field name to reference. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - 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."); } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 57% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs rename to 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 { /// Rewrites field references into property references. - public class FieldToPropertyRewriter : FieldFinder + internal class FieldToPropertyRewriter : FieldFinder { /********* ** Properties @@ -24,31 +24,28 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// Construct an instance. /// The type whose field to which references should be rewritten. /// The field name to rewrite. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 63% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs rename to 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 { /// Rewrites method references from one parent type to another if the signatures match. - public class MethodParentRewriter : IInstructionRewriter + internal class MethodParentRewriter : IInstructionHandler { /********* ** Properties @@ -34,43 +34,38 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// The type whose methods to remap. /// The type with methods to map to. /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - 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; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 similarity index 74% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs rename to 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 { /// Rewrites all references to a type. - public class TypeReferenceRewriter : TypeFinder + internal class TypeReferenceRewriter : TypeFinder { /********* ** Properties @@ -24,22 +24,19 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - 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; } - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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; } - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 *********/ /// Get the adjusted type reference if it matches, else the same value. - /// The module being rewritten. + /// The assembly module containing the instruction. /// The type to replace if it matches. 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 +{ + /// Rewrites virtual calls to the method. + internal class VirtualEntryCallRemover : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The type containing the method. + private readonly Type ToType; + + /// The name of the method. + private readonly string MethodName; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public VirtualEntryCallRemover() + { + this.ToType = typeof(Mod); + this.MethodName = nameof(Mod.Entry); + this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + // get instructions comprising method call + int index = cil.Body.Instructions.IndexOf(instruction); + Instruction loadArg0 = cil.Body.Instructions[index - 2]; + Instruction loadArg1 = cil.Body.Instructions[index - 1]; + if (loadArg0.OpCode != OpCodes.Ldarg_0) + throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); + if (loadArg1.OpCode != OpCodes.Ldarg_1) + throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); + + // remove method call + cil.Remove(loadArg0); + cil.Remove(loadArg1); + cil.Remove(instruction); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.ToType.FullName + && methodRef.Name == this.MethodName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs similarity index 94% rename from src/StardewModdingAPI/Framework/ModRegistry.cs rename to 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)); } /// Get whether a mod has been loaded. diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs similarity index 82% rename from src/StardewModdingAPI/Framework/Models/Manifest.cs rename to 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; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } /// The other mods that must be loaded before this mod. [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } + /// The unique mod ID. public string UniqueID { get; set; } -#if SMAPI_1_x - /// Whether the mod uses per-save config files. - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public bool PerSaveConfigs { get; set; } -#endif - /// Any manifest fields which didn't match a valid field. [JsonExtensionData] public IDictionary ExtraFields { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs similarity index 88% rename from src/StardewModdingAPI/Framework/Models/ManifestDependency.cs rename to 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 { /// A mod dependency listed in a mod manifest. internal class ManifestDependency : IManifestDependency @@ -12,10 +12,8 @@ /// The minimum required version (if any). public ISemanticVersion MinimumVersion { get; set; } -#if !SMAPI_1_x /// Whether the dependency must be installed to use the mod. public bool IsRequired { get; set; } -#endif /********* ** Public methods @@ -24,19 +22,13 @@ /// The unique mod ID to require. /// The minimum required version (if any). /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion -#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 +{ + /// Specifies the compatibility of a given mod version range. + internal class ModCompatibility + { + /********* + ** Accessors + *********/ + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + /// The mod compatibility. + public ModStatus Status { get; } + + /// The reason phrase to show in log output, or null to use the default value. + /// For example, "this version is incompatible with the latest version of the game". + public string ReasonPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range. + /// The mod compatibility. + /// The reason phrase to show in log output, or null to use the default value. + public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) + { + // extract version strings + string[] versions = versionRange.Split('~'); + if (versions.Length != 2) + throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); + + // initialise + this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; + this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; + this.Status = status; + this.ReasonPhrase = reasonPhrase; + } + + /// Get whether a given version is contained within this compatibility range. + /// The version to check. + public bool MatchesVersion(ISemanticVersion version) + { + return + (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); + } + } +} diff --git a/src/SMAPI/Framework/Models/ModDataID.cs b/src/SMAPI/Framework/Models/ModDataID.cs new file mode 100644 index 00000000..d19434fa --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataID.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.Models +{ + /// Uniquely identifies a mod in SMAPI's internal data. + /// + /// This represents a custom format which uniquely identifies a mod across all versions, even + /// if its field values change or it doesn't specify a unique ID. This is mapped to a string + /// with the following format: + /// + /// 1. If the mod's identifier changed over time, multiple variants can be separated by the | + /// character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. + /// + internal class ModDataID + { + /********* + ** Properties + *********/ + /// The unique sets of field values which identify this mod. + private readonly FieldSnapshot[] Snapshots; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModDataID() { } + + /// Construct an instance. + /// The mod identifier string (see remarks on ). + public ModDataID(string data) + { + this.Snapshots = + ( + from string part in data.Split('|') + let str = part.Trim() + select str.StartsWith("{") + ? JsonConvert.DeserializeObject(str) + : new FieldSnapshot { ID = str } + ) + .ToArray(); + } + + /// Get whether this ID matches a given mod manifest. + /// The mod's unique ID, or a substitute ID if it isn't set in the manifest. + /// The manifest to check. + public bool Matches(string id, IManifest manifest) + { + return this.Snapshots.Any(snapshot => + snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) + && ( + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) + ); + } + + + /********* + ** Private models + *********/ + /// A unique set of fields which identifies the mod. + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The mod name, or null to ignore the mod name. + public string Name { get; set; } + + /// The author name, or null to ignore the author. + public string Author { get; set; } + } + } +} diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs new file mode 100644 index 00000000..c6a12188 --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataRecord.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata about a mod from SMAPI's internal data. + internal class ModDataRecord + { + /********* + ** Accessors + *********/ + /// The unique mod identifier. + [JsonConverter(typeof(SFieldConverter))] + public ModDataID ID { get; set; } + + /// A value to inject into field if it's not already set. + public string[] UpdateKeys { get; set; } + + /// The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible. + public string AlternativeUrl { get; set; } + + /// The compatibility of given mod versions (if any). + [JsonConverter(typeof(SFieldConverter))] + public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; + + /// Map local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Map remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get the compatibility record for a given version, if any. + /// The mod version to check. + public ModCompatibility GetCompatibility(ISemanticVersion version) + { + return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); + } + + /// Get a semantic local version for update checks. + /// The local version to normalise. + public string GetLocalVersionForUpdateChecks(string version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + } +} diff --git a/src/SMAPI/Framework/Models/ModStatus.cs b/src/SMAPI/Framework/Models/ModStatus.cs new file mode 100644 index 00000000..343ccb7e --- /dev/null +++ b/src/SMAPI/Framework/Models/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Indicates how SMAPI should treat a mod. + internal enum ModStatus + { + /// Don't override the status. + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, + + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible + } +} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs new file mode 100644 index 00000000..401e1a3a --- /dev/null +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// The SMAPI configuration settings. + internal class SConfig + { + /******** + ** Accessors + ********/ + /// Whether to enable development features. + public bool DeveloperMode { get; set; } + + /// Whether to check for newer versions of SMAPI and mods on startup. + public bool CheckForUpdates { get; set; } + + /// SMAPI's GitHub project name, used to perform update checks. + public string GitHubProjectName { get; set; } + + /// The base URL for SMAPI's web API, used to perform update checks. + public string WebApiBaseUrl { get; set; } + + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } + + /// Extra metadata about mods. + public ModDataRecord[] ModData { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs similarity index 90% rename from src/StardewModdingAPI/Framework/Monitor.cs rename to 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 /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; -#if !SMAPI_1_x /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } -#endif /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } @@ -89,7 +87,6 @@ namespace StardewModdingAPI.Framework this.ExitTokenSource.Cancel(); } -#if !SMAPI_1_x /// Write a newline to the console and log file. internal void Newline() { @@ -98,20 +95,6 @@ namespace StardewModdingAPI.Framework if (this.WriteToFile) this.LogFile.WriteLine(""); } -#endif - -#if SMAPI_1_x - /// Log a message for the player or developer, using the specified console color. - /// The name of the mod logging the message. - /// The message to log. - /// The console color. - /// The log level. - [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 /// Get the color scheme to use for the current console. private static IDictionary GetConsoleColorScheme() { -#if !SMAPI_1_x // scheme for dark console background if (Monitor.IsDark(Console.BackgroundColor)) { -#endif return new Dictionary { [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 } /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. diff --git a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs rename to src/SMAPI/Framework/Reflection/CacheEntry.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs b/src/SMAPI/Framework/Reflection/PrivateField.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/PrivateField.cs rename to src/SMAPI/Framework/Reflection/PrivateField.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs b/src/SMAPI/Framework/Reflection/PrivateMethod.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs rename to src/SMAPI/Framework/Reflection/PrivateMethod.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs rename to src/SMAPI/Framework/Reflection/PrivateProperty.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/Reflector.cs rename to src/SMAPI/Framework/Reflection/Reflector.cs diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/SMAPI/Framework/RequestExitDelegate.cs similarity index 100% rename from src/StardewModdingAPI/Framework/RequestExitDelegate.cs rename to src/SMAPI/Framework/RequestExitDelegate.cs diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs similarity index 82% rename from src/StardewModdingAPI/Framework/SContentManager.cs rename to 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 *********/ - /// The possible directory separator characters in an asset key. - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// The preferred directory separator chaeacter in an asset key. private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); @@ -51,6 +48,9 @@ namespace StardewModdingAPI.Framework /// A lookup of the content managers which loaded each asset. private readonly IDictionary> AssetLoaders = new Dictionary>(); + /// An object locked to prevent concurrent changes to the underlying assets. + private readonly object Lock = new object(); + /********* ** Accessors @@ -61,8 +61,11 @@ namespace StardewModdingAPI.Framework /// Interceptors which edit matching assets after they're loaded. internal IDictionary> Editors { get; } = new Dictionary>(); + /// The possible directory separator characters in an asset key. + internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + /// The absolute path to the . - 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 /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); + lock (this.Lock) + { + assetName = this.NormaliseAssetName(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } } /// Load an asset that has been processed by the content pipeline. @@ -146,38 +152,41 @@ namespace StardewModdingAPI.Framework /// The content manager instance for which to load the asset. public T LoadFor(string assetName, ContentManager instance) { - assetName = this.NormaliseAssetName(assetName); + lock (this.Lock) + { + assetName = this.NormaliseAssetName(assetName); - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - { - this.TrackAssetLoader(assetName, instance); - return base.Load(assetName); - } - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load(assetName); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } + this.TrackAssetLoader(assetName, instance); + return base.Load(assetName); + } - // update cache & return data - this.Cache[assetName] = data; - this.TrackAssetLoader(assetName, instance); - return data; + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Cache[assetName] = data; + this.TrackAssetLoader(assetName, instance); + return data; + } } /// Inject an asset into the cache. @@ -186,9 +195,12 @@ namespace StardewModdingAPI.Framework /// The asset value. public void Inject(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); + } } /// Get the current content locale. @@ -200,16 +212,19 @@ namespace StardewModdingAPI.Framework /// Get the cached asset keys. public IEnumerable GetAssetKeys() { - IEnumerable GetAllAssetKeys() + lock (this.Lock) { - foreach (string cacheKey in this.Cache.Keys) + IEnumerable 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(); + } } /// Purge assets from the cache that match one of the interceptors. @@ -248,45 +263,48 @@ namespace StardewModdingAPI.Framework /// Returns whether any cache entries were invalidated. public bool InvalidateCache(Func predicate, bool dispose = false) { - // find matching asset keys - HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet purgeAssetKeys = new HashSet(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 purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet purgeAssetKeys = new HashSet(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; } /// Dispose assets for the given content manager shim. diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs similarity index 96% rename from src/StardewModdingAPI/Framework/SGame.cs rename to 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 /// The time of day (in 24-hour military format) at last check. private int PreviousTime; -#if SMAPI_1_x - /// The day of month (1–28) at last check. - private int PreviousDay; - - /// The season name (winter, spring, summer, or fall) at last check. - private string PreviousSeason; - - /// The year number at last check. - private int PreviousYear; - - /// Whether the game was transitioning to a new day at last check. - private bool PreviousIsNewDay; - - /// The player character at last check. - private SFarmer PreviousFarmer; -#endif - /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -164,10 +144,10 @@ namespace StardewModdingAPI.Framework private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(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,25 +538,8 @@ 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 *********/ @@ -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 similarity index 100% rename from src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs rename to src/SMAPI/Framework/Serialisation/JsonHelper.cs diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs similarity index 81% rename from src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs rename to 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[]); } /// Reads the JSON representation of the object. @@ -68,7 +69,7 @@ namespace StardewModdingAPI.Framework.Serialisation } } - // manifest dependency + // manifest dependencies if (objectType == typeof(IManifestDependency[])) { List result = new List(); @@ -76,26 +77,30 @@ namespace StardewModdingAPI.Framework.Serialisation { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); -#if SMAPI_1_x - result.Add(new ManifestDependency(uniqueID, minVersion)); -#else bool required = obj.Value(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 result = new List(); - foreach (JToken child in JArray.Load(reader).Children()) + JToken token = JToken.Load(reader); + return new ModDataID(token.Value()); + } + + // mod compatibility records + if (objectType == typeof(ModCompatibility[])) + { + List result = new List(); + foreach (JProperty property in JObject.Load(reader).Properties()) { - result.Add(child is JValue value - ? new ModCompatibilityID(value.Value()) - : child.ToObject() - ); + string range = property.Name; + ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value(nameof(ModCompatibility.Status))); + string reasonPhrase = property.Value.Value(nameof(ModCompatibility.ReasonPhrase)); + + result.Add(new ModCompatibility(range, status, reasonPhrase)); } return result.ToArray(); } diff --git a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs rename to src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/SMAPI/Framework/Utilities/ContextHash.cs similarity index 97% rename from src/StardewModdingAPI/Framework/Utilities/ContextHash.cs rename to 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 /// The specified key is already added. 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 similarity index 100% rename from src/StardewModdingAPI/Framework/Utilities/Countdown.cs rename to 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 +{ + /// Provides methods for interacting with the SMAPI web API. + internal class WebApiClient + { + /********* + ** Properties + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) + { +#if !SMAPI_FOR_WINDOWS + baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } + + /// Get the latest SMAPI version. + /// The mod keys for which to fetch the latest version. + public IDictionary GetModInfo(params string[] modKeys) + { + return this.Post>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys) + ); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The body content type. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + /// The body content to post. + private TResult Post(string url, TBody content) + { + /*** + ** Note: avoid HttpClient for Mac compatibility. + ***/ + using (WebClient client = new WebClient()) + { + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response); + } + } + } +} diff --git a/src/StardewModdingAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs similarity index 100% rename from src/StardewModdingAPI/IAssetData.cs rename to src/SMAPI/IAssetData.cs diff --git a/src/StardewModdingAPI/IAssetDataForDictionary.cs b/src/SMAPI/IAssetDataForDictionary.cs similarity index 100% rename from src/StardewModdingAPI/IAssetDataForDictionary.cs rename to src/SMAPI/IAssetDataForDictionary.cs diff --git a/src/StardewModdingAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs similarity index 100% rename from src/StardewModdingAPI/IAssetDataForImage.cs rename to src/SMAPI/IAssetDataForImage.cs diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs similarity index 100% rename from src/StardewModdingAPI/IAssetEditor.cs rename to src/SMAPI/IAssetEditor.cs diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs similarity index 100% rename from src/StardewModdingAPI/IAssetInfo.cs rename to src/SMAPI/IAssetInfo.cs diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs similarity index 100% rename from src/StardewModdingAPI/IAssetLoader.cs rename to src/SMAPI/IAssetLoader.cs diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs similarity index 100% rename from src/StardewModdingAPI/ICommandHelper.cs rename to src/SMAPI/ICommandHelper.cs diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs similarity index 98% rename from src/StardewModdingAPI/IContentHelper.cs rename to 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 /// Interceptors which provide the initial versions of matching content assets. IList AssetLoaders { get; } /// Interceptors which edit matching content assets after they're loaded. IList AssetEditors { get; } -#endif /// The game's current locale code (like pt-BR). string CurrentLocale { get; } @@ -44,7 +42,6 @@ namespace StardewModdingAPI /// The is empty or contains invalid characters. string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); -#if !SMAPI_1_x /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. /// The asset key to invalidate in the content folder. /// The is empty or contains invalid characters. @@ -55,6 +52,5 @@ namespace StardewModdingAPI /// The asset type to remove from the cache. /// Returns whether any assets were invalidated. bool InvalidateCache(); -#endif } } diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs similarity index 96% rename from src/StardewModdingAPI/ICursorPosition.cs rename to 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 similarity index 83% rename from src/StardewModdingAPI/IManifest.cs rename to 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 /// The unique mod ID. string UniqueID { get; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. string EntryDll { get; } /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; set; } + /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs similarity index 90% rename from src/StardewModdingAPI/IManifestDependency.cs rename to 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 { /// A mod dependency listed in a mod manifest. public interface IManifestDependency @@ -12,9 +12,7 @@ /// The minimum required version (if any). ISemanticVersion MinimumVersion { get; } -#if !SMAPI_1_x /// Whether the dependency must be installed to use the mod. bool IsRequired { get; } -#endif } } diff --git a/src/StardewModdingAPI/IMod.cs b/src/SMAPI/IMod.cs similarity index 100% rename from src/StardewModdingAPI/IMod.cs rename to src/SMAPI/IMod.cs diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs similarity index 100% rename from src/StardewModdingAPI/IModHelper.cs rename to src/SMAPI/IModHelper.cs diff --git a/src/StardewModdingAPI/IModLinked.cs b/src/SMAPI/IModLinked.cs similarity index 100% rename from src/StardewModdingAPI/IModLinked.cs rename to src/SMAPI/IModLinked.cs diff --git a/src/StardewModdingAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs similarity index 100% rename from src/StardewModdingAPI/IModRegistry.cs rename to src/SMAPI/IModRegistry.cs diff --git a/src/StardewModdingAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs similarity index 100% rename from src/StardewModdingAPI/IMonitor.cs rename to src/SMAPI/IMonitor.cs diff --git a/src/StardewModdingAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs similarity index 100% rename from src/StardewModdingAPI/IPrivateField.cs rename to src/SMAPI/IPrivateField.cs diff --git a/src/StardewModdingAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs similarity index 100% rename from src/StardewModdingAPI/IPrivateMethod.cs rename to src/SMAPI/IPrivateMethod.cs diff --git a/src/StardewModdingAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs similarity index 100% rename from src/StardewModdingAPI/IPrivateProperty.cs rename to src/SMAPI/IPrivateProperty.cs diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs similarity index 100% rename from src/StardewModdingAPI/IReflectionHelper.cs rename to src/SMAPI/IReflectionHelper.cs diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs similarity index 96% rename from src/StardewModdingAPI/ISemanticVersion.cs rename to 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 { /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable -#if !SMAPI_1_x - , IEquatable -#endif + public interface ISemanticVersion : IComparable, IEquatable { /********* ** Accessors @@ -59,4 +56,4 @@ namespace StardewModdingAPI /// Get a string representation of the version. string ToString(); } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs similarity index 100% rename from src/StardewModdingAPI/ITranslationHelper.cs rename to src/SMAPI/ITranslationHelper.cs diff --git a/src/StardewModdingAPI/LogLevel.cs b/src/SMAPI/LogLevel.cs similarity index 100% rename from src/StardewModdingAPI/LogLevel.cs rename to src/SMAPI/LogLevel.cs diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs similarity index 96% rename from src/StardewModdingAPI/Metadata/CoreAssets.cs rename to 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> { + // from CraftingRecipe.InitShared + ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load>(key), + ["Data\\CookingRecipes"] = (content, key) => CraftingRecipe.cookingRecipes = content.Load>(key), + // from Game1.loadContent ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(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 +{ + /// Provides CIL instruction handlers which rewrite mods for compatibility and throw exceptions for incompatible code. + internal class InstructionMetadata + { + /********* + ** Public methods + *********/ + /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. + public IEnumerable GetHandlers() + { + return new IInstructionHandler[] + { + /**** + ** throw exception for incompatible code + ****/ + // changes in Stardew Valley 1.2 (with no rewriters) + new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible), + + // APIs removed in SMAPI 1.9 + new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Extensions", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Inheritance.SGame", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Inheritance.SObject", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.LogWriter", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Manifest", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Version", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), + + // APIs removed in SMAPI 2.0 + new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), + + /**** + ** detect code which may impact game stability + ****/ + new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch), + new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), + + /**** + ** rewrite CIL to fix incompatible code + ****/ + // crossplatform + new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), + + // Stardew Valley 1.2 + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), + new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), + new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), + + // SMAPI 1.9 + new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), + + // SMAPI 2.0 + new VirtualEntryCallRemover() // Mod.Entry changed from virtual to abstract in SMAPI 2.0, which breaks the few mods which called base.Entry() + }; + } + } +} diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs new file mode 100644 index 00000000..ee75ba54 --- /dev/null +++ b/src/SMAPI/Mod.cs @@ -0,0 +1,50 @@ +using System; + +namespace StardewModdingAPI +{ + /// The base class for a mod. + public abstract class Mod : IMod, IDisposable + { + /********* + ** Accessors + *********/ + /// Provides simplified APIs for writing mods. + public IModHelper Helper { get; internal set; } + + /// Writes messages to the console and log file. + public IMonitor Monitor { get; internal set; } + + /// The mod's manifest. + public IManifest ModManifest { get; internal set; } + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public abstract void Entry(IModHelper helper); + + /// Release or reset unmanaged resources. + public void Dispose() + { + (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it + this.Dispose(true); + GC.SuppressFinalize(this); + } + + + /********* + ** Private methods + *********/ + /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. + /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. + protected virtual void Dispose(bool disposing) { } + + /// Destruct the instance. + ~Mod() + { + this.Dispose(false); + } + } +} diff --git a/src/StardewModdingAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs similarity index 100% rename from src/StardewModdingAPI/PatchMode.cs rename to src/SMAPI/PatchMode.cs diff --git a/src/StardewModdingAPI/Program.cs b/src/SMAPI/Program.cs similarity index 83% rename from src/StardewModdingAPI/Program.cs rename to 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 - /// Get a monitor for legacy code which doesn't have one passed in. - [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 - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 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 deprecationWarnings = new List(); - foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) - { - // missing fields that will be required in SMAPI 2.0 - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - 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 ' for a command's usage", LogLevel.Info); this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); @@ -563,24 +480,125 @@ namespace StardewModdingAPI return !issuesFound; } - /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. - private void CheckForUpdateAsync() + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. + /// The mods to include in the update check (if eligible). + private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) return; new Thread(() => { + // create client + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + + // check SMAPI version try { - 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); + 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.\n{ex.GetLogSummary()}"); + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {ex.GetLogSummary()}"); + } + + // check mod versions + try + { + // log issues + if (this.Settings.VerboseLogging) + { + this.VerboseLog("Validating mod update keys..."); + foreach (IModMetadata mod in mods) + { + if (mod.Manifest == null) + this.VerboseLog($" {mod.DisplayName}: no manifest."); + else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) + this.VerboseLog($" {mod.DisplayName}: no update keys."); + } + } + + // prepare update keys + Dictionary modsByKey = + ( + from mod in mods + where mod.Manifest?.UpdateKeys != null + from key in mod.Manifest.UpdateKeys + select new { key, mod } + ) + .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.Select(p => p.mod).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + + // fetch results + this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); + var results = + ( + from entry in client.GetModInfo(modsByKey.Keys.ToArray()) + from mod in modsByKey[entry.Key] + orderby mod.DisplayName + select new { entry.Key, Mod = mod, Info = entry.Value } + ) + .ToArray(); + + // extract latest versions + IDictionary updatesByMod = new Dictionary(); + foreach (var result in results) + { + IModMetadata mod = result.Mod; + ModInfoModel info = result.Info; + + // handle error + if (info.Error != null) + { + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); + continue; + } + + // track update + ISemanticVersion localVersion = mod.DataRecord != null + ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) + : mod.Manifest.Version; + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null + ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) + : info.Version + ); + bool isUpdate = latestVersion.IsNewerThan(localVersion); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); + if (isUpdate) + { + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) + updatesByMod[mod] = info; + } + } + + // output + if (updatesByMod.Any()) + { + this.Monitor.Newline(); + this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) + this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); + } + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } }).Start(); } @@ -604,24 +622,16 @@ namespace StardewModdingAPI /// The mods to load. /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. -#if SMAPI_1_x - /// A list to populate with any deprecation warnings. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList 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 skippedMods = new Dictionary(); { 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[] 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(); } + + /// Log a message if verbose mode is enabled. + /// The message to log. + private void VerboseLog(string message) + { + if (this.Settings.VerboseLogging) + this.Monitor.Log(message, LogLevel.Trace); + } } } diff --git a/src/StardewModdingAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI/Properties/AssemblyInfo.cs rename to src/SMAPI/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/SMAPI/SButton.cs similarity index 99% rename from src/StardewModdingAPI/Utilities/SButton.cs rename to 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 { /// A unified button constant which includes all controller, keyboard, and mouse buttons. /// Derived from , , and . -#if SMAPI_1_x - internal -#else - public -#endif - enum SButton + public enum SButton { /// No valid key. None = 0, @@ -594,12 +589,7 @@ namespace StardewModdingAPI.Utilities } /// Provides extension methods for . -#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 similarity index 52% rename from src/StardewModdingAPI/SemanticVersion.cs rename to 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 *********/ - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - private static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + /// The underlying semantic version implementation. + private readonly SemanticVersionImpl Version; /********* ** Accessors *********/ /// The major version incremented for major API changes. - public int MajorVersion { get; } + public int MajorVersion => this.Version.Major; /// The minor version incremented for backwards-compatible changes. - public int MinorVersion { get; } + public int MinorVersion => this.Version.Minor; /// The patch version for backwards-compatible bug fixes. - public int PatchVersion { get; } + public int PatchVersion => this.Version.Patch; /// An optional build tag. - public string Build { get; } + public string Build => this.Version.Tag; /********* @@ -47,32 +40,14 @@ namespace StardewModdingAPI /// An optional build tag. [JsonConstructor] public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - { - this.MajorVersion = majorVersion; - this.MinorVersion = minorVersion; - this.PatchVersion = patchVersion; - this.Build = this.GetNormalisedTag(build); - } + : this(new SemanticVersionImpl(majorVersion, minorVersion, patchVersion, build)) { } /// Construct an instance. /// The semantic version string. /// The is null. /// The is not a valid semantic version. public SemanticVersion(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersion.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.MajorVersion = int.Parse(match.Groups["major"].Value); - this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } + : this(new SemanticVersionImpl(version)) { } /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. @@ -80,56 +55,7 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - if (other == null) - throw new ArgumentNullException(nameof(other)); - - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.MajorVersion != other.MajorVersion) - return this.MajorVersion.CompareTo(other.MajorVersion); - if (this.MinorVersion != other.MinorVersion) - return this.MinorVersion.CompareTo(other.MinorVersion); - if (this.PatchVersion != other.PatchVersion) - return this.PatchVersion.CompareTo(other.PatchVersion); - if (this.Build == other.Build) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Build); - bool otherIsStable = string.IsNullOrWhiteSpace(other.Build); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Build.Split('.', '-'); - string[] otherParts = other.Build.Split('.', '-'); - for (int i = 0; i < curParts.Length; i++) - { - // longer prerelease tag supercedes if otherwise equal - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); + return this.Version.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); } /// Get whether this version is older than the specified version. @@ -179,7 +105,6 @@ namespace StardewModdingAPI return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); } -#if !SMAPI_1_x /// Indicates whether the current object is equal to another object of the same type. /// true if the current object is equal to the parameter; otherwise, false. /// An object to compare with this object. @@ -187,21 +112,11 @@ namespace StardewModdingAPI { return other != null && this.CompareTo(other) == 0; } -#endif /// Get a string representation of the version. public override string ToString() { - // version - string result = this.PatchVersion != 0 - ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" - : $"{this.MajorVersion}.{this.MinorVersion}"; - - // tag - string tag = this.Build; - if (tag != null) - result += $"-{tag}"; - return result; + return this.Version.ToString(); } /// Parse a version string without throwing an exception if it fails. @@ -210,30 +125,26 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. 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 *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) + /// Construct an instance. + /// The underlying semantic version implementation. + 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 similarity index 85% rename from src/StardewModdingAPI/StardewModdingAPI.csproj rename to src/SMAPI/StardewModdingAPI.csproj index 8daf21b7..b8d5990e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -65,9 +65,8 @@ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll True - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll @@ -87,10 +86,26 @@ - + Properties\GlobalAssemblyInfo.cs - + + + + + + + + + + + + + + + + + @@ -104,10 +119,8 @@ - - @@ -115,26 +128,21 @@ - - - - - - + @@ -157,7 +165,7 @@ - + @@ -186,7 +194,7 @@ - + @@ -202,14 +210,13 @@ - - + - + @@ -222,7 +229,7 @@ - + @@ -257,12 +264,13 @@ false + - + {10db0676-9fc1-4771-a2c8-e2519f091e49} StardewModdingAPI.AssemblyRewriters - + \ No newline at end of file diff --git a/src/StardewModdingAPI/Translation.cs b/src/SMAPI/Translation.cs similarity index 100% rename from src/StardewModdingAPI/Translation.cs rename to src/SMAPI/Translation.cs diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs similarity index 87% rename from src/StardewModdingAPI/Utilities/SDate.cs rename to 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 /// The year. public int Year { get; } -#if !SMAPI_1_x /// The day of week. public DayOfWeek DayOfWeek { get; } -#endif /********* @@ -54,30 +52,12 @@ namespace StardewModdingAPI.Utilities /// The year. /// One of the arguments has an invalid value (like day 35). public SDate(int day, string season, int year) - { - // validate - if (season == null) - throw new ArgumentNullException(nameof(season)); - if (!this.Seasons.Contains(season)) - throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); - if (day < 1 || day > this.DaysInSeason) - throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); - if (year < 1) - throw new ArgumentException($"Invalid year '{year}', must be at least 1."); - - // initialise - this.Day = day; - this.Season = season; - this.Year = year; -#if !SMAPI_1_x - this.DayOfWeek = this.GetDayOfWeek(); -#endif - } + : this(day, season, year, allowDayZero: false) { } /// Get the current in-game date. public static SDate Now() { - return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year); + return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } /// Get a new date with the given number of days added. @@ -199,6 +179,42 @@ namespace StardewModdingAPI.Utilities /********* ** Private methods *********/ + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// Whether to allow 0 spring Y1 as a valid date. + /// One of the arguments has an invalid value (like day 35). + 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(); + } + + /// Get whether a date represents 0 spring Y1, which is the date during the in-game intro. + /// The day of month. + /// The season name. + /// The year. + private bool IsDayZero(int day, string season, int year) + { + return day == 0 && season == "spring" && year == 1; + } + /// Get the day of week for the current date. private DayOfWeek GetDayOfWeek() { diff --git a/src/StardewModdingAPI/icon.ico b/src/SMAPI/icon.ico similarity index 100% rename from src/StardewModdingAPI/icon.ico rename to src/SMAPI/icon.ico diff --git a/src/StardewModdingAPI/packages.config b/src/SMAPI/packages.config similarity index 58% rename from src/StardewModdingAPI/packages.config rename to src/SMAPI/packages.config index e5fa3c3a..98d742c7 100644 --- a/src/StardewModdingAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/StardewModdingAPI/steam_appid.txt b/src/SMAPI/steam_appid.txt similarity index 100% rename from src/StardewModdingAPI/steam_appid.txt rename to src/SMAPI/steam_appid.txt diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh similarity index 100% rename from src/StardewModdingAPI/unix-launcher.sh rename to 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 -{ - /// Rewrites CIL instructions for compatibility. - public interface IInstructionRewriter - { - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the rewriter matches. - string NounPhrase { get; } - - - /********* - ** Methods - *********/ - /// Rewrite a method definition for compatibility. - /// The module being rewritten. - /// The method definition to rewrite. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); - - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - 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 @@ - - - - \ 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 @@ - - - - - - - \ 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 -{ - /// A command that can be submitted through the SMAPI console to interact with SMAPI. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] - public class Command - { - /********* - ** Properties - *********/ - /// The commands registered with SMAPI. - private static readonly IDictionary LegacyCommands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - /// Manages console commands. - private static CommandManager CommandManager; - - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// Tracks the installed mods. - private static ModRegistry ModRegistry; - - - /********* - ** Accessors - *********/ - /// The event raised when this command is submitted through the console. - public event EventHandler CommandFired; - - /**** - ** Command - ****/ - /// The name of the command. - public string CommandName; - - /// A human-readable description of what the command does. - public string CommandDesc; - - /// A human-readable list of accepted arguments. - public string[] CommandArgs; - - /// The actual submitted argument values. - public string[] CalledArgs; - - - /********* - ** Public methods - *********/ - /**** - ** Command - ****/ - /// Injects types required for backwards compatibility. - /// Manages console commands. - /// Manages deprecation warnings. - /// Tracks the installed mods. - internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry) - { - Command.CommandManager = commandManager; - Command.DeprecationManager = deprecationManager; - Command.ModRegistry = modRegistry; - } - - /// Construct an instance. - /// The name of the command. - /// A human-readable description of what the command does. - /// A human-readable list of accepted arguments. - 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; - } - - /// Trigger this command. - 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 - ****/ - /// Parse a command string and invoke it if valid. - /// The command to run, including the command name and any arguments. - /// Encapsulates monitoring and logging. - public static void CallCommand(string input, IMonitor monitor) - { - Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.PendingRemoval); - Command.CommandManager.Trigger(input); - } - - /// Register a command with SMAPI. - /// The name of the command. - /// A human-readable description of what the command does. - /// A human-readable list of accepted arguments. - 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() ?? ""; - 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; - } - - /// Find a command with the given name. - /// The command name to find. - 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 - *********/ - /// Trigger this command. - /// The command name. - /// The command arguments. - 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 -{ - /// A dynamic configuration class for a mod. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public abstract class Config - { - /********* - ** Properties - *********/ - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - - /********* - ** Accessors - *********/ - /// The full path to the configuration file. - [JsonIgnore] - public virtual string ConfigLocation { get; protected internal set; } - - /// The directory path containing the configuration file. - [JsonIgnore] - public virtual string ConfigDir => Path.GetDirectoryName(this.ConfigLocation); - - - /********* - ** Public methods - *********/ - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - Config.DeprecationManager = deprecationManager; - } - - /// Construct an instance of the config class. - /// The config class type. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual Config Instance() where T : Config => Activator.CreateInstance(); - - /// Load the config from the JSON file, saving it to disk if needed. - /// The config class type. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T LoadConfig() 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(); - config.ConfigLocation = this.ConfigLocation; - returnValue = config; - } - else - { - try - { - T config = JsonConvert.DeserializeObject(File.ReadAllText(this.ConfigLocation)); - config.ConfigLocation = this.ConfigLocation; - returnValue = config.UpdateConfig(); - } - catch (Exception ex) - { - Log.Error($"Invalid JSON ({this.GetType().Name}): {this.ConfigLocation} \n{ex}"); - return this.GenerateDefaultConfig(); - } - } - - returnValue.WriteConfig(); - return returnValue; - } - - /// Get the default config values. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T GenerateDefaultConfig() where T : Config - { - return null; - } - - /// Get the current configuration with missing values defaulted. - /// The config class type. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T UpdateConfig() where T : Config - { - try - { - // get default + user config - JObject defaultConfig = JObject.FromObject(this.Instance().GenerateDefaultConfig()); - JObject currentConfig = JObject.FromObject(this); - defaultConfig.Merge(currentConfig, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); - - // cast json object to config - T config = defaultConfig.ToObject(); - - // 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 - *********/ - /// Construct an instance. - 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 - } - } - - /// Provides extension methods for classes. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static class ConfigExtensions - { - /// 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! - /// The config class type. - /// The base configuration to initialise. - /// The base configuration file path. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static T InitializeConfig(this T baseConfig, string configLocation) where T : Config - { - if (baseConfig == null) - baseConfig = Activator.CreateInstance(); - - 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(); - } - - /// Writes the configuration to the JSON file. - /// The config class type. - /// The base configuration to initialise. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static void WriteConfig(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); - } - - /// 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! - /// The config class type. - /// The base configuration to initialise. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static T ReloadConfig(this T baseConfig) where T : Config - { - return baseConfig.LoadConfig(); - } - } -} -#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 -{ - /// Event arguments for a event. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] - public class EventArgsCommand : EventArgs - { - /********* - ** Accessors - *********/ - /// The triggered command. - public Command Command { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The triggered command. - 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 -{ - /// Event arguments for a event. - public class EventArgsFarmerChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous player character. - public SFarmer NewFarmer { get; } - - /// The new player character. - public SFarmer PriorFarmer { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous player character. - /// The new player character. - 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 -{ - /// Event arguments for a event. - public class EventArgsLoadedGameChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// Whether the save has been loaded. This is always true. - public bool LoadedGame { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Whether the save has been loaded. This is always true. - 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 -{ - /// Event arguments for a event. - public class EventArgsNewDay : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous day value. - public int PreviousDay { get; } - - /// The current day value. - public int CurrentDay { get; } - - /// Whether the game just started the transition (true) or finished it (false). - public bool IsNewDay { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous day value. - /// The current day value. - /// Whether the game just started the transition (true) or finished it (false). - 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 -{ - /// Event arguments for a string field that changed value. - public class EventArgsStringChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous value. - public string NewString { get; } - - /// The current value. - public string PriorString { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous value. - /// The current value. - 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 -{ - /// Events raised when the game changes state. - public static class GameEvents - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _Initialize; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _LoadContent; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _GameLoaded; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _FirstUpdateTick; -#endif - - - /********* - ** Events - *********/ - /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . - internal static event EventHandler InitializeInternal; - - /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. - internal static event EventHandler GameLoadedInternal; - -#if SMAPI_1_x - /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . - [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; - } - - /// Raised before XNA loads or reloads graphics resources. Called during . - [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; - } - - /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. - [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; - } - - /// Raised during the first game update tick. - [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 - - /// Raised when the game updates its state (≈60 times per second). - public static event EventHandler UpdateTick; - - /// Raised every other tick (≈30 times per second). - public static event EventHandler SecondUpdateTick; - - /// Raised every fourth tick (≈15 times per second). - public static event EventHandler FourthUpdateTick; - - /// Raised every eighth tick (≈8 times per second). - public static event EventHandler EighthUpdateTick; - - /// Raised every 15th tick (≈4 times per second). - public static event EventHandler QuarterSecondTick; - - /// Raised every 30th tick (≈twice per second). - public static event EventHandler HalfSecondTick; - - /// Raised every 60th tick (≈once per second). - public static event EventHandler OneSecondTick; - - - /********* - ** Internal methods - *********/ -#if SMAPI_1_x - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - GameEvents.DeprecationManager = deprecationManager; - } -#endif - - /// Raise an event. - /// Encapsulates logging and monitoring. - 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 - /// Raise a event. - /// Encapsulates logging and monitoring. - internal static void InvokeLoadContent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); - } -#endif - - /// Raise a event. - /// Encapsulates monitoring and logging. - 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 - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFirstUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents._FirstUpdateTick?.GetInvocationList()); - } -#endif - - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeSecondUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFourthUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeEighthUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeQuarterSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeHalfSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeOneSecondTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); - } - } -} diff --git a/src/StardewModdingAPI/Events/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 -{ - /// Events raised when the player data changes. - public static class PlayerEvents - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _LoadedGame; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _FarmerChanged; -#endif - - - /********* - ** Events - *********/ -#if SMAPI_1_x - /// Raised after the player loads a saved game. - [Obsolete("Use " + nameof(SaveEvents) + "." + nameof(SaveEvents.AfterLoad) + " instead")] - public static event EventHandler LoadedGame - { - add - { - PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", "1.6", DeprecationLevel.PendingRemoval); - PlayerEvents._LoadedGame += value; - } - remove => PlayerEvents._LoadedGame -= value; - } - - /// Raised after the game assigns a new player character. This happens just before ; it's unclear how this would happen any other time. - [Obsolete("should no longer be used")] - public static event EventHandler FarmerChanged - { - add - { - PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", "1.6", DeprecationLevel.PendingRemoval); - PlayerEvents._FarmerChanged += value; - } - remove => PlayerEvents._FarmerChanged -= value; - } -#endif - - /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public static event EventHandler InventoryChanged; - - /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. - public static event EventHandler LeveledUp; - - - /********* - ** Internal methods - *********/ -#if SMAPI_1_x - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - PlayerEvents.DeprecationManager = deprecationManager; - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// Whether the save has been loaded. This is always true. - internal static void InvokeLoadedGame(IMonitor monitor, EventArgsLoadedGameChanged loaded) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", PlayerEvents._LoadedGame?.GetInvocationList(), null, loaded); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous player character. - /// The new player character. - 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 - - /// Raise an event. - /// Encapsulates monitoring and logging. - /// The player's inventory. - /// The inventory changes. - internal static void InvokeInventoryChanged(IMonitor monitor, List inventory, IEnumerable changedItems) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); - } - - /// Rase a event. - /// Encapsulates monitoring and logging. - /// The player skill that leveled up. - /// The new skill level. - internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel)); - } - } -} diff --git a/src/StardewModdingAPI/Events/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 -{ - /// Events raised when the in-game date or time changes. - public static class TimeEvents - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _OnNewDay; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _DayOfMonthChanged; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _SeasonOfYearChanged; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _YearOfGameChanged; -#endif - - - /********* - ** Events - *********/ - /// Raised after the game begins a new day, including when loading a save. - public static event EventHandler AfterDayStarted; - - /// Raised after the in-game clock changes. - public static event EventHandler TimeOfDayChanged; - -#if SMAPI_1_x - /// Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use instead. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler DayOfMonthChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._DayOfMonthChanged += value; - } - remove => TimeEvents._DayOfMonthChanged -= value; - } - - /// Raised after the year value changes. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler YearOfGameChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._YearOfGameChanged += value; - } - remove => TimeEvents._YearOfGameChanged -= value; - } - - /// Raised after the season value changes. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler SeasonOfYearChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._SeasonOfYearChanged += value; - } - remove => TimeEvents._SeasonOfYearChanged -= value; - } - - /// 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. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler 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 - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - TimeEvents.DeprecationManager = deprecationManager; - } -#endif - - /// Raise an event. - /// Encapsulates monitoring and logging. - internal static void InvokeAfterDayStarted(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous time in military time format (e.g. 6:00pm is 1800). - /// The current time in military time format (e.g. 6:10pm is 1810). - internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); - } - -#if SMAPI_1_x - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous day value. - /// The current day value. - 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)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous year value. - /// The current year value. - 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)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous season name. - /// The current season name. - 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)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous day value. - /// The current day value. - /// Whether the game just started the transition (true) or finished it (false). - 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 -{ - /// Metadata about for a mod that should never be loaded. - internal class DisabledMod - { - /********* - ** Accessors - *********/ - /**** - ** From config - ****/ - /// The unique mod IDs. - public string[] ID { get; set; } - - /// The mod name. - public string Name { get; set; } - - /// The reason phrase to show in the warning, or null to use the default value. - /// "this mod is no longer supported or used" - 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 -{ - /// Metadata about a GitHub release tag. - internal class GitRelease - { - /********* - ** Accessors - *********/ - /// The display name. - [JsonProperty("name")] - public string Name { get; set; } - - /// The semantic version string. - [JsonProperty("tag_name")] - public string Tag { get; set; } - } -} \ 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 -{ - /// Metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - internal class ModCompatibility - { - /********* - ** Accessors - *********/ - /// The unique mod IDs. - [JsonConverter(typeof(SFieldConverter))] - public ModCompatibilityID[] ID { get; set; } - - /// The mod name. - public string Name { get; set; } - - /// The oldest incompatible mod version, or null for all past versions. - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion LowerVersion { get; set; } - - /// The most recent incompatible mod version. - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion UpperVersion { get; set; } - - /// A label to show to the user instead of , when the manifest version differs from the user-facing version. - public string UpperVersionLabel { get; set; } - - /// The URLs the user can check for a newer version. - public string[] UpdateUrls { get; set; } - - /// The reason phrase to show in the warning, or null to use the default value. - /// "this version is incompatible with the latest version of the game" - public string ReasonPhrase { get; set; } - - /// Indicates how SMAPI should consider the mod. - 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 -{ - /// Uniquely identifies a mod for compatibility checks. - internal class ModCompatibilityID - { - /********* - ** Accessors - *********/ - /// The unique mod ID. - public string ID { get; set; } - - /// The mod name to disambiguate non-unique IDs, or null to ignore the mod name. - public string Name { get; set; } - - /// The author name to disambiguate non-unique IDs, or null to ignore the author. - public string Author { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ModCompatibilityID() { } - - /// Construct an instance. - /// The mod ID or a JSON string matching the fields. - 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; - } - - /// Get whether this ID matches a given mod manifest. - /// The mod's unique ID, or a substitute ID if it isn't set in the manifest. - /// The manifest to check. - public bool Matches(string id, IManifest manifest) - { - return - this.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 -{ - /// Indicates how SMAPI should consider a mod. - internal enum ModCompatibilityType - { - /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken = 0, - - /// Assume the mod is compatible, even if SMAPI detects incompatible code. - 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 -{ - /// The SMAPI configuration settings. - internal class SConfig - { - /******** - ** Accessors - ********/ - /// Whether to enable development features. - public bool DeveloperMode { get; set; } - - /// Whether to check if a newer version of SMAPI is available on startup. - public bool CheckForUpdates { get; set; } = true; - - /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } = false; - - /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. - public ModCompatibility[] ModCompatibility { get; set; } - - /// A list of mods which should be considered obsolete and not loaded. - 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 -{ - /// Provides utility methods for mod updates. - internal class UpdateHelper - { - /********* - ** Public methods - *********/ - /// Get the latest release from a GitHub repository. - /// The name of the repository from which to fetch releases (like "cjsu/SMAPI"). - public static async Task 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(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 -{ - /// A singleton which logs messages to the SMAPI console and log file. - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Monitor))] - public static class Log - { - /********* - ** Properties - *********/ - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The underlying logger. - private static Monitor Monitor; - - /// Tracks the installed mods. - private static ModRegistry ModRegistry; - - - /********* - ** Public methods - *********/ - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - /// The underlying logger. - /// Tracks the installed mods. - internal static void Shim(DeprecationManager deprecationManager, Monitor monitor, ModRegistry modRegistry) - { - Log.DeprecationManager = deprecationManager; - Log.Monitor = monitor; - Log.ModRegistry = modRegistry; - } - - /**** - ** Exceptions - ****/ - /// Log an exception event. - /// The event sender. - /// The event arguments. - public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - Log.WarnDeprecated(); - Log.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - } - - /// Log a thread exception event. - /// The event sender. - /// The event arguments. - public static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) - { - Log.WarnDeprecated(); - Log.Monitor.Log($"Critical thread exception: {e.Exception}", LogLevel.Error); - } - - /**** - ** Synchronous logging - ****/ - /// Synchronously log a message to the console. NOTE: synchronous logging is discouraged; use asynchronous methods instead. - /// The message to log. - /// The message color. - public static void SyncColour(object message, ConsoleColor color) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), color); - } - - /**** - ** Asynchronous logging - ****/ - /// Asynchronously log a message to the console with the specified color. - /// The message to log. - /// The message color. - public static void AsyncColour(object message, ConsoleColor color) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), color); - } - - /// Asynchronously log a message to the console. - /// The message to log. - public static void Async(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Gray); - } - - /// Asynchronously log a red message to the console. - /// The message to log. - public static void AsyncR(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Red); - } - - /// Asynchronously log an orange message to the console. - /// The message to log. - public static void AsyncO(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.DarkYellow); - } - - /// Asynchronously log a yellow message to the console. - /// The message to log. - public static void AsyncY(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Yellow); - } - - /// Asynchronously log a green message to the console. - /// The message to log. - public static void AsyncG(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Green); - } - - /// Asynchronously log a cyan message to the console. - /// The message to log. - public static void AsyncC(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Cyan); - } - - /// Asynchronously log a magenta message to the console. - /// The message to log. - public static void AsyncM(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Magenta); - } - - /// Asynchronously log a warning to the console. - /// The message to log. - public static void Warning(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Yellow, LogLevel.Warn); - } - - /// Asynchronously log an error to the console. - /// The message to log. - public static void Error(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Red, LogLevel.Error); - } - - /// Asynchronously log a success message to the console. - /// The message to log. - public static void Success(object message) - { - Log.WarnDeprecated(); - Log.AsyncG(message); - } - - /// Asynchronously log an info message to the console. - /// The message to log. - public static void Info(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.White, LogLevel.Info); - } - - /// Asynchronously log an info message to the console. - /// The message to log. - public static void Out(object message) - { - Log.WarnDeprecated(); - Log.Async($"[OUT] {message}"); - } - - /// Asynchronously log a debug message to the console. - /// The message to log. - public static void Debug(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.DarkGray); - } - - /// Asynchronously log a message to the file that's not shown in the console. - /// The message to log. - internal static void LogToFile(string message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message, ConsoleColor.DarkGray, LogLevel.Trace); - } - - /// Obsolete. - public static void LogValueNotSpecified() - { - Log.WarnDeprecated(); - Log.AsyncR(" must be specified"); - } - - /// Obsolete. - public static void LogObjectValueNotSpecified() - { - Log.WarnDeprecated(); - Log.AsyncR(" and must be specified"); - } - - /// Obsolete. - public static void LogValueInvalid() - { - Log.WarnDeprecated(); - Log.AsyncR(" is invalid"); - } - - /// Obsolete. - public static void LogObjectInvalid() - { - Log.WarnDeprecated(); - Log.AsyncR(" is invalid"); - } - - /// Obsolete. - public static void LogValueNotInt32() - { - Log.WarnDeprecated(); - Log.AsyncR(" must be a whole number (Int32)"); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - /// Obsolete. - [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); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Success(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Success(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Verbose(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Out(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Comment(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.AsyncC(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Info(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Info(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Error(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Error(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [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 - *********/ - /// Raise a deprecation warning. - private static void WarnDeprecated() - { - Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.PendingRemoval); - } - - /// Get the name of the mod logging a message from the stack. - private static string GetModName() - { - return Log.ModRegistry.GetModFromStack() ?? ""; - } - } -} -#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 -{ - /// The base class for a mod. - public class Mod : IMod, IDisposable - { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - - /// The backing field for . - private string _pathOnDisk; -#endif - - - /********* - ** Accessors - *********/ - /// Provides simplified APIs for writing mods. - public IModHelper Helper { get; internal set; } - - /// Writes messages to the console and log file. - public IMonitor Monitor { get; internal set; } - - /// The mod's manifest. - public IManifest ModManifest { get; internal set; } - -#if SMAPI_1_x - /// The full path to the mod's directory on the disk. - [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; } - } - - /// The full path to the mod's config.json file on the disk. - [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"); - } - } - - /// The full path to the per-save configs folder (if is true). - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")] - public string PerSaveConfigFolder => this.GetPerSaveConfigFolder(); - - /// The full path to the per-save configuration file for the current save (if is true). - [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 - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - Mod.DeprecationManager = deprecationManager; - } - - /// The mod entry point, called after the mod is first loaded. - [Obsolete("This overload is obsolete since SMAPI 1.0.")] - public virtual void Entry(params object[] objects) { } -#endif - - /// The mod entry point, called after the mod is first loaded. - /// Provides simplified APIs for writing mods. - public virtual void Entry(IModHelper helper) { } - - /// Release or reset unmanaged resources. - public void Dispose() - { - (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it - this.Dispose(true); - GC.SuppressFinalize(this); - } - - - /********* - ** Private methods - *********/ -#if SMAPI_1_x - /// Get the full path to the per-save configuration file for the current save (if is true). - [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 - - /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. - /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. - protected virtual void Dispose(bool disposing) { } - - /// Destruct the instance. - ~Mod() - { - this.Dispose(false); - } - } -} diff --git a/src/StardewModdingAPI/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 -{ - /// A command which shows the load screen. - internal class LoadCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public LoadCommand() - : base("load", "Shows the load screen.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - 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 -{ - /// A command which saves the game. - internal class SaveCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SaveCommand() - : base("save", "Saves the game? Doesn't seem to work.") { } - - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - 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 @@ x86 - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - False + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll @@ -48,7 +47,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -69,8 +68,6 @@ - - @@ -87,7 +84,7 @@ - + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI False @@ -100,5 +97,5 @@ - + \ 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 @@  - + \ No newline at end of file