summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-04-05 14:55:46 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-04-05 14:55:46 -0400
commitdbb9bd84306830456032778fc11fb9a34dd140c7 (patch)
tree0219cab065bfffbbbb9b048b6a30044f510be2c5
parent9c9833c9086b758589dafee10243e3bf47e12d73 (diff)
parent4675da0600edf6781cd740549ad0a175b606fc1e (diff)
downloadSMAPI-dbb9bd84306830456032778fc11fb9a34dd140c7.tar.gz
SMAPI-dbb9bd84306830456032778fc11fb9a34dd140c7.tar.bz2
SMAPI-dbb9bd84306830456032778fc11fb9a34dd140c7.zip
Merge branch 'develop-1.9' into stable
-rw-r--r--README.md57
-rw-r--r--release-notes.md57
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs83
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs83
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs83
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs135
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs38
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs21
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs35
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs)98
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs53
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs54
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs93
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs30
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs157
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs (renamed from src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs)17
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj20
-rw-r--r--src/StardewModdingAPI.Installer/InteractiveInstaller.cs253
-rw-r--r--src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj2
-rw-r--r--src/StardewModdingAPI.sln4
-rw-r--r--src/StardewModdingAPI.sln.DotSettings1
-rw-r--r--src/StardewModdingAPI/Advanced/ConfigFile.cs35
-rw-r--r--src/StardewModdingAPI/Advanced/IConfigFile.cs25
-rw-r--r--src/StardewModdingAPI/Command.cs109
-rw-r--r--src/StardewModdingAPI/Config.cs18
-rw-r--r--src/StardewModdingAPI/Constants.cs144
-rw-r--r--src/StardewModdingAPI/Entities/SPlayer.cs59
-rw-r--r--src/StardewModdingAPI/Events/ChangeType.cs (renamed from src/StardewModdingAPI/Inheritance/ChangeType.cs)2
-rw-r--r--src/StardewModdingAPI/Events/ContentEvents.cs87
-rw-r--r--src/StardewModdingAPI/Events/EventArgsCommand.cs1
-rw-r--r--src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs8
-rw-r--r--src/StardewModdingAPI/Events/EventArgsIntChanged.cs5
-rw-r--r--src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs1
-rw-r--r--src/StardewModdingAPI/Events/EventArgsValueChanged.cs31
-rw-r--r--src/StardewModdingAPI/Events/GraphicsEvents.cs88
-rw-r--r--src/StardewModdingAPI/Events/ItemStackChange.cs (renamed from src/StardewModdingAPI/Inheritance/ItemStackChange.cs)4
-rw-r--r--src/StardewModdingAPI/Events/PlayerEvents.cs22
-rw-r--r--src/StardewModdingAPI/Events/SaveEvents.cs10
-rw-r--r--src/StardewModdingAPI/Events/TimeEvents.cs30
-rw-r--r--src/StardewModdingAPI/Extensions.cs194
-rw-r--r--src/StardewModdingAPI/Framework/AssemblyLoader.cs134
-rw-r--r--src/StardewModdingAPI/Framework/Command.cs40
-rw-r--r--src/StardewModdingAPI/Framework/CommandHelper.cs53
-rw-r--r--src/StardewModdingAPI/Framework/CommandManager.cs117
-rw-r--r--src/StardewModdingAPI/Framework/Content/ContentEventData.cs111
-rw-r--r--src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs47
-rw-r--r--src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs45
-rw-r--r--src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs70
-rw-r--r--src/StardewModdingAPI/Framework/DeprecationManager.cs2
-rw-r--r--src/StardewModdingAPI/Framework/InternalExtensions.cs16
-rw-r--r--src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs86
-rw-r--r--src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs79
-rw-r--r--src/StardewModdingAPI/Framework/Logging/LogFileManager.cs (renamed from src/StardewModdingAPI/Framework/LogFileManager.cs)8
-rw-r--r--src/StardewModdingAPI/Framework/Manifest.cs39
-rw-r--r--src/StardewModdingAPI/Framework/ModHelper.cs (renamed from src/StardewModdingAPI/ModHelper.cs)72
-rw-r--r--src/StardewModdingAPI/Framework/ModRegistry.cs30
-rw-r--r--src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs57
-rw-r--r--src/StardewModdingAPI/Framework/Models/ModCompatibility.cs65
-rw-r--r--src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs12
-rw-r--r--src/StardewModdingAPI/Framework/Models/SConfig.cs (renamed from src/StardewModdingAPI/Framework/Models/UserSettings.cs)11
-rw-r--r--src/StardewModdingAPI/Framework/Monitor.cs71
-rw-r--r--src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs93
-rw-r--r--src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs57
-rw-r--r--src/StardewModdingAPI/Framework/RequestExitDelegate.cs7
-rw-r--r--src/StardewModdingAPI/Framework/SContentManager.cs135
-rw-r--r--src/StardewModdingAPI/Framework/SGame.cs (renamed from src/StardewModdingAPI/Inheritance/SGame.cs)483
-rw-r--r--src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs69
-rw-r--r--src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs37
-rw-r--r--src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs51
-rw-r--r--src/StardewModdingAPI/ICommandHelper.cs26
-rw-r--r--src/StardewModdingAPI/IContentEventData.cs38
-rw-r--r--src/StardewModdingAPI/IContentEventHelper.cs26
-rw-r--r--src/StardewModdingAPI/IContentEventHelperForDictionary.cs26
-rw-r--r--src/StardewModdingAPI/IContentEventHelperForImage.cs23
-rw-r--r--src/StardewModdingAPI/IModHelper.cs3
-rw-r--r--src/StardewModdingAPI/IPrivateProperty.cs26
-rw-r--r--src/StardewModdingAPI/IReflectionHelper.cs14
-rw-r--r--src/StardewModdingAPI/Inheritance/SObject.cs249
-rw-r--r--src/StardewModdingAPI/Log.cs22
-rw-r--r--src/StardewModdingAPI/LogInfo.cs44
-rw-r--r--src/StardewModdingAPI/LogWriter.cs66
-rw-r--r--src/StardewModdingAPI/Manifest.cs70
-rw-r--r--src/StardewModdingAPI/Mod.cs50
-rw-r--r--src/StardewModdingAPI/PatchMode.cs12
-rw-r--r--src/StardewModdingAPI/Program.cs524
-rw-r--r--src/StardewModdingAPI/SemanticVersion.cs23
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.config.json198
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.csproj82
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.data.json50
-rw-r--r--src/StardewModdingAPI/Version.cs121
-rw-r--r--src/TrainerMod/Framework/Extensions.cs25
-rw-r--r--src/TrainerMod/TrainerMod.cs1145
-rw-r--r--src/TrainerMod/TrainerMod.csproj3
-rw-r--r--src/TrainerMod/manifest.json12
-rw-r--r--src/prepare-install-package.targets4
95 files changed, 4519 insertions, 2707 deletions
diff --git a/README.md b/README.md
index b1062077..74388144 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,7 @@
![](docs/imgs/SMAPI.png)
-**SMAPI** is an [open-source](LICENSE) modding API for [Stardew Valley](http://stardewvalley.net/).
-It takes care of loading mods into the game context, and exposes events they can use to interact
-with the game. It's safely installed alongside the game's executable, and doesn't change any of
-your game files.
-
## Contents
+* [What is SMAPI?](#what-is-smapi)
* **[For players](#for-players)**
* **[For mod developers](#for-mod-developers)**
* [For SMAPI developers](#for-smapi-developers)
@@ -16,6 +12,34 @@ your game files.
* [Configuration file](#configuration-file)
* [Command-line arguments](#command-line-arguments)
+## What is SMAPI?
+**SMAPI** is an [open-source](LICENSE) modding API for [Stardew Valley](http://stardewvalley.net/)
+that lets you play the game with mods. It's safely installed alongside the game's executable, and
+doesn't change any of your game files. It serves five main purposes:
+
+1. **Load mods into the game.**
+ _SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't
+ possible without SMAPI to load them.)_
+
+2. **Provide APIs and events for mods.**
+ _SMAPI provides low-level APIs and events which let mods interact with the game in ways they
+ otherwise couldn't._
+
+3. **Rewrite mods for crossplatform compatibility.**
+ _SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows
+ without the mods needing to handle differences between the Linux/Mac and Windows versions of the
+ game._
+
+4. **Rewrite mods to update them.**
+ _SMAPI detects when a mod accesses part of the game that changed in a recent update which
+ affects many mods, and rewrites the mod so it's compatible._
+
+5. **Intercept errors.**
+ _SMAPI intercepts errors that happen in the game, displays the error details in the console
+ window, and in most cases automatically recovers the game. This prevents mods from accidentally
+ crashing the game, and makes it possible to troubleshoot errors in the game itself that would
+ otherwise show a generic 'program has stopped working' type of message._
+
## For players
* [How to install SMAPI & use mods](http://canimod.com/guides/using-mods#installing-smapi)
* [Release notes](release-notes.md#release-notes)
@@ -26,7 +50,7 @@ your game files.
* [How to develop mods](http://canimod.com/guides/creating-a-smapi-mod)
* [How to update mods](http://canimod.com/guides/updating-a-smapi-mod)
* [Release notes](release-notes.md#release-notes)
-* [SMAPI/Farmhand Discord](https://discordapp.com/invite/0t3fh2xhHVc6Vdyx) (chat with SMAPI developers)
+* [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
@@ -81,12 +105,10 @@ folder containing `src`).
Mono/
Mods/*
Mono.Cecil.dll
- Mono.Cecil.Rocks.dll
Newtonsoft.Json.dll
StardewModdingAPI
StardewModdingAPI.AssemblyRewriters.dll
StardewModdingAPI.config.json
- StardewModdingAPI.data.json
StardewModdingAPI.exe
StardewModdingAPI.exe.mdb
steam_appid.txt
@@ -95,11 +117,9 @@ folder containing `src`).
Windows/
Mods/*
Mono.Cecil.dll
- Mono.Cecil.Rocks.dll
Newtonsoft.Json.dll
StardewModdingAPI.AssemblyRewriters.dll
StardewModdingAPI.config.json
- StardewModdingAPI.data.json
StardewModdingAPI.exe
StardewModdingAPI.pdb
StardewModdingAPI.xml
@@ -107,27 +127,28 @@ folder containing `src`).
```
4. Open a terminal in the `SMAPI <version>` folder and run `chmod 755 internal/Mono/StardewModdingAPI`.
5. Copy & paste the `SMAPI <version>` folder as `SMAPI <version> for developers`.
- 6. In the `SMAPI <version>` folder, delete the following files:
- * `internal/Mono/StardewModdingAPI.config.json`
- * `internal/Windows/StardewModdingAPI.config.json`
- * `internal/Windows/StardewModdingAPI.xml`
+ 6. In the `SMAPI <version>` folder...
+ * edit `internal/Mono/StardewModdingAPI.config.json` and
+ `internal/Windows/StardewModdingAPI.config.json` to disable developer mode;
+ * delete `internal/Windows/StardewModdingAPI.xml`.
7. Compress the two folders into `SMAPI <version>.zip` and `SMAPI <version> for developers.zip`.
## Advanced usage
### Configuration file
You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your
-game folder. If it's missing, it'll be generated automatically next time SMAPI runs. It contains
-these fields:
+game folder. It contains these fields:
field | purpose
----- | -------
`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console.
`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.
+`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. Changing this field is not recommended and may destabilise your game.
### Command-line arguments
-SMAPI recognises the following command-line arguments. These are intended for internal use and may
-change without warning.
+SMAPI recognises the following command-line arguments. These are intended for internal use or
+testing and may change without warning.
argument | purpose
-------- | -------
+`--log path "path"` | The relative or absolute path of the log file SMAPI should write.
`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.)
diff --git a/release-notes.md b/release-notes.md
index 1ff868a4..a5ef4a0b 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -1,5 +1,62 @@
# Release notes
+<!--
+## 2.0
+See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...2.0).
+
+For mod developers:
+* Added `ContentEvents.AssetLoading` event with a helper which lets you intercept the XNB content
+ load, and dynamically adjust or replace the content being loaded (including support for patching
+ images).
+
+## 1.10
+See [log](https://github.com/Pathoschild/SMAPI/compare/1.9...1.10).
+* Updated for Stardew Valley 1.2.
+* SMAPI now rewrites many mods for compatibility with game updates, but some mods will need an update.
+-->
+
+## 1.9
+See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9).
+
+For players:
+* SMAPI now detects incompatible mods and disables them before they cause problems.
+* SMAPI now allows mods nested into an otherwise empty parent folder (like `Mods\ModName-1.0\ModName\manifest.json`), since that's a common default behaviour when unpacking mods.
+* The installer now detects if you need to update .NET Framework before installing SMAPI.
+* The installer now detects if you need to run the game at least once (to let it perform first-time setup) before installing SMAPI.
+* The installer on Linux now finds games installed to `~/.steam/steam/steamapps/common/Stardew Valley` too.
+* The installer now removes old SMAPI logs to prevent confusion.
+* The console now has simpler error messages.
+* The console now has improved command handling & feedback.
+* The console no longer shows the game's debug output (unless you use a _SMAPI for developers_ build).
+* Fixed the game-needs-an-update error not pausing before exit.
+* Fixed installer errors for some players when deleting files.
+* Fixed installer not ignoring potential game folders that don't contain a Stardew Valley exe.
+* Fixed installer not recognising Linux/Mac paths starting with `~/` or containing an escaped space.
+* Fixed TrainerMod letting you add invalid items which may crash the game.
+* Fixed TrainerMod's `world_downminelevel` command not working.
+* Fixed rare issue where mod dependencies would override SMAPI dependencies and cause unpredictable bugs.
+* Fixed errors in mods' console command handlers crashing the game.
+
+For mod developers:
+* Added a simpler API for console commands (see `helper.ConsoleCommands`).
+* Added `TimeEvents.AfterDayStarted` event triggered when a day starts. This happens no matter how the day started (including new game, loaded save, or player went to bed).
+* Added `ContentEvents.AfterLocaleChanged` event triggered when the player changes the content language (for the upcoming Stardew Valley 1.2).
+* Added `SaveEvents.AfterReturnToTitle` event triggered when the player returns to the title screen (for the upcoming Stardew Valley 1.2).
+* Added `helper.Reflection.GetPrivateProperty` method.
+* Added a `--log-path` argument to specify the SMAPI log path during testing.
+* SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings automatically, so mods no longer need to add a `StringEnumConverter` themselves for those.
+* The SMAPI log now has a simpler filename.
+* The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available.
+* The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing.
+* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised.
+* Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly.
+* Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)),
+ and all _notice_-level deprecations have been increased to _info_.
+* Removed the experimental `IConfigFile`.
+
+For SMAPI developers:
+* Added support for debugging SMAPI on Linux/Mac if supported by the editor.
+
## 1.8
See [log](https://github.com/Pathoschild/SMAPI/compare/1.7...1.8).
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs
new file mode 100644
index 00000000..c0051469
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs
@@ -0,0 +1,83 @@
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters.Finders
+{
+ /// <summary>Finds incompatible CIL instructions that reference a given event and throws an <see cref="IncompatibleInstructionException"/>.</summary>
+ public class EventFinder : IInstructionRewriter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The full type name for which to find references.</summary>
+ private readonly string FullTypeName;
+
+ /// <summary>The event name for which to find references.</summary>
+ private readonly string EventName;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+ public string NounPhrase { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fullTypeName">The full type name for which to find references.</param>
+ /// <param name="eventName">The event name for which to find references.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public EventFinder(string fullTypeName, string eventName, string nounPhrase = null)
+ {
+ this.FullTypeName = fullTypeName;
+ this.EventName = eventName;
+ this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{eventName} event";
+ }
+
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ return false;
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction))
+ return false;
+
+ throw new IncompatibleInstructionException(this.NounPhrase);
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get whether a CIL instruction matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ protected bool IsMatch(Instruction instruction)
+ {
+ MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ return
+ methodRef != null
+ && methodRef.DeclaringType.FullName == this.FullTypeName
+ && (methodRef.Name == "add_" + this.EventName || methodRef.Name == "remove_" + this.EventName);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs
new file mode 100644
index 00000000..b44883e9
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs
@@ -0,0 +1,83 @@
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters.Finders
+{
+ /// <summary>Finds incompatible CIL instructions that reference a given field and throws an <see cref="IncompatibleInstructionException"/>.</summary>
+ public class FieldFinder : IInstructionRewriter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The full type name for which to find references.</summary>
+ private readonly string FullTypeName;
+
+ /// <summary>The field name for which to find references.</summary>
+ private readonly string FieldName;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+ public string NounPhrase { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fullTypeName">The full type name for which to find references.</param>
+ /// <param name="fieldName">The field name for which to find references.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public FieldFinder(string fullTypeName, string fieldName, string nounPhrase = null)
+ {
+ this.FullTypeName = fullTypeName;
+ this.FieldName = fieldName;
+ this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{fieldName} field";
+ }
+
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ return false;
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction))
+ return false;
+
+ throw new IncompatibleInstructionException(this.NounPhrase);
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get whether a CIL instruction matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ protected bool IsMatch(Instruction instruction)
+ {
+ FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ return
+ fieldRef != null
+ && fieldRef.DeclaringType.FullName == this.FullTypeName
+ && fieldRef.Name == this.FieldName;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs
new file mode 100644
index 00000000..19dda58a
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs
@@ -0,0 +1,83 @@
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters.Finders
+{
+ /// <summary>Finds incompatible CIL instructions that reference a given method and throws an <see cref="IncompatibleInstructionException"/>.</summary>
+ public class MethodFinder : IInstructionRewriter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The full type name for which to find references.</summary>
+ private readonly string FullTypeName;
+
+ /// <summary>The method name for which to find references.</summary>
+ private readonly string MethodName;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+ public string NounPhrase { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fullTypeName">The full type name for which to find references.</param>
+ /// <param name="methodName">The method name for which to find references.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public MethodFinder(string fullTypeName, string methodName, string nounPhrase = null)
+ {
+ this.FullTypeName = fullTypeName;
+ this.MethodName = methodName;
+ this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{methodName} method";
+ }
+
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ return false;
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction))
+ return false;
+
+ throw new IncompatibleInstructionException(this.NounPhrase);
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get whether a CIL instruction matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ protected bool IsMatch(Instruction instruction)
+ {
+ MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ return
+ methodRef != null
+ && methodRef.DeclaringType.FullName == this.FullTypeName
+ && methodRef.Name == this.MethodName;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs
new file mode 100644
index 00000000..0560e38e
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs
@@ -0,0 +1,135 @@
+using System.Linq;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters.Finders
+{
+ /// <summary>Finds incompatible CIL instructions that reference a given type and throws an <see cref="IncompatibleInstructionException"/>.</summary>
+ public class TypeFinder : IInstructionRewriter
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The full type name for which to find references.</summary>
+ private readonly string FullTypeName;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+ public string NounPhrase { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fullTypeName">The full type name to match.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public TypeFinder(string fullTypeName, string nounPhrase = null)
+ {
+ this.FullTypeName = fullTypeName;
+ this.NounPhrase = nounPhrase ?? $"{fullTypeName} type";
+ }
+
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(method))
+ return false;
+
+ throw new IncompatibleInstructionException(this.NounPhrase);
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction))
+ return false;
+
+ throw new IncompatibleInstructionException(this.NounPhrase);
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get whether a CIL instruction matches.</summary>
+ /// <param name="method">The method deifnition.</param>
+ protected bool IsMatch(MethodDefinition method)
+ {
+ if (this.IsMatch(method.ReturnType))
+ return true;
+
+ foreach (VariableDefinition variable in method.Body.Variables)
+ {
+ if (this.IsMatch(variable.VariableType))
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>Get whether a CIL instruction matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ protected bool IsMatch(Instruction instruction)
+ {
+ // field reference
+ FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ if (fieldRef != null)
+ {
+ return
+ this.IsMatch(fieldRef.DeclaringType) // field on target class
+ || this.IsMatch(fieldRef.FieldType); // field value is target class
+ }
+
+ // method reference
+ MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ if (methodRef != null)
+ {
+ return
+ this.IsMatch(methodRef.DeclaringType) // method on target class
+ || this.IsMatch(methodRef.ReturnType) // method returns target class
+ || methodRef.Parameters.Any(p => this.IsMatch(p.ParameterType)); // method parameters
+ }
+
+ return false;
+ }
+
+ /// <summary>Get whether a type reference matches the expected type.</summary>
+ /// <param name="type">The type to check.</param>
+ protected bool IsMatch(TypeReference type)
+ {
+ // root type
+ if (type.FullName == this.FullTypeName)
+ return true;
+
+ // generic arguments
+ if (type is GenericInstanceType genericType)
+ {
+ if (genericType.GenericArguments.Any(this.IsMatch))
+ return true;
+ }
+
+ // generic parameters (e.g. constraints)
+ if (type.GenericParameters.Any(this.IsMatch))
+ return true;
+
+ return false;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs
new file mode 100644
index 00000000..2f16b23d
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs
@@ -0,0 +1,38 @@
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters
+{
+ /// <summary>Rewrites CIL instructions for compatibility.</summary>
+ public interface IInstructionRewriter
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase indicating what the rewriter matches.</summary>
+ string NounPhrase { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged);
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged);
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs
deleted file mode 100644
index 5cbb7e0d..00000000
--- a/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Mono.Cecil;
-using Mono.Cecil.Cil;
-
-namespace StardewModdingAPI.AssemblyRewriters
-{
- /// <summary>Rewrites a method for compatibility.</summary>
- public interface IMethodRewriter
- {
- /// <summary>Get whether the given method reference can be rewritten.</summary>
- /// <param name="methodRef">The method reference.</param>
- bool ShouldRewrite(MethodReference methodRef);
-
- /// <summary>Rewrite a method for compatibility.</summary>
- /// <param name="module">The module being rewritten.</param>
- /// <param name="cil">The CIL rewriter.</param>
- /// <param name="callOp">The instruction which calls the method.</param>
- /// <param name="methodRef">The method reference invoked by the <paramref name="callOp"/>.</param>
- /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
- void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap);
- }
-}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs b/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs
new file mode 100644
index 00000000..f7e6bd8f
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace StardewModdingAPI.AssemblyRewriters
+{
+ /// <summary>An exception raised when an incompatible instruction is found while loading a mod assembly.</summary>
+ public class IncompatibleInstructionException : Exception
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase which describes the incompatible instruction that was found.</summary>
+ public string NounPhrase { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="nounPhrase">A brief noun phrase which describes the incompatible instruction that was found.</param>
+ public IncompatibleInstructionException(string nounPhrase)
+ : base($"Found an incompatible CIL instruction ({nounPhrase}).")
+ {
+ this.NounPhrase = nounPhrase;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="nounPhrase">A brief noun phrase which describes the incompatible instruction that was found.</param>
+ /// <param name="message">A message which describes the error.</param>
+ public IncompatibleInstructionException(string nounPhrase, string message)
+ : base(message)
+ {
+ this.NounPhrase = nounPhrase;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs
index 1af6e6c4..cfb330dd 100644
--- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs
+++ b/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs
@@ -4,66 +4,36 @@ using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;
-namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+namespace StardewModdingAPI.AssemblyRewriters
{
- /// <summary>Base class for a method rewriter.</summary>
- public abstract class BaseMethodRewriter : IMethodRewriter
+ /// <summary>Provides helper methods for field rewriters.</summary>
+ internal static class RewriteHelper
{
/*********
** Public methods
*********/
- /// <summary>Get whether the given method reference can be rewritten.</summary>
- /// <param name="methodRef">The method reference.</param>
- public abstract bool ShouldRewrite(MethodReference methodRef);
-
- /// <summary>Rewrite a method for compatibility.</summary>
- /// <param name="module">The module being rewritten.</param>
- /// <param name="cil">The CIL rewriter.</param>
- /// <param name="callOp">The instruction which calls the method.</param>
- /// <param name="methodRef">The method reference invoked by the <paramref name="callOp"/>.</param>
- /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
- public abstract void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap);
-
-
- /*********
- ** Protected methods
- *********/
- /// <summary>Get whether a method definition matches the signature expected by a method reference.</summary>
- /// <param name="definition">The method definition.</param>
- /// <param name="reference">The method reference.</param>
- protected bool HasMatchingSignature(MethodInfo definition, MethodReference reference)
+ /// <summary>Get the field reference from an instruction if it matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ public static FieldReference AsFieldReference(Instruction instruction)
{
- // same name
- if (definition.Name != reference.Name)
- return false;
-
- // same arguments
- ParameterInfo[] definitionParameters = definition.GetParameters();
- ParameterDefinition[] referenceParameters = reference.Parameters.ToArray();
- if (referenceParameters.Length != definitionParameters.Length)
- return false;
- for (int i = 0; i < referenceParameters.Length; i++)
- {
- if (!this.IsMatchingType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType))
- return false;
- }
- return true;
+ return instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld
+ ? (FieldReference)instruction.Operand
+ : null;
}
- /// <summary>Get whether a type has a method whose signature matches the one expected by a method reference.</summary>
- /// <param name="type">The type to check.</param>
- /// <param name="reference">The method reference.</param>
- protected bool HasMatchingSignature(Type type, MethodReference reference)
+ /// <summary>Get the method reference from an instruction if it matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ public static MethodReference AsMethodReference(Instruction instruction)
{
- return type
- .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)
- .Any(method => this.HasMatchingSignature(method, reference));
+ return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt
+ ? (MethodReference)instruction.Operand
+ : null;
}
/// <summary>Get whether a type matches a type reference.</summary>
/// <param name="type">The defined type.</param>
/// <param name="reference">The type reference.</param>
- private bool IsMatchingType(Type type, TypeReference reference)
+ public static bool IsSameType(Type type, TypeReference reference)
{
// same namespace & name
if (type.Namespace != reference.Namespace || type.Name != reference.Name)
@@ -81,12 +51,44 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters
return false;
for (int i = 0; i < defGenerics.Length; i++)
{
- if (!this.IsMatchingType(defGenerics[i], refGenerics[i]))
+ if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i]))
return false;
}
}
return true;
}
+
+ /// <summary>Get whether a method definition matches the signature expected by a method reference.</summary>
+ /// <param name="definition">The method definition.</param>
+ /// <param name="reference">The method reference.</param>
+ public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference)
+ {
+ // same name
+ if (definition.Name != reference.Name)
+ return false;
+
+ // same arguments
+ ParameterInfo[] definitionParameters = definition.GetParameters();
+ ParameterDefinition[] referenceParameters = reference.Parameters.ToArray();
+ if (referenceParameters.Length != definitionParameters.Length)
+ return false;
+ for (int i = 0; i < referenceParameters.Length; i++)
+ {
+ if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType))
+ return false;
+ }
+ return true;
+ }
+
+ /// <summary>Get whether a type has a method whose signature matches the one expected by a method reference.</summary>
+ /// <param name="type">The type to check.</param>
+ /// <param name="reference">The method reference.</param>
+ public static bool HasMatchingSignature(Type type, MethodReference reference)
+ {
+ return type
+ .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)
+ .Any(method => RewriteHelper.HasMatchingSignature(method, reference));
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs
new file mode 100644
index 00000000..73844073
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Reflection;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using StardewModdingAPI.AssemblyRewriters.Finders;
+
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+{
+ /// <summary>Rewrites references to one field with another.</summary>
+ public class FieldReplaceRewriter : FieldFinder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The new field to reference.</summary>
+ private readonly FieldInfo ToField;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="type">The type whose field to which references should be rewritten.</param>
+ /// <param name="fromFieldName">The field name to rewrite.</param>
+ /// <param name="toFieldName">The new field name to reference.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName, string nounPhrase = null)
+ : base(type.FullName, fromFieldName, nounPhrase)
+ {
+ this.ToField = type.GetField(toFieldName);
+ if (this.ToField == null)
+ throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field.");
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction))
+ return false;
+
+ FieldReference newRef = module.Import(this.ToField);
+ cil.Replace(instruction, cil.Create(instruction.OpCode, newRef));
+ return true;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs
new file mode 100644
index 00000000..3f57042d
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs
@@ -0,0 +1,54 @@
+using System;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using StardewModdingAPI.AssemblyRewriters.Finders;
+
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+{
+ /// <summary>Rewrites field references into property references.</summary>
+ public class FieldToPropertyRewriter : FieldFinder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The type whose field to which references should be rewritten.</summary>
+ private readonly Type Type;
+
+ /// <summary>The field name to rewrite.</summary>
+ private readonly string FieldName;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="type">The type whose field to which references should be rewritten.</param>
+ /// <param name="fieldName">The field name to rewrite.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public FieldToPropertyRewriter(Type type, string fieldName, string nounPhrase = null)
+ : base(type.FullName, fieldName, nounPhrase)
+ {
+ this.Type = type;
+ this.FieldName = fieldName;
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction))
+ return false;
+
+ 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;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs
new file mode 100644
index 00000000..035ef211
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs
@@ -0,0 +1,93 @@
+using System;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+{
+ /// <summary>Rewrites method references from one parent type to another if the signatures match.</summary>
+ public class MethodParentRewriter : IInstructionRewriter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The type whose methods to remap.</summary>
+ private readonly Type FromType;
+
+ /// <summary>The type with methods to map to.</summary>
+ private readonly Type ToType;
+
+ /// <summary>Whether to only rewrite references if loading the assembly on a different platform than it was compiled on.</summary>
+ private readonly bool OnlyIfPlatformChanged;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
+ public string NounPhrase { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fromType">The type whose methods to remap.</param>
+ /// <param name="toType">The type with methods to map to.</param>
+ /// <param name="onlyIfPlatformChanged">Whether to only rewrite references if loading the assembly on a different platform than it was compiled on.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null)
+ {
+ this.FromType = fromType;
+ this.ToType = toType;
+ this.NounPhrase = nounPhrase ?? $"{fromType.Name} methods";
+ this.OnlyIfPlatformChanged = onlyIfPlatformChanged;
+ }
+
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ return false;
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction, platformChanged))
+ return false;
+
+ MethodReference methodRef = (MethodReference)instruction.Operand;
+ methodRef.DeclaringType = module.Import(this.ToType);
+ return true;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get whether a CIL instruction matches.</summary>
+ /// <param name="instruction">The IL instruction.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ protected bool IsMatch(Instruction instruction, bool platformChanged)
+ {
+ MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ return
+ methodRef != null
+ && (platformChanged || !this.OnlyIfPlatformChanged)
+ && methodRef.DeclaringType.FullName == this.FromType.FullName
+ && RewriteHelper.HasMatchingSignature(this.ToType, methodRef);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs
deleted file mode 100644
index 1c0a5cf3..00000000
--- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Microsoft.Xna.Framework.Graphics;
-using Mono.Cecil;
-using Mono.Cecil.Cil;
-using StardewModdingAPI.AssemblyRewriters.Wrappers;
-
-namespace StardewModdingAPI.AssemblyRewriters.Rewriters
-{
- /// <summary>Rewrites references to <see cref="SpriteBatch"/> to fix inconsistent method signatures between MonoGame and XNA.</summary>
- /// <remarks>MonoGame has one <c>SpriteBatch.Begin</c> method with optional arguments, but XNA has multiple method overloads. Incompatible method references are rewritten to use <see cref="CompatibleSpriteBatch"/>, which redirects all method signatures to the proper compiled MonoGame/XNA method.</remarks>
- public class SpriteBatchRewriter : BaseMethodRewriter
- {
- /// <summary>Get whether the given method reference can be rewritten.</summary>
- /// <param name="methodRef">The method reference.</param>
- public override bool ShouldRewrite(MethodReference methodRef)
- {
- return methodRef.DeclaringType.FullName == typeof(SpriteBatch).FullName && this.HasMatchingSignature(typeof(CompatibleSpriteBatch), methodRef);
- }
-
- /// <summary>Rewrite a method for compatibility.</summary>
- /// <param name="module">The module being rewritten.</param>
- /// <param name="cil">The CIL rewriter.</param>
- /// <param name="callOp">The instruction which calls the method.</param>
- /// <param name="methodRef">The method reference invoked by the <paramref name="callOp"/>.</param>
- /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
- public override void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap)
- {
- methodRef.DeclaringType = module.Import(typeof(CompatibleSpriteBatch));
- }
- }
-} \ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs
new file mode 100644
index 00000000..da6d9bc9
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs
@@ -0,0 +1,157 @@
+using System;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using StardewModdingAPI.AssemblyRewriters.Finders;
+
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+{
+ /// <summary>Rewrites all references to a type.</summary>
+ public class TypeReferenceRewriter : TypeFinder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The full type name to which to find references.</summary>
+ private readonly string FromTypeName;
+
+ /// <summary>The new type to reference.</summary>
+ private readonly Type ToType;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fromTypeFullName">The full type name to which to find references.</param>
+ /// <param name="toType">The new type to reference.</param>
+ /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
+ public TypeReferenceRewriter(string fromTypeFullName, Type toType, string nounPhrase = null)
+ : base(fromTypeFullName, nounPhrase)
+ {
+ this.FromTypeName = fromTypeFullName;
+ this.ToType = toType;
+ }
+
+ /// <summary>Rewrite a method definition for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="method">The method definition to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public override bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ bool rewritten = false;
+
+ // return type
+ if (this.IsMatch(method.ReturnType))
+ {
+ method.ReturnType = this.RewriteIfNeeded(module, method.ReturnType);
+ rewritten = true;
+ }
+
+ // parameters
+ foreach (ParameterDefinition parameter in method.Parameters)
+ {
+ if (this.IsMatch(parameter.ParameterType))
+ {
+ parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType);
+ rewritten = true;
+ }
+ }
+
+ // generic parameters
+ for (int i = 0; i < method.GenericParameters.Count; i++)
+ {
+ var parameter = method.GenericParameters[i];
+ if (this.IsMatch(parameter))
+ {
+ TypeReference newType = this.RewriteIfNeeded(module, parameter);
+ if (newType != parameter)
+ method.GenericParameters[i] = new GenericParameter(parameter.Name, newType);
+ rewritten = true;
+ }
+ }
+
+ // local variables
+ foreach (VariableDefinition variable in method.Body.Variables)
+ {
+ if (this.IsMatch(variable.VariableType))
+ {
+ variable.VariableType = this.RewriteIfNeeded(module, variable.VariableType);
+ rewritten = true;
+ }
+ }
+
+ return rewritten;
+ }
+
+ /// <summary>Rewrite a CIL instruction for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="instruction">The instruction to rewrite.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
+ /// <returns>Returns whether the instruction was rewritten.</returns>
+ /// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
+ public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
+ {
+ if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName))
+ return false;
+
+ // field reference
+ FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ if (fieldRef != null)
+ {
+ fieldRef.DeclaringType = this.RewriteIfNeeded(module, fieldRef.DeclaringType);
+ fieldRef.FieldType = this.RewriteIfNeeded(module, fieldRef.FieldType);
+ }
+
+ // method reference
+ MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ if (methodRef != null)
+ {
+ methodRef.DeclaringType = this.RewriteIfNeeded(module, methodRef.DeclaringType);
+ methodRef.ReturnType = this.RewriteIfNeeded(module, methodRef.ReturnType);
+ foreach (var parameter in methodRef.Parameters)
+ parameter.ParameterType = this.RewriteIfNeeded(module, parameter.ParameterType);
+ }
+
+ // type reference
+ if (instruction.Operand is TypeReference typeRef)
+ {
+ TypeReference newRef = this.RewriteIfNeeded(module, typeRef);
+ if (typeRef != newRef)
+ cil.Replace(instruction, cil.Create(instruction.OpCode, newRef));
+ }
+
+ return true;
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the adjusted type reference if it matches, else the same value.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="type">The type to replace if it matches.</param>
+ private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type)
+ {
+ // root type
+ if (type.FullName == this.FromTypeName)
+ return module.Import(this.ToType);
+
+ // generic arguments
+ if (type is GenericInstanceType genericType)
+ {
+ for (int i = 0; i < genericType.GenericArguments.Count; i++)
+ genericType.GenericArguments[i] = this.RewriteIfNeeded(module, genericType.GenericArguments[i]);
+ }
+
+ // generic parameters (e.g. constraints)
+ for (int i = 0; i < type.GenericParameters.Count; i++)
+ type.GenericParameters[i] = new GenericParameter(this.RewriteIfNeeded(module, type.GenericParameters[i]));
+
+ return type;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs
index e28d1a68..ee68f1d5 100644
--- a/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs
@@ -1,21 +1,23 @@
-using Microsoft.Xna.Framework;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
-#pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required
-namespace StardewModdingAPI.AssemblyRewriters.Wrappers
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers
{
/// <summary>Wraps <see cref="SpriteBatch"/> methods that are incompatible when converting compiled code between MonoGame and XNA.</summary>
- public class CompatibleSpriteBatch : SpriteBatch
+ public class SpriteBatchWrapper : SpriteBatch
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public CompatibleSpriteBatch(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
+ public SpriteBatchWrapper(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
+
/****
** MonoGame signatures
****/
+ [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")]
public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix)
{
base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity);
@@ -24,26 +26,31 @@ namespace StardewModdingAPI.AssemblyRewriters.Wrappers
/****
** XNA signatures
****/
+ [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")]
public new void Begin()
{
base.Begin();
}
+ [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")]
public new void Begin(SpriteSortMode sortMode, BlendState blendState)
{
base.Begin(sortMode, blendState);
}
+ [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")]
public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState)
{
base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState);
}
+ [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")]
public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect)
{
base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect);
}
+ [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")]
public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix)
{
base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix);
diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
index 1e6caacc..775de9f2 100644
--- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
+++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
@@ -60,23 +60,27 @@
<HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath>
<Private>True</Private>
</Reference>
- <Reference Include="Mono.Cecil.Rocks, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
- <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll</HintPath>
- <Private>True</Private>
- </Reference>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
- <Compile Include="IMethodRewriter.cs" />
+ <Compile Include="Finders\EventFinder.cs" />
+ <Compile Include="Finders\FieldFinder.cs" />
+ <Compile Include="Finders\MethodFinder.cs" />
+ <Compile Include="Finders\TypeFinder.cs" />
+ <Compile Include="IncompatibleInstructionException.cs" />
+ <Compile Include="RewriteHelper.cs" />
+ <Compile Include="IInstructionRewriter.cs" />
<Compile Include="Platform.cs" />
<Compile Include="PlatformAssemblyMap.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="Rewriters\BaseMethodRewriter.cs" />
- <Compile Include="Rewriters\SpriteBatchRewriter.cs" />
- <Compile Include="Wrappers\CompatibleSpriteBatch.cs" />
+ <Compile Include="Rewriters\TypeReferenceRewriter.cs" />
+ <Compile Include="Rewriters\FieldReplaceRewriter.cs" />
+ <Compile Include="Rewriters\FieldToPropertyRewriter.cs" />
+ <Compile Include="Rewriters\MethodParentRewriter.cs" />
+ <Compile Include="Rewriters\Wrappers\SpriteBatchWrapper.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs
index 5abcfc8f..fffba30f 100644
--- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs
+++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs
@@ -3,9 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
-#if SMAPI_FOR_WINDOWS
+using System.Threading;
using Microsoft.Win32;
-#endif
using StardewModdingApi.Installer.Enums;
namespace StardewModdingApi.Installer
@@ -16,38 +15,48 @@ namespace StardewModdingApi.Installer
/*********
** Properties
*********/
+ /// <summary>The <see cref="Environment.OSVersion"/> value that represents Windows 7.</summary>
+ private readonly Version Windows7Version = new Version(6, 1);
+
/// <summary>The default file paths where Stardew Valley can be installed.</summary>
+ /// <param name="platform">The target platform.</param>
/// <remarks>Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. </remarks>
- private IEnumerable<string> DefaultInstallPaths
+ private IEnumerable<string> GetDefaultInstallPaths(Platform platform)
{
- get
+ switch (platform)
{
- // Linux
- yield return $"{Environment.GetEnvironmentVariable("HOME")}/GOG Games/Stardew Valley/game";
- yield return $"{Environment.GetEnvironmentVariable("HOME")}/.local/share/Steam/steamapps/common/Stardew Valley";
-
- // Mac
- yield return "/Applications/Stardew Valley.app/Contents/MacOS";
- yield return $"{Environment.GetEnvironmentVariable("HOME")}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS";
-
- // Windows
- yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley";
- yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley";
+ case Platform.Mono:
+ // Linux
+ yield return $"{Environment.GetEnvironmentVariable("HOME")}/GOG Games/Stardew Valley/game";
+ yield return $"{Environment.GetEnvironmentVariable("HOME")}/.local/share/Steam/steamapps/common/Stardew Valley";
+ yield return $"{Environment.GetEnvironmentVariable("HOME")}/.steam/steam/steamapps/common/Stardew Valley";
+
+ // Mac
+ yield return "/Applications/Stardew Valley.app/Contents/MacOS";
+ yield return $"{Environment.GetEnvironmentVariable("HOME")}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS";
+ break;
+
+ case Platform.Windows:
+ // Windows
+ yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley";
+ yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley";
+
+ // Windows registry
+ IDictionary<string, string> registryKeys = new Dictionary<string, string>
+ {
+ [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam
+ [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows
+ };
+ foreach (var pair in registryKeys)
+ {
+ string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value);
+ if (!string.IsNullOrWhiteSpace(path))
+ yield return path;
+ }
+ break;
- // Windows registry
-#if SMAPI_FOR_WINDOWS
- IDictionary<string, string> registryKeys = new Dictionary<string, string>
- {
- [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam
- [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows
- };
- foreach (var pair in registryKeys)
- {
- string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value);
- if (!string.IsNullOrWhiteSpace(path))
- yield return path;
- }
-#endif
+ default:
+ throw new InvalidOperationException($"Unknown platform '{platform}'.");
}
}
@@ -59,6 +68,8 @@ namespace StardewModdingApi.Installer
Func<string, string> installPath = path => Path.Combine(installDir.FullName, path);
// common
+ yield return installPath("Mono.Cecil.dll");
+ yield return installPath("Newtonsoft.Json.dll");
yield return installPath("StardewModdingAPI.exe");
yield return installPath("StardewModdingAPI.config.json");
yield return installPath("StardewModdingAPI.data.json");
@@ -66,9 +77,6 @@ namespace StardewModdingApi.Installer
yield return installPath("steam_appid.txt");
// Linux/Mac only
- yield return installPath("Mono.Cecil.dll");
- yield return installPath("Mono.Cecil.Rocks.dll");
- yield return installPath("Newtonsoft.Json.dll");
yield return installPath("StardewModdingAPI");
yield return installPath("StardewModdingAPI.exe.mdb");
yield return installPath("System.Numerics.dll");
@@ -79,9 +87,14 @@ namespace StardewModdingApi.Installer
// obsolete
yield return installPath("Mods/.cache"); // 1.3-1.4
+ yield return installPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8
yield return installPath("StardewModdingAPI-settings.json"); // 1.0-1.4
- foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories())
- yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7
+ if (modsDir.Exists)
+ {
+ foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories())
+ yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7
+ }
+ yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files
}
/// <summary>Whether the current console supports color formatting.</summary>
@@ -100,7 +113,7 @@ namespace StardewModdingApi.Installer
///
/// Uninstall logic:
/// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup.
- /// 2. Delete all files and folders in the game directory matching one of the <see cref="UninstallPaths"/>.
+ /// 2. Delete all files and folders in the game directory matching one of the values returned by <see cref="GetUninstallPaths"/>.
///
/// Install flow:
/// 1. Run the uninstall flow.
@@ -133,14 +146,41 @@ namespace StardewModdingApi.Installer
****/
if (!packageDir.Exists)
{
- this.ExitError($"The 'internal/{platform}' package folder is missing (should be at {packageDir}).");
+ this.PrintError($"The 'internal/{platform}' package folder is missing (should be at {packageDir}).");
+ Console.ReadLine();
return;
}
if (!File.Exists(paths.executable))
{
- this.ExitError("The detected game install path doesn't contain a Stardew Valley executable.");
+ this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
+ Console.ReadLine();
return;
}
+
+ /****
+ ** validate Windows dependencies
+ ****/
+ if (platform == Platform.Windows)
+ {
+ // .NET Framework 4.5+
+ if (!this.HasNetFramework45(platform))
+ {
+ this.PrintError(Environment.OSVersion.Version >= this.Windows7Version
+ ? "Please install the latest version of .NET Framework before installing SMAPI." // Windows 7+
+ : "Please install .NET Framework 4.5 before installing SMAPI." // Windows Vista or earlier
+ );
+ this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details.");
+ Console.ReadLine();
+ return;
+ }
+ if (!this.HasXNA(platform))
+ {
+ this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup.");
+ Console.ReadLine();
+ return;
+ }
+ }
+
Console.WriteLine();
/****
@@ -175,8 +215,7 @@ namespace StardewModdingApi.Installer
if (platform == Platform.Mono && File.Exists(paths.unixLauncherBackup))
{
this.PrintDebug("Removing SMAPI launcher...");
- if (File.Exists(paths.unixLauncher))
- File.Delete(paths.unixLauncher);
+ this.InteractivelyDelete(paths.unixLauncher);
File.Move(paths.unixLauncherBackup, paths.unixLauncher);
}
@@ -188,12 +227,7 @@ namespace StardewModdingApi.Installer
{
this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files...");
foreach (string path in removePaths)
- {
- if (Directory.Exists(path))
- Directory.Delete(path, recursive: true);
- else
- File.Delete(path);
- }
+ this.InteractivelyDelete(path);
}
/****
@@ -206,8 +240,7 @@ namespace StardewModdingApi.Installer
foreach (FileInfo sourceFile in packageDir.EnumerateFiles())
{
string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
- if (File.Exists(targetPath))
- File.Delete(targetPath);
+ this.InteractivelyDelete(targetPath);
sourceFile.CopyTo(targetPath);
}
@@ -218,7 +251,7 @@ namespace StardewModdingApi.Installer
if (!File.Exists(paths.unixLauncherBackup))
File.Move(paths.unixLauncher, paths.unixLauncherBackup);
else if (File.Exists(paths.unixLauncher))
- File.Delete(paths.unixLauncher);
+ this.InteractivelyDelete(paths.unixLauncher);
File.Move(paths.unixSmapiLauncher, paths.unixLauncher);
}
@@ -242,8 +275,7 @@ namespace StardewModdingApi.Installer
// initialise target dir
DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name));
- if (targetDir.Exists)
- targetDir.Delete(recursive: true);
+ this.InteractivelyDelete(targetDir.FullName);
targetDir.Create();
// copy files
@@ -308,7 +340,6 @@ namespace StardewModdingApi.Installer
}
}
-#if SMAPI_FOR_WINDOWS
/// <summary>Get the value of a key in the Windows registry.</summary>
/// <param name="key">The full path of the registry key relative to HKLM.</param>
/// <param name="name">The name of the value.</param>
@@ -321,7 +352,6 @@ namespace StardewModdingApi.Installer
using (openKey)
return (string)openKey.GetValue(name);
}
-#endif
/// <summary>Print a debug message.</summary>
/// <param name="text">The text to print.</param>
@@ -337,12 +367,11 @@ namespace StardewModdingApi.Installer
this.PrintColor(text, ConsoleColor.DarkYellow);
}
- /// <summary>Print an error and pause the console if needed.</summary>
- /// <param name="error">The error text.</param>
- private void ExitError(string error)
+ /// <summary>Print a warning message.</summary>
+ /// <param name="text">The text to print.</param>
+ private void PrintError(string text)
{
- this.PrintColor(error, ConsoleColor.Red);
- Console.ReadLine();
+ this.PrintColor(text, ConsoleColor.Red);
}
/// <summary>Print a message to the console.</summary>
@@ -360,6 +389,93 @@ namespace StardewModdingApi.Installer
Console.WriteLine(text);
}
+ /// <summary>Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows.</summary>
+ /// <param name="platform">The current platform.</param>
+ /// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
+ private bool HasNetFramework45(Platform platform)
+ {
+ switch (platform)
+ {
+ case Platform.Windows:
+ using (RegistryKey versionKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"))
+ return versionKey?.GetValue("Release") != null; // .NET Framework 4.5+
+
+ default:
+ throw new NotSupportedException("The installed .NET Framework version can only be checked on Windows.");
+ }
+ }
+
+ /// <summary>Get whether the current system has XNA Framework installed. This only applies on Windows.</summary>
+ /// <param name="platform">The current platform.</param>
+ /// <exception cref="NotSupportedException">The current platform is not Windows.</exception>
+ private bool HasXNA(Platform platform)
+ {
+ switch (platform)
+ {
+ case Platform.Windows:
+ using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\XNA\Framework"))
+ return key != null; // XNA Framework 4.0+
+
+ default:
+ throw new NotSupportedException("The installed XNA Framework version can only be checked on Windows.");
+ }
+ }
+
+ /// <summary>Interactively delete a file or folder path, and block until deletion completes.</summary>
+ /// <param name="path">The file or folder path.</param>
+ private void InteractivelyDelete(string path)
+ {
+ while (true)
+ {
+ try
+ {
+ this.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path));
+ break;
+ }
+ catch (Exception ex)
+ {
+ this.PrintError($"Oops! The installer couldn't delete {path}: [{ex.GetType().Name}] {ex.Message}.");
+ this.PrintError("Please delete it yourself, then press any key to retry.");
+ Console.ReadKey();
+ }
+ }
+ }
+
+ /// <summary>Delete a file or folder regardless of file permissions, and block until deletion completes.</summary>
+ /// <param name="entry">The file or folder to reset.</param>
+ private void ForceDelete(FileSystemInfo entry)
+ {
+ // ignore if already deleted
+ entry.Refresh();
+ if (!entry.Exists)
+ return;
+
+ // delete children
+ var folder = entry as DirectoryInfo;
+ if (folder != null)
+ {
+ foreach (FileSystemInfo child in folder.GetFileSystemInfos())
+ this.ForceDelete(child);
+ }
+
+ // reset permissions & delete
+ entry.Attributes = FileAttributes.Normal;
+ entry.Delete();
+
+ // wait for deletion to finish
+ for (int i = 0; i < 10; i++)
+ {
+ entry.Refresh();
+ if (entry.Exists)
+ Thread.Sleep(500);
+ }
+
+ // throw exception if deletion didn't happen before timeout
+ entry.Refresh();
+ if (entry.Exists)
+ throw new IOException($"Timed out trying to delete {entry.FullName}");
+ }
+
/// <summary>Interactively ask the user to choose a value.</summary>
/// <param name="message">The message to print.</param>
/// <param name="options">The allowed options (not case sensitive).</param>
@@ -382,10 +498,16 @@ namespace StardewModdingApi.Installer
/// <param name="platform">The current platform.</param>
private DirectoryInfo InteractivelyGetInstallPath(Platform platform)
{
+ // get executable name
+ string executableFilename = platform == Platform.Windows
+ ? "Stardew Valley.exe"
+ : "StardewValley.exe";
+
// try default paths
- foreach (string defaultPath in this.DefaultInstallPaths)
+ foreach (string defaultPath in this.GetDefaultInstallPaths(platform))
{
- if (Directory.Exists(defaultPath))
+ DirectoryInfo dir = new DirectoryInfo(defaultPath);
+ if (dir.Exists && dir.EnumerateFiles(executableFilename).Any())
return new DirectoryInfo(defaultPath);
}
@@ -394,7 +516,7 @@ namespace StardewModdingApi.Installer
while (true)
{
// get path from user
- Console.WriteLine($"Type the file path to the game directory (the one containing '{(platform == Platform.Mono ? "StardewValley.exe" : "Stardew Valley.exe")}'), then press enter.");
+ Console.WriteLine($"Type the file path to the game directory (the one containing '{executableFilename}'), then press enter.");
string path = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(path))
{
@@ -402,9 +524,16 @@ namespace StardewModdingApi.Installer
continue;
}
- // normalise on Windows
+ // normalise path
if (platform == Platform.Windows)
path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path
+ if (platform == Platform.Mono)
+ path = path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line
+ if (path.StartsWith("~/"))
+ {
+ string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE");
+ path = Path.Combine(home, path.Substring(2));
+ }
// get directory
if (File.Exists(path))
@@ -417,7 +546,7 @@ namespace StardewModdingApi.Installer
Console.WriteLine(" That directory doesn't seem to exist.");
continue;
}
- if (!directory.EnumerateFiles("*.exe").Any(p => p.Name == "StardewValley.exe" || p.Name == "Stardew Valley.exe"))
+ if (!directory.EnumerateFiles(executableFilename).Any())
{
Console.WriteLine(" That directory doesn't contain a Stardew Valley executable.");
continue;
diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
index e31a1452..366e1c6e 100644
--- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
+++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
@@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>StardewModdingAPI.Installer</RootNamespace>
<AssemblyName>StardewModdingAPI.Installer</AssemblyName>
- <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln
index 8ab297ed..441b51a9 100644
--- a/src/StardewModdingAPI.sln
+++ b/src/StardewModdingAPI.sln
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
+# Visual Studio 15
+VisualStudioVersion = 15.0.26228.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}"
EndProject
diff --git a/src/StardewModdingAPI.sln.DotSettings b/src/StardewModdingAPI.sln.DotSettings
index 7ee9b76e..81b52fd4 100644
--- a/src/StardewModdingAPI.sln.DotSettings
+++ b/src/StardewModdingAPI.sln.DotSettings
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNameQualifier/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/StaticQualifier/STATIC_MEMBERS_QUALIFY_MEMBERS/@EntryValue">Field, Property, Event, Method</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">Field, Property, Event, Method</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/LINE_FEED_AT_FILE_END/@EntryValue">True</s:Boolean>
diff --git a/src/StardewModdingAPI/Advanced/ConfigFile.cs b/src/StardewModdingAPI/Advanced/ConfigFile.cs
deleted file mode 100644
index 1a2e6618..00000000
--- a/src/StardewModdingAPI/Advanced/ConfigFile.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.IO;
-using Newtonsoft.Json;
-
-namespace StardewModdingAPI.Advanced
-{
- /// <summary>Wraps a configuration file with IO methods for convenience.</summary>
- public abstract class ConfigFile : IConfigFile
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Provides simplified APIs for writing mods.</summary>
- public IModHelper ModHelper { get; set; }
-
- /// <summary>The file path from which the model was loaded, relative to the mod directory.</summary>
- public string FilePath { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Reparse the underlying file and update this model.</summary>
- public void Reload()
- {
- string json = File.ReadAllText(Path.Combine(this.ModHelper.DirectoryPath, this.FilePath));
- JsonConvert.PopulateObject(json, this);
- }
-
- /// <summary>Save this model to the underlying file.</summary>
- public void Save()
- {
- this.ModHelper.WriteJsonFile(this.FilePath, this);
- }
- }
-} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Advanced/IConfigFile.cs b/src/StardewModdingAPI/Advanced/IConfigFile.cs
deleted file mode 100644
index 5bc31a88..00000000
--- a/src/StardewModdingAPI/Advanced/IConfigFile.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace StardewModdingAPI.Advanced
-{
- /// <summary>Wraps a configuration file with IO methods for convenience.</summary>
- public interface IConfigFile
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Provides simplified APIs for writing mods.</summary>
- IModHelper ModHelper { get; set; }
-
- /// <summary>The file path from which the model was loaded, relative to the mod directory.</summary>
- string FilePath { get; set; }
-
-
- /*********
- ** Methods
- *********/
- /// <summary>Reparse the underlying file and update this model.</summary>
- void Reload();
-
- /// <summary>Save this model to the underlying file.</summary>
- void Save();
- }
-}
diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs
index 1fa18d49..e2d08538 100644
--- a/src/StardewModdingAPI/Command.cs
+++ b/src/StardewModdingAPI/Command.cs
@@ -1,23 +1,33 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
namespace StardewModdingAPI
{
/// <summary>A command that can be submitted through the SMAPI console to interact with SMAPI.</summary>
+ [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))]
public class Command
{
/*********
** Properties
*********/
- /****
- ** SMAPI
- ****/
/// <summary>The commands registered with SMAPI.</summary>
- internal static List<Command> RegisteredCommands = new List<Command>();
+ private static readonly IDictionary<string, Command> LegacyCommands = new Dictionary<string, Command>(StringComparer.InvariantCultureIgnoreCase);
+
+ /// <summary>Manages console commands.</summary>
+ private static CommandManager CommandManager;
+ /// <summary>Manages deprecation warnings.</summary>
+ private static DeprecationManager DeprecationManager;
+
+ /// <summary>Tracks the installed mods.</summary>
+ private static ModRegistry ModRegistry;
+
+
+ /*********
+ ** Accessors
+ *********/
/// <summary>The event raised when this command is submitted through the console.</summary>
public event EventHandler<EventArgsCommand> CommandFired;
@@ -43,6 +53,17 @@ namespace StardewModdingAPI
/****
** Command
****/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="commandManager">Manages console commands.</param>
+ /// <param name="deprecationManager">Manages deprecation warnings.</param>
+ /// <param name="modRegistry">Tracks the installed mods.</param>
+ internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry)
+ {
+ Command.CommandManager = commandManager;
+ Command.DeprecationManager = deprecationManager;
+ Command.ModRegistry = modRegistry;
+ }
+
/// <summary>Construct an instance.</summary>
/// <param name="name">The name of the command.</param>
/// <param name="description">A human-readable description of what the command does.</param>
@@ -64,44 +85,17 @@ namespace StardewModdingAPI
this.CommandFired.Invoke(this, new EventArgsCommand(this));
}
+
/****
** SMAPI
****/
/// <summary>Parse a command string and invoke it if valid.</summary>
/// <param name="input">The command to run, including the command name and any arguments.</param>
- [Obsolete("Use the overload which passes in your mod's monitor")]
- public static void CallCommand(string input)
- {
- Program.DeprecationManager.Warn($"an old version of {nameof(Command)}.{nameof(Command.CallCommand)}", "1.1", DeprecationLevel.Notice);
- Command.CallCommand(input, Program.GetLegacyMonitorForMod());
- }
-
- /// <summary>Parse a command string and invoke it if valid.</summary>
- /// <param name="input">The command to run, including the command name and any arguments.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public static void CallCommand(string input, IMonitor monitor)
{
- // normalise input
- input = input?.Trim();
- if (string.IsNullOrWhiteSpace(input))
- return;
-
- // tokenise input
- string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
- string commandName = args[0];
- args = args.Skip(1).ToArray();
-
- // get command
- Command command = Command.FindCommand(commandName);
- if (command == null)
- {
- monitor.Log("Unknown command", LogLevel.Error);
- return;
- }
-
- // fire command
- command.CalledArgs = args;
- command.Fire();
+ Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Info);
+ Command.CommandManager.Trigger(input);
}
/// <summary>Register a command with SMAPI.</summary>
@@ -110,11 +104,25 @@ namespace StardewModdingAPI
/// <param name="args">A human-readable list of accepted arguments.</param>
public static Command RegisterCommand(string name, string description, string[] args = null)
{
- var command = new Command(name, description, args);
- if (Command.RegisteredCommands.Contains(command))
- throw new InvalidOperationException($"The '{command.CommandName}' command is already registered!");
+ name = name?.Trim().ToLower();
+
+ // raise deprecation warning
+ Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Info);
- Command.RegisteredCommands.Add(command);
+ // validate
+ if (Command.LegacyCommands.ContainsKey(name))
+ throw new InvalidOperationException($"The '{name}' command is already registered!");
+
+ // add command
+ string modName = Command.ModRegistry.GetModFromStack() ?? "<unknown mod>";
+ string documentation = args?.Length > 0
+ ? $"{description} - {string.Join(", ", args)}"
+ : description;
+ Command.CommandManager.Add(modName, name, documentation, Command.Fire);
+
+ // add legacy command
+ Command command = new Command(name, description, args);
+ Command.LegacyCommands.Add(name, command);
return command;
}
@@ -122,7 +130,28 @@ namespace StardewModdingAPI
/// <param name="name">The command name to find.</param>
public static Command FindCommand(string name)
{
- return Command.RegisteredCommands.Find(x => x.CommandName.Equals(name));
+ Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Info);
+ if (name == null)
+ return null;
+
+ Command command;
+ Command.LegacyCommands.TryGetValue(name.Trim(), out command);
+ return command;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Trigger this command.</summary>
+ /// <param name="name">The command name.</param>
+ /// <param name="args">The command arguments.</param>
+ private static void Fire(string name, string[] args)
+ {
+ Command command;
+ if (!Command.LegacyCommands.TryGetValue(name, out command))
+ throw new InvalidOperationException($"Can't run command '{name}' because there's no such legacy command.");
+ command.Fire();
}
}
}
diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs
index 037c0fdf..9f4bfad2 100644
--- a/src/StardewModdingAPI/Config.cs
+++ b/src/StardewModdingAPI/Config.cs
@@ -12,6 +12,13 @@ namespace StardewModdingAPI
public abstract class Config
{
/*********
+ ** Properties
+ *********/
+ /// <summary>Manages deprecation warnings.</summary>
+ private static DeprecationManager DeprecationManager;
+
+
+ /*********
** Accessors
*********/
/// <summary>The full path to the configuration file.</summary>
@@ -26,6 +33,13 @@ namespace StardewModdingAPI
/*********
** Public methods
*********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="deprecationManager">Manages deprecation warnings.</param>
+ internal static void Shim(DeprecationManager deprecationManager)
+ {
+ Config.DeprecationManager = deprecationManager;
+ }
+
/// <summary>Construct an instance of the config class.</summary>
/// <typeparam name="T">The config class type.</typeparam>
[Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")]
@@ -111,8 +125,8 @@ namespace StardewModdingAPI
/// <summary>Construct an instance.</summary>
protected Config()
{
- Program.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Notice);
- Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings
+ Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Info);
+ Config.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings
}
}
diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs
index a62a0d58..4a036cd0 100644
--- a/src/StardewModdingAPI/Constants.cs
+++ b/src/StardewModdingAPI/Constants.cs
@@ -3,8 +3,12 @@ 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 StardewValley;
namespace StardewModdingAPI
@@ -15,64 +19,74 @@ namespace StardewModdingAPI
/*********
** Properties
*********/
- /// <summary>The directory name containing the current save's data (if a save is loaded).</summary>
- private static string RawSaveFolderName => Constants.PlayerNull ? string.Empty : Constants.GetSaveFolderName();
-
/// <summary>The directory path containing the current save's data (if a save is loaded).</summary>
- private static string RawSavePath => Constants.PlayerNull ? string.Empty : Path.Combine(Constants.SavesPath, Constants.RawSaveFolderName);
+ private static string RawSavePath => Constants.IsSaveLoaded ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : null;
+
+ /// <summary>Whether the directory containing the current save's data exists on disk.</summary>
+ private static bool SavePathReady => Constants.IsSaveLoaded && Directory.Exists(Constants.RawSavePath);
/*********
** Accessors
*********/
+ /****
+ ** Public
+ ****/
/// <summary>SMAPI's current semantic version.</summary>
- [Obsolete("Use " + nameof(Constants) + "." + nameof(ApiVersion))]
- public static readonly Version Version = (Version)Constants.ApiVersion;
-
- /// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion => new Version(1, 8, 0, null, suppressDeprecationWarning: true);
+ public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 9, 0);
/// <summary>The minimum supported version of Stardew Valley.</summary>
- public const string MinimumGameVersion = "1.1";
+ public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.1");
- /// <summary>The GitHub repository to check for updates.</summary>
- public const string GitHubRepository = "Pathoschild/SMAPI";
+ /// <summary>The maximum supported version of Stardew Valley.</summary>
+ public static ISemanticVersion MaximumGameVersion { get; } = new SemanticVersion("1.1.1");
+
+ /// <summary>The path to the game folder.</summary>
+ public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
/// <summary>The directory path containing Stardew Valley's app data.</summary>
- public static string DataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
+ public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
- /// <summary>The directory path where all saves are stored.</summary>
- public static string SavesPath => Path.Combine(Constants.DataPath, "Saves");
+ /// <summary>The directory path in which error logs should be stored.</summary>
+ public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs");
- /// <summary>Whether the directory containing the current save's data exists on disk.</summary>
- public static bool CurrentSavePathExists => Directory.Exists(Constants.RawSavePath);
+ /// <summary>The directory path where all saves are stored.</summary>
+ public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves");
/// <summary>The directory name containing the current save's data (if a save is loaded and the directory exists).</summary>
- public static string SaveFolderName => Constants.CurrentSavePathExists ? Constants.RawSaveFolderName : "";
+ public static string SaveFolderName => Constants.SavePathReady ? Constants.GetSaveFolderName() : "";
/// <summary>The directory path containing the current save's data (if a save is loaded and the directory exists).</summary>
- public static string CurrentSavePath => Constants.CurrentSavePathExists ? Constants.RawSavePath : "";
+ public static string CurrentSavePath => Constants.SavePathReady ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : "";
- /// <summary>Whether a player save has been loaded.</summary>
- public static bool PlayerNull => !Game1.hasLoadedGame || Game1.player == null || string.IsNullOrEmpty(Game1.player.name);
+ /****
+ ** Internal
+ ****/
+ /// <summary>The GitHub repository to check for updates.</summary>
+ internal const string GitHubRepository = "Pathoschild/SMAPI";
- /// <summary>The path to the current assembly being executing.</summary>
- public static string ExecutionPath => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ /// <summary>The file path for the SMAPI configuration file.</summary>
+ internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json");
- /// <summary>The title of the SMAPI console window.</summary>
- public static string ConsoleTitle => $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {Program.ModsLoaded}";
+ /// <summary>The file path to the log where the latest output should be saved.</summary>
+ internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt");
- /// <summary>The directory path in which error logs should be stored.</summary>
- public static string LogDir => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs");
+ /// <summary>The full path to the folder containing mods.</summary>
+ internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods");
- /// <summary>The file path to the error log where the latest output should be saved.</summary>
- public static string LogPath => Path.Combine(Constants.LogDir, "MODDED_ProgramLog.Log_LATEST.txt");
+ /// <summary>Whether a player save has been loaded.</summary>
+ internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name);
- /// <summary>The file path for the SMAPI configuration file.</summary>
- internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json");
+ /// <summary>The game's current semantic version.</summary>
+ internal static ISemanticVersion GameVersion { get; } = Constants.GetGameVersion();
- /// <summary>The file path for the SMAPI data file containing metadata about known mods.</summary>
- internal static string ApiModMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.data.json");
+ /// <summary>The target game platform.</summary>
+ internal static Platform TargetPlatform { get; } =
+#if SMAPI_FOR_WINDOWS
+ Platform.Windows;
+#else
+ Platform.Mono;
+#endif
/*********
@@ -124,20 +138,76 @@ namespace StardewModdingAPI
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
}
- /// <summary>Get method rewriters which fix incompatible method calls in mod assemblies.</summary>
- internal static IEnumerable<IMethodRewriter> GetMethodRewriters()
+ /// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary>
+ internal static IEnumerable<IInstructionRewriter> GetRewriters()
{
- return new[]
+ return new IInstructionRewriter[]
{
- new SpriteBatchRewriter()
+ /****
+ ** Finders throw an exception when incompatible code is found.
+ ****/
+ // 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"),
+
+ /****
+ ** Rewriters change CIL as needed to fix incompatible code
+ ****/
+ // crossplatform
+ new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true),
+
+ // SMAPI 1.9
+ new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange))
};
}
+ /// <summary>Get game current version as it should be displayed to players.</summary>
+ /// <param name="version">The semantic game version.</param>
+ internal static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version)
+ {
+ switch (version.ToString())
+ {
+ case "1.1.1":
+ return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11
+ default:
+ return version;
+ }
+ }
+
/// <summary>Get the name of a save directory for the current player.</summary>
private static string GetSaveFolderName()
{
string prefix = new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray());
return $"{prefix}_{Game1.uniqueIDForThisGame}";
}
+
+ /// <summary>Get the game's current semantic version.</summary>
+ private static ISemanticVersion GetGameVersion()
+ {
+ // get raw version
+ // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time
+ FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static);
+ if (field == null)
+ throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found.");
+ string version = (string)field.GetValue(null);
+
+ // get semantic version
+ if (version == "1.11")
+ version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks
+ return new SemanticVersion(version);
+ }
}
}
diff --git a/src/StardewModdingAPI/Entities/SPlayer.cs b/src/StardewModdingAPI/Entities/SPlayer.cs
deleted file mode 100644
index 66c7ba44..00000000
--- a/src/StardewModdingAPI/Entities/SPlayer.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System;
-using System.Collections.Generic;
-using StardewModdingAPI.Framework;
-using StardewValley;
-
-namespace StardewModdingAPI.Entities
-{
- /// <summary>Static class for integrating with the player.</summary>
- [Obsolete("This API was never officially documented and will be removed soon.")]
- public class SPlayer
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Obsolete.</summary>
- [Obsolete("Use " + nameof(Game1) + "." + nameof(Game1.getAllFarmers) + " instead")]
- public static List<Farmer> AllFarmers
- {
- get
- {
- Program.DeprecationManager.Warn(nameof(SPlayer), "1.0", DeprecationLevel.Info);
- return Game1.getAllFarmers();
- }
- }
-
- /// <summary>Obsolete.</summary>
- [Obsolete("Use " + nameof(Game1) + "." + nameof(Game1.player) + " instead")]
- public static Farmer CurrentFarmer
- {
- get
- {
- Program.DeprecationManager.Warn(nameof(SPlayer), "1.0", DeprecationLevel.Info);
- return Game1.player;
- }
- }
-
- /// <summary>Obsolete.</summary>
- [Obsolete("Use " + nameof(Game1) + "." + nameof(Game1.player) + " instead")]
- public static Farmer Player
- {
- get
- {
- Program.DeprecationManager.Warn(nameof(SPlayer), "1.0", DeprecationLevel.Info);
- return Game1.player;
- }
- }
-
- /// <summary>Obsolete.</summary>
- [Obsolete("Use " + nameof(Game1) + "." + nameof(Game1.player) + "." + nameof(Farmer.currentLocation) + " instead")]
- public static GameLocation CurrentFarmerLocation
- {
- get
- {
- Program.DeprecationManager.Warn(nameof(SPlayer), "1.0", DeprecationLevel.Info);
- return Game1.player.currentLocation;
- }
- }
- }
-} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Inheritance/ChangeType.cs b/src/StardewModdingAPI/Events/ChangeType.cs
index 94eb33ed..4b207f08 100644
--- a/src/StardewModdingAPI/Inheritance/ChangeType.cs
+++ b/src/StardewModdingAPI/Events/ChangeType.cs
@@ -1,4 +1,4 @@
-namespace StardewModdingAPI.Inheritance
+namespace StardewModdingAPI.Events
{
/// <summary>Indicates how an inventory item changed.</summary>
public enum ChangeType
diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs
new file mode 100644
index 00000000..9418673a
--- /dev/null
+++ b/src/StardewModdingAPI/Events/ContentEvents.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using StardewModdingAPI.Framework;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Events raised when the game loads content.</summary>
+ [Obsolete("This is an undocumented experimental API and may change without warning.")]
+ public static class ContentEvents
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Tracks the installed mods.</summary>
+ private static ModRegistry ModRegistry;
+
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ private static IMonitor Monitor;
+
+ /// <summary>The mods using the experimental API for which a warning has been raised.</summary>
+ private static readonly HashSet<string> WarnedMods = new HashSet<string>();
+
+
+ /*********
+ ** Events
+ *********/
+ /// <summary>Raised after the content language changes.</summary>
+ public static event EventHandler<EventArgsValueChanged<string>> AfterLocaleChanged;
+
+ /// <summary>Raised when an XNB file is being read into the cache. Mods can change the data here before it's cached.</summary>
+ internal static event EventHandler<IContentEventHelper> AfterAssetLoaded;
+
+
+ /*********
+ ** Internal methods
+ *********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="modRegistry">Tracks the installed mods.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ internal static void Shim(ModRegistry modRegistry, IMonitor monitor)
+ {
+ ContentEvents.ModRegistry = modRegistry;
+ ContentEvents.Monitor = monitor;
+ }
+
+ /// <summary>Raise an <see cref="AfterLocaleChanged"/> event.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="oldLocale">The previous locale.</param>
+ /// <param name="newLocale">The current locale.</param>
+ internal static void InvokeAfterLocaleChanged(IMonitor monitor, string oldLocale, string newLocale)
+ {
+ monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterLocaleChanged)}", ContentEvents.AfterLocaleChanged?.GetInvocationList(), null, new EventArgsValueChanged<string>(oldLocale, newLocale));
+ }
+
+ /// <summary>Raise an <see cref="AfterAssetLoaded"/> event.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="contentHelper">Encapsulates access and changes to content being read from a data file.</param>
+ internal static void InvokeAfterAssetLoaded(IMonitor monitor, IContentEventHelper contentHelper)
+ {
+ if (ContentEvents.AfterAssetLoaded != null)
+ {
+ Delegate[] handlers = ContentEvents.AfterAssetLoaded.GetInvocationList();
+ ContentEvents.RaiseDeprecationWarning(handlers);
+ monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterAssetLoaded)}", handlers, null, contentHelper);
+ }
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Raise a 'experimental API' warning for each mod using the content API.</summary>
+ /// <param name="handlers">The event handlers.</param>
+ private static void RaiseDeprecationWarning(Delegate[] handlers)
+ {
+ foreach (Delegate handler in handlers)
+ {
+ string modName = ContentEvents.ModRegistry.GetModFrom(handler) ?? "An unknown mod";
+ if (!ContentEvents.WarnedMods.Contains(modName))
+ {
+ ContentEvents.WarnedMods.Add(modName);
+ ContentEvents.Monitor.Log($"{modName} used the undocumented and experimental content API, which may change or be removed without warning.", LogLevel.Warn);
+ }
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Events/EventArgsCommand.cs b/src/StardewModdingAPI/Events/EventArgsCommand.cs
index ddf644fb..bae13694 100644
--- a/src/StardewModdingAPI/Events/EventArgsCommand.cs
+++ b/src/StardewModdingAPI/Events/EventArgsCommand.cs
@@ -3,6 +3,7 @@
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for a <see cref="StardewModdingAPI.Command.CommandFired"/> event.</summary>
+ [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))]
public class EventArgsCommand : EventArgs
{
/*********
diff --git a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs
index 273f9d25..699d90be 100644
--- a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs
+++ b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs
@@ -1,5 +1,5 @@
using System;
-using StardewValley;
+using SFarmer = StardewValley.Farmer;
namespace StardewModdingAPI.Events
{
@@ -10,10 +10,10 @@ namespace StardewModdingAPI.Events
** Accessors
*********/
/// <summary>The previous player character.</summary>
- public Farmer NewFarmer { get; }
+ public SFarmer NewFarmer { get; }
/// <summary>The new player character.</summary>
- public Farmer PriorFarmer { get; }
+ public SFarmer PriorFarmer { get; }
/*********
@@ -22,7 +22,7 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="priorFarmer">The previous player character.</param>
/// <param name="newFarmer">The new player character.</param>
- public EventArgsFarmerChanged(Farmer priorFarmer, Farmer newFarmer)
+ public EventArgsFarmerChanged(SFarmer priorFarmer, SFarmer newFarmer)
{
this.PriorFarmer = priorFarmer;
this.NewFarmer = newFarmer;
diff --git a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs b/src/StardewModdingAPI/Events/EventArgsIntChanged.cs
index 31079730..0c742d12 100644
--- a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs
+++ b/src/StardewModdingAPI/Events/EventArgsIntChanged.cs
@@ -9,11 +9,10 @@ namespace StardewModdingAPI.Events
** Accessors
*********/
/// <summary>The previous value.</summary>
- public int NewInt { get; }
-
- /// <summary>The current value.</summary>
public int PriorInt { get; }
+ /// <summary>The current value.</summary>
+ public int NewInt { get; }
/*********
** Public methods
diff --git a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs b/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs
index 40c77419..11cbcedf 100644
--- a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs
+++ b/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using StardewModdingAPI.Inheritance;
using StardewValley;
namespace StardewModdingAPI.Events
diff --git a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs b/src/StardewModdingAPI/Events/EventArgsValueChanged.cs
new file mode 100644
index 00000000..1d25af49
--- /dev/null
+++ b/src/StardewModdingAPI/Events/EventArgsValueChanged.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a field that changed value.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ public class EventArgsValueChanged<T> : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The previous value.</summary>
+ public T PriorValue { get; }
+
+ /// <summary>The current value.</summary>
+ public T NewValue { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="priorValue">The previous value.</param>
+ /// <param name="newValue">The current value.</param>
+ public EventArgsValueChanged(T priorValue, T newValue)
+ {
+ this.PriorValue = priorValue;
+ this.NewValue = newValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/StardewModdingAPI/Events/GraphicsEvents.cs
index 5f4feeac..25b976f1 100644
--- a/src/StardewModdingAPI/Events/GraphicsEvents.cs
+++ b/src/StardewModdingAPI/Events/GraphicsEvents.cs
@@ -15,24 +15,13 @@ namespace StardewModdingAPI.Events
/// <summary>Raised after the game window is resized.</summary>
public static event EventHandler Resize;
- /// <summary>Raised when drawing debug information to the screen (when <see cref="StardewModdingAPI.Inheritance.SGame.Debug"/> is true). This is called after the sprite batch is begun. If you just want to add debug info, use <see cref="StardewModdingAPI.Inheritance.SGame.DebugMessageQueue" /> in your update loop.</summary>
- public static event EventHandler DrawDebug;
-
- /// <summary>Obsolete.</summary>
- [Obsolete("Use the other Pre/Post render events instead.")]
- public static event EventHandler DrawTick;
-
- /// <summary>Obsolete.</summary>
- [Obsolete("Use the other Pre/Post render events instead. All of them will automatically be drawn into the render target if needed.")]
- public static event EventHandler DrawInRenderTargetTick;
-
/****
** Main render events
****/
- /// <summary>Raised before drawing everything to the screen during a draw loop.</summary>
+ /// <summary>Raised before drawing the world to the screen.</summary>
public static event EventHandler OnPreRenderEvent;
- /// <summary>Raised after drawing everything to the screen during a draw loop.</summary>
+ /// <summary>Raised after drawing the world to the screen.</summary>
public static event EventHandler OnPostRenderEvent;
/****
@@ -41,30 +30,18 @@ namespace StardewModdingAPI.Events
/// <summary>Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary>
public static event EventHandler OnPreRenderHudEvent;
- /// <summary>Equivalent to <see cref="OnPreRenderHudEvent"/>, but invoked even if the HUD isn't available.</summary>
- public static event EventHandler OnPreRenderHudEventNoCheck;
-
/// <summary>Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary>
public static event EventHandler OnPostRenderHudEvent;
- /// <summary>Equivalent to <see cref="OnPostRenderHudEvent"/>, but invoked even if the HUD isn't available.</summary>
- public static event EventHandler OnPostRenderHudEventNoCheck;
-
/****
** GUI events
****/
/// <summary>Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary>
public static event EventHandler OnPreRenderGuiEvent;
- /// <summary>Equivalent to <see cref="OnPreRenderGuiEvent"/>, but invoked even if there's no menu being drawn.</summary>
- public static event EventHandler OnPreRenderGuiEventNoCheck;
-
/// <summary>Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary>
public static event EventHandler OnPostRenderGuiEvent;
- /// <summary>Equivalent to <see cref="OnPreRenderGuiEvent"/>, but invoked even if there's no menu being drawn.</summary>
- public static event EventHandler OnPostRenderGuiEventNoCheck;
-
/*********
** Internal methods
@@ -81,29 +58,6 @@ namespace StardewModdingAPI.Events
monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList(), sender, e);
}
- /// <summary>Raise a <see cref="DrawDebug"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal static void InvokeDrawDebug(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.DrawDebug)}", GraphicsEvents.DrawDebug?.GetInvocationList());
- }
-
- /// <summary>Raise a <see cref="DrawTick"/> event.</summary>
- /// <param name="monitor">Encapsulates logging and monitoring.</param>
- [Obsolete("Should not be used.")]
- public static void InvokeDrawTick(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.DrawTick)}", GraphicsEvents.DrawTick?.GetInvocationList());
- }
-
- /// <summary>Raise a <see cref="DrawInRenderTargetTick"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- [Obsolete("Should not be used.")]
- public static void InvokeDrawInRenderTargetTick(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.DrawInRenderTargetTick)}", GraphicsEvents.DrawInRenderTargetTick?.GetInvocationList());
- }
-
/****
** Main render events
****/
@@ -121,8 +75,14 @@ namespace StardewModdingAPI.Events
monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderEvent)}", GraphicsEvents.OnPostRenderEvent?.GetInvocationList());
}
+ /// <summary>Get whether there are any post-render event listeners.</summary>
+ internal static bool HasPostRenderListeners()
+ {
+ return GraphicsEvents.OnPostRenderEvent != null;
+ }
+
/****
- ** HUD events
+ ** GUI events
****/
/// <summary>Raise an <see cref="OnPreRenderGuiEvent"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
@@ -131,13 +91,6 @@ namespace StardewModdingAPI.Events
monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderGuiEvent)}", GraphicsEvents.OnPreRenderGuiEvent?.GetInvocationList());
}
- /// <summary>Raise an <see cref="OnPreRenderGuiEventNoCheck"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal static void InvokeOnPreRenderGuiEventNoCheck(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderGuiEventNoCheck)}", GraphicsEvents.OnPreRenderGuiEventNoCheck?.GetInvocationList());
- }
-
/// <summary>Raise an <see cref="OnPostRenderGuiEvent"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeOnPostRenderGuiEvent(IMonitor monitor)
@@ -145,15 +98,8 @@ namespace StardewModdingAPI.Events
monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderGuiEvent)}", GraphicsEvents.OnPostRenderGuiEvent?.GetInvocationList());
}
- /// <summary>Raise an <see cref="OnPostRenderGuiEventNoCheck"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal static void InvokeOnPostRenderGuiEventNoCheck(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderGuiEventNoCheck)}", GraphicsEvents.OnPostRenderGuiEventNoCheck?.GetInvocationList());
- }
-
/****
- ** GUI events
+ ** HUD events
****/
/// <summary>Raise an <see cref="OnPreRenderHudEvent"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
@@ -162,25 +108,11 @@ namespace StardewModdingAPI.Events
monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderHudEvent)}", GraphicsEvents.OnPreRenderHudEvent?.GetInvocationList());
}
- /// <summary>Raise an <see cref="OnPreRenderHudEventNoCheck"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal static void InvokeOnPreRenderHudEventNoCheck(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderHudEventNoCheck)}", GraphicsEvents.OnPreRenderHudEventNoCheck?.GetInvocationList());
- }
-
/// <summary>Raise an <see cref="OnPostRenderHudEvent"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeOnPostRenderHudEvent(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderHudEvent)}", GraphicsEvents.OnPostRenderHudEvent?.GetInvocationList());
}
-
- /// <summary>Raise an <see cref="OnPostRenderHudEventNoCheck"/> event.</summary>
- /// <param name="monitor">Encapsulates monitoring and logging.</param>
- internal static void InvokeOnPostRenderHudEventNoCheck(IMonitor monitor)
- {
- monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderHudEventNoCheck)}", GraphicsEvents.OnPostRenderHudEventNoCheck?.GetInvocationList());
- }
}
}
diff --git a/src/StardewModdingAPI/Inheritance/ItemStackChange.cs b/src/StardewModdingAPI/Events/ItemStackChange.cs
index 8d15b894..f9ae6df6 100644
--- a/src/StardewModdingAPI/Inheritance/ItemStackChange.cs
+++ b/src/StardewModdingAPI/Events/ItemStackChange.cs
@@ -1,6 +1,6 @@
-using StardewValley;
+using StardewValley;
-namespace StardewModdingAPI.Inheritance
+namespace StardewModdingAPI.Events
{
/// <summary>Represents an inventory slot that changed.</summary>
public class ItemStackChange
diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs
index dd3ff220..b02ebfec 100644
--- a/src/StardewModdingAPI/Events/PlayerEvents.cs
+++ b/src/StardewModdingAPI/Events/PlayerEvents.cs
@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Framework;
-using StardewModdingAPI.Inheritance;
using StardewValley;
+using SFarmer = StardewValley.Farmer;
namespace StardewModdingAPI.Events
{
@@ -11,6 +11,13 @@ namespace StardewModdingAPI.Events
public static class PlayerEvents
{
/*********
+ ** Properties
+ *********/
+ /// <summary>Manages deprecation warnings.</summary>
+ private static DeprecationManager DeprecationManager;
+
+
+ /*********
** Events
*********/
/// <summary>Raised after the player loads a saved game.</summary>
@@ -31,6 +38,13 @@ namespace StardewModdingAPI.Events
/*********
** Internal methods
*********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="deprecationManager">Manages deprecation warnings.</param>
+ internal static void Shim(DeprecationManager deprecationManager)
+ {
+ PlayerEvents.DeprecationManager = deprecationManager;
+ }
+
/// <summary>Raise a <see cref="LoadedGame"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="loaded">Whether the save has been loaded. This is always true.</param>
@@ -42,7 +56,7 @@ namespace StardewModdingAPI.Events
string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}";
Delegate[] handlers = PlayerEvents.LoadedGame.GetInvocationList();
- Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice);
+ PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info);
monitor.SafelyRaiseGenericEvent(name, handlers, null, loaded);
}
@@ -50,7 +64,7 @@ namespace StardewModdingAPI.Events
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="priorFarmer">The previous player character.</param>
/// <param name="newFarmer">The new player character.</param>
- internal static void InvokeFarmerChanged(IMonitor monitor, Farmer priorFarmer, Farmer newFarmer)
+ internal static void InvokeFarmerChanged(IMonitor monitor, SFarmer priorFarmer, SFarmer newFarmer)
{
if (PlayerEvents.FarmerChanged == null)
return;
@@ -58,7 +72,7 @@ namespace StardewModdingAPI.Events
string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}";
Delegate[] handlers = PlayerEvents.FarmerChanged.GetInvocationList();
- Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice);
+ PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info);
monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsFarmerChanged(priorFarmer, newFarmer));
}
diff --git a/src/StardewModdingAPI/Events/SaveEvents.cs b/src/StardewModdingAPI/Events/SaveEvents.cs
index 2921003a..50e6d729 100644
--- a/src/StardewModdingAPI/Events/SaveEvents.cs
+++ b/src/StardewModdingAPI/Events/SaveEvents.cs
@@ -18,6 +18,9 @@ namespace StardewModdingAPI.Events
/// <summary>Raised after the player loads a save slot.</summary>
public static event EventHandler AfterLoad;
+ /// <summary>Raised after the game returns to the title screen.</summary>
+ public static event EventHandler AfterReturnToTitle;
+
/*********
** Internal methods
@@ -42,5 +45,12 @@ namespace StardewModdingAPI.Events
{
monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterLoad)}", SaveEvents.AfterLoad?.GetInvocationList(), null, EventArgs.Empty);
}
+
+ /// <summary>Raise a <see cref="AfterReturnToTitle"/> event.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ internal static void InvokeAfterReturnToTitle(IMonitor monitor)
+ {
+ monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterReturnToTitle)}", SaveEvents.AfterReturnToTitle?.GetInvocationList(), null, EventArgs.Empty);
+ }
}
}
diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs
index dedd7e77..3f06a46b 100644
--- a/src/StardewModdingAPI/Events/TimeEvents.cs
+++ b/src/StardewModdingAPI/Events/TimeEvents.cs
@@ -7,12 +7,22 @@ namespace StardewModdingAPI.Events
public static class TimeEvents
{
/*********
+ ** Properties
+ *********/
+ /// <summary>Manages deprecation warnings.</summary>
+ private static DeprecationManager DeprecationManager;
+
+
+ /*********
** Events
*********/
+ /// <summary>Raised after the game begins a new day, including when loading a save.</summary>
+ public static event EventHandler AfterDayStarted;
+
/// <summary>Raised after the in-game clock changes.</summary>
public static event EventHandler<EventArgsIntChanged> TimeOfDayChanged;
- /// <summary>Raised after the day-of-month value changes, including when loading a save (unlike <see cref="OnNewDay"/>).</summary>
+ /// <summary>Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use <see cref="AfterDayStarted"/> instead.</summary>
public static event EventHandler<EventArgsIntChanged> DayOfMonthChanged;
/// <summary>Raised after the year value changes.</summary>
@@ -22,13 +32,27 @@ namespace StardewModdingAPI.Events
public static event EventHandler<EventArgsStringChanged> SeasonOfYearChanged;
/// <summary>Raised when the player is transitioning to a new day and the game is performing its day update logic. This event is triggered twice: once after the game starts transitioning, and again after it finishes.</summary>
- [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(DayOfMonthChanged) + " or " + nameof(SaveEvents) + " instead")]
+ [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")]
public static event EventHandler<EventArgsNewDay> OnNewDay;
/*********
** Internal methods
*********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="deprecationManager">Manages deprecation warnings.</param>
+ internal static void Shim(DeprecationManager deprecationManager)
+ {
+ TimeEvents.DeprecationManager = deprecationManager;
+ }
+
+ /// <summary>Raise an <see cref="AfterDayStarted"/> event.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ internal static void InvokeAfterDayStarted(IMonitor monitor)
+ {
+ monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty);
+ }
+
/// <summary>Raise a <see cref="InvokeDayOfMonthChanged"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="priorTime">The previous time in military time format (e.g. 6:00pm is 1800).</param>
@@ -78,7 +102,7 @@ namespace StardewModdingAPI.Events
string name = $"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}";
Delegate[] handlers = TimeEvents.OnNewDay.GetInvocationList();
- Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice);
+ TimeEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info);
monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsNewDay(priorDay, newDay, isTransitioning));
}
}
diff --git a/src/StardewModdingAPI/Extensions.cs b/src/StardewModdingAPI/Extensions.cs
deleted file mode 100644
index 0e9dbbf7..00000000
--- a/src/StardewModdingAPI/Extensions.cs
+++ /dev/null
@@ -1,194 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Input;
-using StardewModdingAPI.Framework;
-
-namespace StardewModdingAPI
-{
- /// <summary>Provides general utility extensions.</summary>
- public static class Extensions
- {
- /*********
- ** Properties
- *********/
- /// <summary>The backing field for <see cref="Random"/>.</summary>
- private static readonly Random _random = new Random();
-
-
- /*********
- ** Accessors
- *********/
- /// <summary>A pseudo-random number generator.</summary>
- public static Random Random
- {
- get
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.Random)}", "1.0", DeprecationLevel.PendingRemoval);
- return Extensions._random;
- }
- }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether the given key is currently being pressed.</summary>
- /// <param name="key">The key to check.</param>
- public static bool IsKeyDown(this Keys key)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsKeyDown)}", "1.0", DeprecationLevel.PendingRemoval);
-
- return Keyboard.GetState().IsKeyDown(key);
- }
-
- /// <summary>Get a random color.</summary>
- public static Color RandomColour()
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.RandomColour)}", "1.0", DeprecationLevel.PendingRemoval);
-
- return new Color(Extensions.Random.Next(0, 255), Extensions.Random.Next(0, 255), Extensions.Random.Next(0, 255));
- }
-
- /// <summary>Concatenate an enumeration into a delimiter-separated string.</summary>
- /// <param name="ienum">The values to concatenate.</param>
- /// <param name="split">The value separator.</param>
- [Obsolete("The usage of ToSingular has changed. Please update your call to use ToSingular<T>")]
- public static string ToSingular(this IEnumerable ienum, string split = ", ")
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.ToSingular)}", "0.39.3", DeprecationLevel.PendingRemoval);
- return "";
- }
-
- /// <summary>Concatenate an enumeration into a delimiter-separated string.</summary>
- /// <typeparam name="T">The enumerated value type.</typeparam>
- /// <param name="ienum">The values to concatenate.</param>
- /// <param name="split">The value separator.</param>
- public static string ToSingular<T>(this IEnumerable<T> ienum, string split = ", ")
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.ToSingular)}", "1.0", DeprecationLevel.PendingRemoval);
-
- //Apparently Keys[] won't split normally :l
- if (typeof(T) == typeof(Keys))
- {
- return string.Join(split, ienum.ToArray());
- }
- return string.Join(split, ienum);
- }
-
- /// <summary>Get whether the value can be parsed as a number.</summary>
- /// <param name="o">The value.</param>
- public static bool IsInt32(this object o)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsInt32)}", "1.0", DeprecationLevel.PendingRemoval);
-
- int i;
- return int.TryParse(o.ToString(), out i);
- }
-
- /// <summary>Get the numeric representation of a value.</summary>
- /// <param name="o">The value.</param>
- public static int AsInt32(this object o)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.AsInt32)}", "1.0", DeprecationLevel.PendingRemoval);
-
- return int.Parse(o.ToString());
- }
-
- /// <summary>Get whether the value can be parsed as a boolean.</summary>
- /// <param name="o">The value.</param>
- public static bool IsBool(this object o)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.IsBool)}", "1.0", DeprecationLevel.PendingRemoval);
-
- bool b;
- return bool.TryParse(o.ToString(), out b);
- }
-
- /// <summary>Get the boolean representation of a value.</summary>
- /// <param name="o">The value.</param>
- public static bool AsBool(this object o)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.AsBool)}", "1.0", DeprecationLevel.PendingRemoval);
-
- return bool.Parse(o.ToString());
- }
-
- /// <summary>Get a list hash calculated from the hashes of the values it contains.</summary>
- /// <param name="enumerable">The values to hash.</param>
- public static int GetHash(this IEnumerable enumerable)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetHash)}", "1.0", DeprecationLevel.PendingRemoval);
-
- var hash = 0;
- foreach (var v in enumerable)
- hash ^= v.GetHashCode();
- return hash;
- }
-
- /// <summary>Cast a value to the given type. This returns <c>null</c> if the value can't be cast.</summary>
- /// <typeparam name="T">The type to which to cast.</typeparam>
- /// <param name="o">The value.</param>
- public static T Cast<T>(this object o) where T : class
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.Cast)}", "1.0", DeprecationLevel.PendingRemoval);
-
- return o as T;
- }
-
- /// <summary>Get all private types on an object.</summary>
- /// <param name="o">The object to scan.</param>
- public static FieldInfo[] GetPrivateFields(this object o)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetPrivateFields)}", "1.0", DeprecationLevel.PendingRemoval);
- return o.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
- }
-
- /// <summary>Get metadata for a private field.</summary>
- /// <param name="t">The type to scan.</param>
- /// <param name="name">The name of the field to find.</param>
- public static FieldInfo GetBaseFieldInfo(this Type t, string name)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetBaseFieldValue)}", "1.0", DeprecationLevel.PendingRemoval);
- return t.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
- }
-
- /// <summary>Get the value of a private field.</summary>
- /// <param name="t">The type to scan.</param>
- /// <param name="o">The instance for which to get a value.</param>
- /// <param name="name">The name of the field to find.</param>
- public static T GetBaseFieldValue<T>(this Type t, object o, string name) where T : class
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.GetBaseFieldValue)}", "1.0", DeprecationLevel.PendingRemoval);
- return t.GetBaseFieldInfo(name).GetValue(o) as T;
- }
-
- /// <summary>Set the value of a private field.</summary>
- /// <param name="t">The type to scan.</param>
- /// <param name="o">The instance for which to set a value.</param>
- /// <param name="name">The name of the field to find.</param>
- /// <param name="newValue">The value to set.</param>
- public static void SetBaseFieldValue<T>(this Type t, object o, string name, object newValue) where T : class
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.SetBaseFieldValue)}", "1.0", DeprecationLevel.PendingRemoval);
- t.GetBaseFieldInfo(name).SetValue(o, newValue as T);
- }
-
- /// <summary>Get a copy of the string with only alphanumeric characters. (Numbers are not removed, despite the name.)</summary>
- /// <param name="st">The string to copy.</param>
- public static string RemoveNumerics(this string st)
- {
- Program.DeprecationManager.Warn($"{nameof(Extensions)}.{nameof(Extensions.RemoveNumerics)}", "1.0", DeprecationLevel.PendingRemoval);
- var s = st;
- foreach (var c in s)
- {
- if (!char.IsLetterOrDigit(c))
- s = s.Replace(c.ToString(), "");
- }
- return s;
- }
- }
-} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs
index 123211b9..f6fe89f5 100644
--- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs
+++ b/src/StardewModdingAPI/Framework/AssemblyLoader.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;
-using Mono.Cecil.Rocks;
using StardewModdingAPI.AssemblyRewriters;
namespace StardewModdingAPI.Framework
@@ -55,14 +54,17 @@ namespace StardewModdingAPI.Framework
/// <summary>Preprocess and load an assembly.</summary>
/// <param name="assemblyPath">The assembly file path.</param>
+ /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param>
/// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns>
- public Assembly Load(string assemblyPath)
+ /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception>
+ public Assembly Load(string assemblyPath, bool assumeCompatible)
{
// get referenced local assemblies
AssemblyParseResult[] assemblies;
{
AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver();
- assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), new HashSet<string>(), resolver).ToArray();
+ HashSet<string> visitedAssemblyNames = new HashSet<string>(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded
+ assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray();
if (!assemblies.Any())
throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist.");
resolver.Add(assemblies.Select(p => p.Definition).ToArray());
@@ -72,10 +74,10 @@ namespace StardewModdingAPI.Framework
Assembly lastAssembly = null;
foreach (AssemblyParseResult assembly in assemblies)
{
- this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace);
- bool changed = this.RewriteAssembly(assembly.Definition);
+ bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible);
if (changed)
{
+ this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace);
using (MemoryStream outStream = new MemoryStream())
{
assembly.Definition.Write(outStream);
@@ -84,7 +86,10 @@ namespace StardewModdingAPI.Framework
}
}
else
+ {
+ this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace);
lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName);
+ }
}
// last assembly loaded is the root
@@ -116,18 +121,16 @@ namespace StardewModdingAPI.Framework
****/
/// <summary>Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root.</summary>
/// <param name="file">The assembly file to load.</param>
- /// <param name="visitedAssemblyPaths">The assembly paths that should be skipped.</param>
+ /// <param name="visitedAssemblyNames">The assembly names that should be skipped.</param>
+ /// <param name="assemblyResolver">A resolver which resolves references to known assemblies.</param>
/// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns>
- private IEnumerable<AssemblyParseResult> GetReferencedLocalAssemblies(FileInfo file, HashSet<string> visitedAssemblyPaths, IAssemblyResolver assemblyResolver)
+ private IEnumerable<AssemblyParseResult> GetReferencedLocalAssemblies(FileInfo file, HashSet<string> visitedAssemblyNames, IAssemblyResolver assemblyResolver)
{
// validate
if (file.Directory == null)
throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'.");
- if (visitedAssemblyPaths.Contains(file.FullName))
- yield break; // already visited
if (!file.Exists)
yield break; // not a local assembly
- visitedAssemblyPaths.Add(file.FullName);
// read assembly
byte[] assemblyBytes = File.ReadAllBytes(file.FullName);
@@ -135,11 +138,16 @@ namespace StardewModdingAPI.Framework
using (Stream readStream = new MemoryStream(assemblyBytes))
assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver });
+ // skip if already visited
+ if (visitedAssemblyNames.Contains(assembly.Name.Name))
+ yield break;
+ visitedAssemblyNames.Add(assembly.Name.Name);
+
// yield referenced assemblies
foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences)
{
FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll"));
- foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyPaths, assemblyResolver))
+ foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver))
yield return result;
}
@@ -152,62 +160,88 @@ namespace StardewModdingAPI.Framework
****/
/// <summary>Rewrite the types referenced by an assembly.</summary>
/// <param name="assembly">The assembly to rewrite.</param>
+ /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param>
/// <returns>Returns whether the assembly was modified.</returns>
- private bool RewriteAssembly(AssemblyDefinition assembly)
+ /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception>
+ private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible)
{
- ModuleDefinition module = assembly.Modules.Single(); // technically an assembly can have multiple modules, but none of the build tools (including MSBuild) support it; simplify by assuming one module
+ ModuleDefinition module = assembly.MainModule;
+ HashSet<string> loggedMessages = new HashSet<string>();
- // remove old assembly references
- bool shouldRewrite = false;
+ // swap assembly references if needed (e.g. XNA => MonoGame)
+ bool platformChanged = false;
for (int i = 0; i < module.AssemblyReferences.Count; i++)
{
+ // remove old assembly reference
if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name))
{
- shouldRewrite = true;
+ this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS...");
+ platformChanged = true;
module.AssemblyReferences.RemoveAt(i);
i--;
}
}
- if (!shouldRewrite)
- return false;
-
- // add target assembly references
- foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values)
- module.AssemblyReferences.Add(target);
+ if (platformChanged)
+ {
+ // add target assembly references
+ foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values)
+ module.AssemblyReferences.Add(target);
- // rewrite type scopes to use target assemblies
- IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName);
- foreach (TypeReference type in typeReferences)
- this.ChangeTypeScope(type);
+ // rewrite type scopes to use target assemblies
+ IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName);
+ foreach (TypeReference type in typeReferences)
+ this.ChangeTypeScope(type);
+ }
- // rewrite incompatible methods
- IMethodRewriter[] methodRewriters = Constants.GetMethodRewriters().ToArray();
+ // find (and optionally rewrite) incompatible instructions
+ bool anyRewritten = false;
+ IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray();
foreach (MethodDefinition method in this.GetMethods(module))
{
- // skip methods with no rewritable method
- bool hasMethodToRewrite = method.Body.Instructions.Any(op => (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt) && methodRewriters.Any(rewriter => rewriter.ShouldRewrite((MethodReference)op.Operand)));
- if (!hasMethodToRewrite)
- continue;
+ // check method definition
+ foreach (IInstructionRewriter rewriter in rewriters)
+ {
+ try
+ {
+ if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged))
+ {
+ this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} 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 {assembly.Name.Name}.");
+ this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn);
+ }
+ }
- // rewrite method references
- method.Body.SimplifyMacros();
+ // check CIL instructions
ILProcessor cil = method.Body.GetILProcessor();
- Instruction[] instructions = cil.Body.Instructions.ToArray();
- foreach (Instruction op in instructions)
+ foreach (Instruction instruction in cil.Body.Instructions.ToArray())
{
- if (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt)
+ foreach (IInstructionRewriter rewriter in rewriters)
{
- IMethodRewriter rewriter = methodRewriters.FirstOrDefault(p => p.ShouldRewrite((MethodReference)op.Operand));
- if (rewriter != null)
+ try
{
- MethodReference methodRef = (MethodReference)op.Operand;
- rewriter.Rewrite(module, cil, op, methodRef, this.AssemblyMap);
+ if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged))
+ {
+ this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} 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 {assembly.Name.Name}.");
+ this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn);
}
}
}
- method.Body.OptimizeMacros();
}
- return true;
+
+ return platformChanged || anyRewritten;
}
/// <summary>Get the correct reference to use for compatibility with the current platform.</summary>
@@ -240,5 +274,19 @@ namespace StardewModdingAPI.Framework
select method
);
}
+
+ /// <summary>Log a message for the player or developer the first time it occurs.</summary>
+ /// <param name="monitor">The monitor through which to log the message.</param>
+ /// <param name="hash">The hash of logged messages.</param>
+ /// <param name="message">The message to log.</param>
+ /// <param name="level">The log severity level.</param>
+ private void LogOnce(IMonitor monitor, HashSet<string> hash, string message, LogLevel level = LogLevel.Trace)
+ {
+ if (!hash.Contains(message))
+ {
+ this.Monitor.Log(message, level);
+ hash.Add(message);
+ }
+ }
}
}
diff --git a/src/StardewModdingAPI/Framework/Command.cs b/src/StardewModdingAPI/Framework/Command.cs
new file mode 100644
index 00000000..943e018d
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Command.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>A command that can be submitted through the SMAPI console to interact with SMAPI.</summary>
+ internal class Command
+ {
+ /*********
+ ** Accessor
+ *********/
+ /// <summary>The friendly name for the mod that registered the command.</summary>
+ public string ModName { get; }
+
+ /// <summary>The command name, which the user must type to trigger it.</summary>
+ public string Name { get; }
+
+ /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
+ public string Documentation { get; }
+
+ /// <summary>The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</summary>
+ public Action<string, string[]> Callback { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modName">The friendly name for the mod that registered the command.</param>
+ /// <param name="name">The command name, which the user must type to trigger it.</param>
+ /// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
+ /// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
+ public Command(string modName, string name, string documentation, Action<string, string[]> callback)
+ {
+ this.ModName = modName;
+ this.Name = name;
+ this.Documentation = documentation;
+ this.Callback = callback;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/CommandHelper.cs b/src/StardewModdingAPI/Framework/CommandHelper.cs
new file mode 100644
index 00000000..2e9dea8e
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/CommandHelper.cs
@@ -0,0 +1,53 @@
+using System;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>Provides an API for managing console commands.</summary>
+ internal class CommandHelper : ICommandHelper
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The friendly mod name for this instance.</summary>
+ private readonly string ModName;
+
+ /// <summary>Manages console commands.</summary>
+ private readonly CommandManager CommandManager;
+
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="modName">The friendly mod name for this instance.</param>
+ /// <param name="commandManager">Manages console commands.</param>
+ public CommandHelper(string modName, CommandManager commandManager)
+ {
+ this.ModName = modName;
+ this.CommandManager = commandManager;
+ }
+
+ /// <summary>Add a console command.</summary>
+ /// <param name="name">The command name, which the user must type to trigger it.</param>
+ /// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
+ /// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
+ /// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
+ /// <exception cref="ArgumentException">There's already a command with that name.</exception>
+ public ICommandHelper Add(string name, string documentation, Action<string, string[]> callback)
+ {
+ this.CommandManager.Add(this.ModName, name, documentation, callback);
+ return this;
+ }
+
+ /// <summary>Trigger a command.</summary>
+ /// <param name="name">The command name.</param>
+ /// <param name="arguments">The command arguments.</param>
+ /// <returns>Returns whether a matching command was triggered.</returns>
+ public bool Trigger(string name, string[] arguments)
+ {
+ return this.CommandManager.Trigger(name, arguments);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/StardewModdingAPI/Framework/CommandManager.cs
new file mode 100644
index 00000000..9af3d27a
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/CommandManager.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>Manages console commands.</summary>
+ internal class CommandManager
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The commands registered with SMAPI.</summary>
+ private readonly IDictionary<string, Command> Commands = new Dictionary<string, Command>(StringComparer.InvariantCultureIgnoreCase);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Add a console command.</summary>
+ /// <param name="modName">The friendly mod name for this instance.</param>
+ /// <param name="name">The command name, which the user must type to trigger it.</param>
+ /// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
+ /// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
+ /// <param name="allowNullCallback">Whether to allow a null <paramref name="callback"/> argument; this should only used for backwards compatibility.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
+ /// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
+ /// <exception cref="ArgumentException">There's already a command with that name.</exception>
+ public void Add(string modName, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false)
+ {
+ name = this.GetNormalisedName(name);
+
+ // validate format
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentNullException(nameof(name), "Can't register a command with no name.");
+ if (name.Any(char.IsWhiteSpace))
+ throw new FormatException($"Can't register the '{name}' command because the name can't contain whitespace.");
+ if (callback == null && !allowNullCallback)
+ throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback.");
+
+ // ensure uniqueness
+ if (this.Commands.ContainsKey(name))
+ throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name.");
+
+ // add command
+ this.Commands.Add(name, new Command(modName, name, documentation, callback));
+ }
+
+ /// <summary>Get a command by its unique name.</summary>
+ /// <param name="name">The command name.</param>
+ /// <returns>Returns the matching command, or <c>null</c> if not found.</returns>
+ public Command Get(string name)
+ {
+ name = this.GetNormalisedName(name);
+ Command command;
+ this.Commands.TryGetValue(name, out command);
+ return command;
+ }
+
+ /// <summary>Get all registered commands.</summary>
+ public IEnumerable<Command> GetAll()
+ {
+ return this.Commands
+ .Values
+ .OrderBy(p => p.Name);
+ }
+
+ /// <summary>Trigger a command.</summary>
+ /// <param name="input">The raw command input.</param>
+ /// <returns>Returns whether a matching command was triggered.</returns>
+ public bool Trigger(string input)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ return false;
+
+ string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ string name = args[0];
+ args = args.Skip(1).ToArray();
+
+ return this.Trigger(name, args);
+ }
+
+ /// <summary>Trigger a command.</summary>
+ /// <param name="name">The command name.</param>
+ /// <param name="arguments">The command arguments.</param>
+ /// <returns>Returns whether a matching command was triggered.</returns>
+ public bool Trigger(string name, string[] arguments)
+ {
+ // get normalised name
+ name = this.GetNormalisedName(name);
+ if (name == null)
+ return false;
+
+ // get command
+ Command command;
+ if (this.Commands.TryGetValue(name, out command))
+ {
+ command.Callback.Invoke(name, arguments);
+ return true;
+ }
+ return false;
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a normalised command name.</summary>
+ /// <param name="name">The command name.</param>
+ private string GetNormalisedName(string name)
+ {
+ name = name?.Trim().ToLower();
+ return !string.IsNullOrWhiteSpace(name)
+ ? name
+ : null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventData.cs b/src/StardewModdingAPI/Framework/Content/ContentEventData.cs
new file mode 100644
index 00000000..1a1779d4
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Content/ContentEventData.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>Base implementation for a content helper which encapsulates access and changes to content being read from a data file.</summary>
+ /// <typeparam name="TValue">The interface value type.</typeparam>
+ internal class ContentEventData<TValue> : EventArgs, IContentEventData<TValue>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Normalises an asset key to match the cache key.</summary>
+ protected readonly Func<string, string> GetNormalisedPath;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The content's locale code, if the content is localised.</summary>
+ public string Locale { get; }
+
+ /// <summary>The normalised asset name being read. The format may change between platforms; see <see cref="IsAssetName"/> to compare with a known path.</summary>
+ public string AssetName { get; }
+
+ /// <summary>The content data being read.</summary>
+ public TValue Data { get; protected set; }
+
+ /// <summary>The content data type.</summary>
+ public Type DataType { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locale">The content's locale code, if the content is localised.</param>
+ /// <param name="assetName">The normalised asset name being read.</param>
+ /// <param name="data">The content data being read.</param>
+ /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ public ContentEventData(string locale, string assetName, TValue data, Func<string, string> getNormalisedPath)
+ : this(locale, assetName, data, data.GetType(), getNormalisedPath) { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locale">The content's locale code, if the content is localised.</param>
+ /// <param name="assetName">The normalised asset name being read.</param>
+ /// <param name="data">The content data being read.</param>
+ /// <param name="dataType">The content data type being read.</param>
+ /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ public ContentEventData(string locale, string assetName, TValue data, Type dataType, Func<string, string> getNormalisedPath)
+ {
+ this.Locale = locale;
+ this.AssetName = assetName;
+ this.Data = data;
+ this.DataType = dataType;
+ this.GetNormalisedPath = getNormalisedPath;
+ }
+
+ /// <summary>Get whether the asset name being loaded matches a given name after normalisation.</summary>
+ /// <param name="path">The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation').</param>
+ public bool IsAssetName(string path)
+ {
+ path = this.GetNormalisedPath(path);
+ return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ /// <summary>Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game.</summary>
+ /// <param name="value">The new content value.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="value"/> is null.</exception>
+ /// <exception cref="InvalidCastException">The <paramref name="value"/>'s type is not compatible with the loaded asset's type.</exception>
+ public void ReplaceWith(TValue value)
+ {
+ if (value == null)
+ throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value.");
+ if (!this.DataType.IsInstanceOfType(value))
+ throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors.");
+
+ this.Data = value;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get a human-readable type name.</summary>
+ /// <param name="type">The type to name.</param>
+ protected string GetFriendlyTypeName(Type type)
+ {
+ // dictionary
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
+ {
+ Type[] genericArgs = type.GetGenericArguments();
+ return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>";
+ }
+
+ // texture
+ if (type == typeof(Texture2D))
+ return type.Name;
+
+ // native type
+ if (type == typeof(int))
+ return "int";
+ if (type == typeof(string))
+ return "string";
+
+ // default
+ return type.FullName;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs
new file mode 100644
index 00000000..9bf1ea17
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>Encapsulates access and changes to content being read from a data file.</summary>
+ internal class ContentEventHelper : ContentEventData<object>, IContentEventHelper
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locale">The content's locale code, if the content is localised.</param>
+ /// <param name="assetName">The normalised asset name being read.</param>
+ /// <param name="data">The content data being read.</param>
+ /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ public ContentEventHelper(string locale, string assetName, object data, Func<string, string> getNormalisedPath)
+ : base(locale, assetName, data, getNormalisedPath) { }
+
+ /// <summary>Get a helper to manipulate the data as a dictionary.</summary>
+ /// <typeparam name="TKey">The expected dictionary key.</typeparam>
+ /// <typeparam name="TValue">The expected dictionary balue.</typeparam>
+ /// <exception cref="InvalidOperationException">The content being read isn't a dictionary.</exception>
+ public IContentEventHelperForDictionary<TKey, TValue> AsDictionary<TKey, TValue>()
+ {
+ return new ContentEventHelperForDictionary<TKey, TValue>(this.Locale, this.AssetName, this.GetData<IDictionary<TKey, TValue>>(), this.GetNormalisedPath);
+ }
+
+ /// <summary>Get a helper to manipulate the data as an image.</summary>
+ /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
+ public IContentEventHelperForImage AsImage()
+ {
+ return new ContentEventHelperForImage(this.Locale, this.AssetName, this.GetData<Texture2D>(), this.GetNormalisedPath);
+ }
+
+ /// <summary>Get the data as a given type.</summary>
+ /// <typeparam name="TData">The expected data type.</typeparam>
+ /// <exception cref="InvalidCastException">The data can't be converted to <typeparamref name="TData"/>.</exception>
+ public TData GetData<TData>()
+ {
+ if (!(this.Data is TData))
+ throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}.");
+ return (TData)this.Data;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs
new file mode 100644
index 00000000..26f059e4
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>Encapsulates access and changes to dictionary content being read from a data file.</summary>
+ internal class ContentEventHelperForDictionary<TKey, TValue> : ContentEventData<IDictionary<TKey, TValue>>, IContentEventHelperForDictionary<TKey, TValue>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locale">The content's locale code, if the content is localised.</param>
+ /// <param name="assetName">The normalised asset name being read.</param>
+ /// <param name="data">The content data being read.</param>
+ /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ public ContentEventHelperForDictionary(string locale, string assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalisedPath)
+ : base(locale, assetName, data, getNormalisedPath) { }
+
+ /// <summary>Add or replace an entry in the dictionary.</summary>
+ /// <param name="key">The entry key.</param>
+ /// <param name="value">The entry value.</param>
+ public void Set(TKey key, TValue value)
+ {
+ this.Data[key] = value;
+ }
+
+ /// <summary>Add or replace an entry in the dictionary.</summary>
+ /// <param name="key">The entry key.</param>
+ /// <param name="value">A callback which accepts the current value and returns the new value.</param>
+ public void Set(TKey key, Func<TValue, TValue> value)
+ {
+ this.Data[key] = value(this.Data[key]);
+ }
+
+ /// <summary>Dynamically replace values in the dictionary.</summary>
+ /// <param name="replacer">A lambda which takes the current key and value for an entry, and returns the new value.</param>
+ public void Set(Func<TKey, TValue, TValue> replacer)
+ {
+ foreach (var pair in this.Data.ToArray())
+ this.Data[pair.Key] = replacer(pair.Key, pair.Value);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs
new file mode 100644
index 00000000..da30590b
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs
@@ -0,0 +1,70 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>Encapsulates access and changes to dictionary content being read from a data file.</summary>
+ internal class ContentEventHelperForImage : ContentEventData<Texture2D>, IContentEventHelperForImage
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locale">The content's locale code, if the content is localised.</param>
+ /// <param name="assetName">The normalised asset name being read.</param>
+ /// <param name="data">The content data being read.</param>
+ /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ public ContentEventHelperForImage(string locale, string assetName, Texture2D data, Func<string, string> getNormalisedPath)
+ : base(locale, assetName, data, getNormalisedPath) { }
+
+ /// <summary>Overwrite part of the image.</summary>
+ /// <param name="source">The image to patch into the content.</param>
+ /// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> texture.</param>
+ /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param>
+ /// <param name="patchMode">Indicates how an image should be patched.</param>
+ /// <exception cref="ArgumentNullException">One of the arguments is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception>
+ public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
+ {
+ // get texture
+ Texture2D target = this.Data;
+
+ // get areas
+ sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height);
+ targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height));
+
+ // validate
+ if (source == null)
+ throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture.");
+ if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height)
+ throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
+ if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height)
+ throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture.");
+ if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height)
+ throw new InvalidOperationException("The source and target areas must be the same size.");
+
+ // get source data
+ int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height;
+ Color[] sourceData = new Color[pixelCount];
+ source.GetData(0, sourceArea, sourceData, 0, pixelCount);
+
+ // merge data in overlay mode
+ if (patchMode == PatchMode.Overlay)
+ {
+ Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height];
+ target.GetData(0, targetArea, newData, 0, newData.Length);
+ for (int i = 0; i < sourceData.Length; i++)
+ {
+ Color pixel = sourceData[i];
+ if (pixel.A != 0) // not transparent
+ newData[i] = pixel;
+ }
+ sourceData = newData;
+ }
+
+ // patch target texture
+ target.SetData(0, targetArea, sourceData, 0, pixelCount);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs
index 8c32ba6a..e44cd369 100644
--- a/src/StardewModdingAPI/Framework/DeprecationManager.cs
+++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs
@@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework
break;
case DeprecationLevel.Info:
- this.Monitor.Log(message, LogLevel.Info);
+ this.Monitor.Log(message, LogLevel.Warn);
break;
case DeprecationLevel.PendingRemoval:
diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs
index c4bd2d35..4ca79518 100644
--- a/src/StardewModdingAPI/Framework/InternalExtensions.cs
+++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs
@@ -9,8 +9,22 @@ namespace StardewModdingAPI.Framework
internal static class InternalExtensions
{
/*********
+ ** Properties
+ *********/
+ /// <summary>Tracks the installed mods.</summary>
+ private static ModRegistry ModRegistry;
+
+
+ /*********
** Public methods
*********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="modRegistry">Tracks the installed mods.</param>
+ internal static void Shim(ModRegistry modRegistry)
+ {
+ InternalExtensions.ModRegistry = modRegistry;
+ }
+
/****
** IMonitor
****/
@@ -103,7 +117,7 @@ namespace StardewModdingAPI.Framework
foreach (Delegate handler in handlers)
{
- string modName = Program.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here
+ string modName = InternalExtensions.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here
deprecationManager.Warn(modName, nounPhrase, version, severity);
}
}
diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs
new file mode 100644
index 00000000..d84671ee
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs
@@ -0,0 +1,86 @@
+using System;
+
+namespace StardewModdingAPI.Framework.Logging
+{
+ /// <summary>Manages console output interception.</summary>
+ internal class ConsoleInterceptionManager : IDisposable
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The intercepting console writer.</summary>
+ private readonly InterceptingTextWriter Output;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the current console supports color formatting.</summary>
+ public bool SupportsColor { get; }
+
+ /// <summary>The event raised when something writes a line to the console directly.</summary>
+ public event Action<string> OnLineIntercepted;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public ConsoleInterceptionManager()
+ {
+ // redirect output through interceptor
+ this.Output = new InterceptingTextWriter(Console.Out);
+ this.Output.OnLineIntercepted += line => this.OnLineIntercepted?.Invoke(line);
+ Console.SetOut(this.Output);
+
+ // test color support
+ this.SupportsColor = this.TestColorSupport();
+ }
+
+ /// <summary>Get an exclusive lock and write to the console output without interception.</summary>
+ /// <param name="action">The action to perform within the exclusive write block.</param>
+ public void ExclusiveWriteWithoutInterception(Action action)
+ {
+ lock (Console.Out)
+ {
+ try
+ {
+ this.Output.ShouldIntercept = false;
+ action();
+ }
+ finally
+ {
+ this.Output.ShouldIntercept = true;
+ }
+ }
+ }
+
+ /// <summary>Release all resources.</summary>
+ public void Dispose()
+ {
+ Console.SetOut(this.Output.Out);
+ this.Output.Dispose();
+ }
+
+
+ /*********
+ ** private methods
+ *********/
+ /// <summary>Test whether the current console supports color formatting.</summary>
+ private bool TestColorSupport()
+ {
+ try
+ {
+ this.ExclusiveWriteWithoutInterception(() =>
+ {
+ Console.ForegroundColor = Console.ForegroundColor;
+ });
+ return true;
+ }
+ catch (Exception)
+ {
+ return false; // Mono bug
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs
new file mode 100644
index 00000000..14789109
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace StardewModdingAPI.Framework.Logging
+{
+ /// <summary>A text writer which allows intercepting output.</summary>
+ internal class InterceptingTextWriter : TextWriter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The current line being intercepted.</summary>
+ private readonly List<char> Line = new List<char>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The underlying console output.</summary>
+ public TextWriter Out { get; }
+
+ /// <summary>The character encoding in which the output is written.</summary>
+ public override Encoding Encoding => this.Out.Encoding;
+
+ /// <summary>Whether to intercept console output.</summary>
+ public bool ShouldIntercept { get; set; }
+
+ /// <summary>The event raised when a line of text is intercepted.</summary>
+ public event Action<string> OnLineIntercepted;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="output">The underlying output writer.</param>
+ public InterceptingTextWriter(TextWriter output)
+ {
+ this.Out = output;
+ }
+
+ /// <summary>Writes a character to the text string or stream.</summary>
+ /// <param name="ch">The character to write to the text stream.</param>
+ public override void Write(char ch)
+ {
+ // intercept
+ if (this.ShouldIntercept)
+ {
+ switch (ch)
+ {
+ case '\r':
+ return;
+
+ case '\n':
+ this.OnLineIntercepted?.Invoke(new string(this.Line.ToArray()));
+ this.Line.Clear();
+ break;
+
+ default:
+ this.Line.Add(ch);
+ break;
+ }
+ }
+
+ // pass through
+ else
+ this.Out.Write(ch);
+ }
+
+ /// <summary>Releases the unmanaged resources used by the <see cref="T:System.IO.TextWriter" /> and optionally releases the managed resources.</summary>
+ /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
+ protected override void Dispose(bool disposing)
+ {
+ this.OnLineIntercepted = null;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/LogFileManager.cs b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs
index c2a2105b..1f6ade1d 100644
--- a/src/StardewModdingAPI/Framework/LogFileManager.cs
+++ b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-namespace StardewModdingAPI.Framework
+namespace StardewModdingAPI.Framework.Logging
{
/// <summary>Manages reading and writing to log file.</summary>
internal class LogFileManager : IDisposable
@@ -34,7 +34,9 @@ namespace StardewModdingAPI.Framework
/// <param name="message">The message to log.</param>
public void WriteLine(string message)
{
- this.Stream.WriteLine(message);
+ // always use Windows-style line endings for convenience
+ // (Linux/Mac editors are fine with them, Windows editors often require them)
+ this.Stream.Write(message + "\r\n");
}
/// <summary>Release all resources.</summary>
@@ -43,4 +45,4 @@ namespace StardewModdingAPI.Framework
this.Stream.Dispose();
}
}
-} \ No newline at end of file
+}
diff --git a/src/StardewModdingAPI/Framework/Manifest.cs b/src/StardewModdingAPI/Framework/Manifest.cs
new file mode 100644
index 00000000..189da9a8
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Manifest.cs
@@ -0,0 +1,39 @@
+using System;
+using Newtonsoft.Json;
+using StardewModdingAPI.Framework.Serialisation;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>A manifest which describes a mod for SMAPI.</summary>
+ internal class Manifest : IManifest
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>A brief description of the mod.</summary>
+ public string Description { get; set; }
+
+ /// <summary>The mod author's name.</summary>
+ public string Author { get; set; }
+
+ /// <summary>The mod version.</summary>
+ [JsonConverter(typeof(SemanticVersionConverter))]
+ public ISemanticVersion Version { get; set; }
+
+ /// <summary>The minimum SMAPI version required by this mod, if any.</summary>
+ public string MinimumApiVersion { get; set; }
+
+ /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary>
+ public string EntryDll { get; set; }
+
+ /// <summary>The unique mod ID.</summary>
+ public string UniqueID { get; set; }
+
+ /// <summary>Whether the mod uses per-save config files.</summary>
+ [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")]
+ public bool PerSaveConfigs { get; set; }
+ }
+}
diff --git a/src/StardewModdingAPI/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs
index c20130cf..c8c44dba 100644
--- a/src/StardewModdingAPI/ModHelper.cs
+++ b/src/StardewModdingAPI/Framework/ModHelper.cs
@@ -1,24 +1,18 @@
using System;
using System.IO;
-using Newtonsoft.Json;
-using StardewModdingAPI.Advanced;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Framework.Serialisation;
-namespace StardewModdingAPI
+namespace StardewModdingAPI.Framework
{
/// <summary>Provides simplified APIs for writing mods.</summary>
- [Obsolete("Use " + nameof(IModHelper) + " instead.")] // only direct mod access to this class is obsolete
- public class ModHelper : IModHelper
+ internal class ModHelper : IModHelper
{
/*********
** Properties
*********/
- /// <summary>The JSON settings to use when serialising and deserialising files.</summary>
- private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
- {
- Formatting = Formatting.Indented,
- ObjectCreationHandling = ObjectCreationHandling.Replace // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
- };
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ private readonly JsonHelper JsonHelper;
/*********
@@ -33,28 +27,38 @@ namespace StardewModdingAPI
/// <summary>Metadata about loaded mods.</summary>
public IModRegistry ModRegistry { get; }
+ /// <summary>An API for managing console commands.</summary>
+ public ICommandHelper ConsoleCommands { get; }
+
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <param name="modName">The friendly mod name.</param>
/// <param name="modDirectory">The mod directory path.</param>
+ /// <param name="jsonHelper">Encapsulate SMAPI's JSON parsing.</param>
/// <param name="modRegistry">Metadata about loaded mods.</param>
- /// <exception cref="ArgumentException">An argument is null or invalid.</exception>
+ /// <param name="commandManager">Manages console commands.</param>
+ /// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
- public ModHelper(string modDirectory, IModRegistry modRegistry)
+ public ModHelper(string modName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager)
{
// validate
- if (modRegistry == null)
- throw new ArgumentException("The mod registry cannot be null.");
if (string.IsNullOrWhiteSpace(modDirectory))
- throw new ArgumentException("The mod directory cannot be empty.");
+ throw new ArgumentNullException(nameof(modDirectory));
+ if (jsonHelper == null)
+ throw new ArgumentNullException(nameof(jsonHelper));
+ if (modRegistry == null)
+ throw new ArgumentNullException(nameof(modRegistry));
if (!Directory.Exists(modDirectory))
throw new InvalidOperationException("The specified mod directory does not exist.");
// initialise
+ this.JsonHelper = jsonHelper;
this.DirectoryPath = modDirectory;
this.ModRegistry = modRegistry;
+ this.ConsoleCommands = new CommandHelper(modName, commandManager);
}
/****
@@ -65,7 +69,7 @@ namespace StardewModdingAPI
public TConfig ReadConfig<TConfig>()
where TConfig : class, new()
{
- var config = this.ReadJsonFile<TConfig>("config.json") ?? new TConfig();
+ TConfig config = this.ReadJsonFile<TConfig>("config.json") ?? new TConfig();
this.WriteConfig(config); // create file or fill in missing fields
return config;
}
@@ -89,28 +93,8 @@ namespace StardewModdingAPI
public TModel ReadJsonFile<TModel>(string path)
where TModel : class
{
- // read file
- string fullPath = Path.Combine(this.DirectoryPath, path);
- string json;
- try
- {
- json = File.ReadAllText(fullPath);
- }
- catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException)
- {
- return null;
- }
-
- // deserialise model
- TModel model = JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings);
- if (model is IConfigFile)
- {
- var wrapper = (IConfigFile)model;
- wrapper.ModHelper = this;
- wrapper.FilePath = path;
- }
-
- return model;
+ path = Path.Combine(this.DirectoryPath, path);
+ return this.JsonHelper.ReadJsonFile<TModel>(path);
}
/// <summary>Save to a JSON file.</summary>
@@ -121,15 +105,7 @@ namespace StardewModdingAPI
where TModel : class
{
path = Path.Combine(this.DirectoryPath, path);
-
- // create directory if needed
- string dir = Path.GetDirectoryName(path);
- if (!Directory.Exists(dir))
- Directory.CreateDirectory(dir);
-
- // write file
- string json = JsonConvert.SerializeObject(model, this.JsonSettings);
- File.WriteAllText(path, json);
+ this.JsonHelper.WriteJsonFile(path, model);
}
}
}
diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs
index 209f1928..f015b7ba 100644
--- a/src/StardewModdingAPI/Framework/ModRegistry.cs
+++ b/src/StardewModdingAPI/Framework/ModRegistry.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
+using StardewModdingAPI.Framework.Models;
namespace StardewModdingAPI.Framework
{
@@ -18,10 +19,21 @@ namespace StardewModdingAPI.Framework
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
+ /// <summary>Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ private readonly ModCompatibility[] CompatibilityRecords;
+
/*********
** Public methods
*********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
+ public ModRegistry(IEnumerable<ModCompatibility> compatibilityRecords)
+ {
+ this.CompatibilityRecords = compatibilityRecords.ToArray();
+ }
+
+
/****
** IModRegistry
****/
@@ -113,5 +125,21 @@ namespace StardewModdingAPI.Framework
// no known assembly found
return null;
}
+
+ /// <summary>Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ /// <param name="manifest">The mod manifest.</param>
+ /// <returns>Returns the incompatibility record if applicable, else <c>null</c>.</returns>
+ internal ModCompatibility GetCompatibilityRecord(IManifest manifest)
+ {
+ string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
+ return (
+ from mod in this.CompatibilityRecords
+ where
+ mod.ID == key
+ && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
+ && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
+ select mod
+ ).FirstOrDefault();
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs b/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs
deleted file mode 100644
index bcf5639c..00000000
--- a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System.Text.RegularExpressions;
-
-namespace StardewModdingAPI.Framework.Models
-{
- /// <summary>Contains abstract metadata about an incompatible mod.</summary>
- internal class IncompatibleMod
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The unique mod ID.</summary>
- public string ID { get; set; }
-
- /// <summary>The mod name.</summary>
- public string Name { get; set; }
-
- /// <summary>The oldest incompatible mod version, or <c>null</c> for all past versions.</summary>
- public string LowerVersion { get; set; }
-
- /// <summary>The most recent incompatible mod version.</summary>
- public string UpperVersion { get; set; }
-
- /// <summary>The URL the user can check for an official updated version.</summary>
- public string UpdateUrl { get; set; }
-
- /// <summary>The URL the user can check for an unofficial updated version.</summary>
- public string UnofficialUpdateUrl { get; set; }
-
- /// <summary>A regular expression matching version strings to consider compatible, even if they technically precede <see cref="UpperVersion"/>.</summary>
- public string ForceCompatibleVersion { get; set; }
-
- /// <summary>The reason phrase to show in the warning, or <c>null</c> to use the default value.</summary>
- /// <example>"this version is incompatible with the latest version of the game"</example>
- public string ReasonPhrase { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether the specified version is compatible according to this metadata.</summary>
- /// <param name="version">The current version of the matching mod.</param>
- public bool IsCompatible(ISemanticVersion version)
- {
- ISemanticVersion lowerVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null;
- ISemanticVersion upperVersion = new SemanticVersion(this.UpperVersion);
-
- // ignore versions not in range
- if (lowerVersion != null && version.IsOlderThan(lowerVersion))
- return true;
- if (version.IsNewerThan(upperVersion))
- return true;
-
- // allow versions matching override
- return !string.IsNullOrWhiteSpace(this.ForceCompatibleVersion) && Regex.IsMatch(version.ToString(), this.ForceCompatibleVersion, RegexOptions.IgnoreCase);
- }
- }
-}
diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs
new file mode 100644
index 00000000..1e71dae0
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs
@@ -0,0 +1,65 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Framework.Models
+{
+ /// <summary>Metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ internal class ModCompatibility
+ {
+ /*********
+ ** Accessors
+ *********/
+ /****
+ ** From config
+ ****/
+ /// <summary>The unique mod ID.</summary>
+ public string ID { get; set; }
+
+ /// <summary>The mod name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>The oldest incompatible mod version, or <c>null</c> for all past versions.</summary>
+ public string LowerVersion { get; set; }
+
+ /// <summary>The most recent incompatible mod version.</summary>
+ public string UpperVersion { get; set; }
+
+ /// <summary>The URL the user can check for an official updated version.</summary>
+ public string UpdateUrl { get; set; }
+
+ /// <summary>The URL the user can check for an unofficial updated version.</summary>
+ public string UnofficialUpdateUrl { get; set; }
+
+ /// <summary>The reason phrase to show in the warning, or <c>null</c> to use the default value.</summary>
+ /// <example>"this version is incompatible with the latest version of the game"</example>
+ public string ReasonPhrase { get; set; }
+
+ /// <summary>Indicates how SMAPI should consider the mod.</summary>
+ public ModCompatibilityType Compatibility { get; set; }
+
+
+ /****
+ ** Injected
+ ****/
+ /// <summary>The semantic version corresponding to <see cref="LowerVersion"/>.</summary>
+ [JsonIgnore]
+ public ISemanticVersion LowerSemanticVersion { get; set; }
+
+ /// <summary>The semantic version corresponding to <see cref="UpperVersion"/>.</summary>
+ [JsonIgnore]
+ public ISemanticVersion UpperSemanticVersion { get; set; }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method called when the model finishes deserialising.</summary>
+ /// <param name="context">The deserialisation context.</param>
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ this.LowerSemanticVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null;
+ this.UpperSemanticVersion = this.UpperVersion != null ? new SemanticVersion(this.UpperVersion) : null;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs
new file mode 100644
index 00000000..35edec5e
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI.Framework.Models
+{
+ /// <summary>Indicates how SMAPI should consider a mod.</summary>
+ internal enum ModCompatibilityType
+ {
+ /// <summary>Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code.</summary>
+ AssumeBroken = 0,
+
+ /// <summary>Assume the mod is compatible, even if SMAPI detects incompatible code.</summary>
+ AssumeCompatible = 1
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Models/UserSettings.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs
index a0074f77..0de96297 100644
--- a/src/StardewModdingAPI/Framework/Models/UserSettings.cs
+++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs
@@ -1,15 +1,18 @@
namespace StardewModdingAPI.Framework.Models
{
- /// <summary>Contains user settings from SMAPI's JSON configuration file.</summary>
- internal class UserSettings
+ /// <summary>The SMAPI configuration settings.</summary>
+ internal class SConfig
{
- /*********
+ /********
** Accessors
- *********/
+ ********/
/// <summary>Whether to enable development features.</summary>
public bool DeveloperMode { get; set; }
/// <summary>Whether to check if a newer version of SMAPI is available on startup.</summary>
public bool CheckForUpdates { get; set; } = true;
+
+ /// <summary>A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code.</summary>
+ public ModCompatibility[] ModCompatibility { get; set; }
}
}
diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs
index 39b567d8..64075f2f 100644
--- a/src/StardewModdingAPI/Framework/Monitor.cs
+++ b/src/StardewModdingAPI/Framework/Monitor.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using StardewModdingAPI.Framework.Logging;
namespace StardewModdingAPI.Framework
{
@@ -13,6 +14,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The name of the module which logs messages using this instance.</summary>
private readonly string Source;
+ /// <summary>Manages access to the console output.</summary>
+ private readonly ConsoleInterceptionManager ConsoleManager;
+
/// <summary>The log file to which to write messages.</summary>
private readonly LogFileManager LogFile;
@@ -30,27 +34,32 @@ namespace StardewModdingAPI.Framework
[LogLevel.Alert] = ConsoleColor.Magenta
};
+ /// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
+ private RequestExitDelegate RequestExit;
+
/*********
** Accessors
*********/
- /// <summary>Whether the current console supports color codes.</summary>
- internal static readonly bool ConsoleSupportsColor = Monitor.GetConsoleSupportsColor();
-
/// <summary>Whether to show trace messages in the console.</summary>
internal bool ShowTraceInConsole { get; set; }
/// <summary>Whether to write anything to the console. This should be disabled if no console is available.</summary>
internal bool WriteToConsole { get; set; } = true;
+ /// <summary>Whether to write anything to the log file. This should almost always be enabled.</summary>
+ internal bool WriteToFile { get; set; } = true;
+
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="source">The name of the module which logs messages using this instance.</param>
+ /// <param name="consoleManager">Manages access to the console output.</param>
/// <param name="logFile">The log file to which to write messages.</param>
- public Monitor(string source, LogFileManager logFile)
+ /// <param name="requestExitDelegate">A delegate which requests that SMAPI immediately exit the game.</param>
+ public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, RequestExitDelegate requestExitDelegate)
{
// validate
if (string.IsNullOrWhiteSpace(source))
@@ -61,6 +70,7 @@ namespace StardewModdingAPI.Framework
// initialise
this.Source = source;
this.LogFile = logFile;
+ this.ConsoleManager = consoleManager;
}
/// <summary>Log a message for the player or developer.</summary>
@@ -68,23 +78,21 @@ namespace StardewModdingAPI.Framework
/// <param name="level">The log severity level.</param>
public void Log(string message, LogLevel level = LogLevel.Debug)
{
- this.LogImpl(this.Source, message, Monitor.Colors[level], level);
+ this.LogImpl(this.Source, message, level, Monitor.Colors[level]);
}
/// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
/// <param name="reason">The reason for the shutdown.</param>
public void ExitGameImmediately(string reason)
{
- Program.ExitGameImmediately(this.Source, reason);
- Program.gamePtr.Exit();
+ this.RequestExit(this.Source, reason);
}
/// <summary>Log a fatal error message.</summary>
/// <param name="message">The message to log.</param>
internal void LogFatal(string message)
{
- Console.BackgroundColor = ConsoleColor.Red;
- this.LogImpl(this.Source, message, ConsoleColor.White, LogLevel.Error);
+ this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red);
}
/// <summary>Log a message for the player or developer, using the specified console color.</summary>
@@ -95,7 +103,7 @@ namespace StardewModdingAPI.Framework
[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, color, level);
+ this.LogImpl(source, message, level, color);
}
@@ -105,41 +113,34 @@ namespace StardewModdingAPI.Framework
/// <summary>Write a message line to the log.</summary>
/// <param name="source">The name of the mod logging the message.</param>
/// <param name="message">The message to log.</param>
- /// <param name="color">The console color.</param>
/// <param name="level">The log level.</param>
- private void LogImpl(string source, string message, ConsoleColor color, LogLevel level)
+ /// <param name="color">The console foreground color.</param>
+ /// <param name="background">The console background color (or <c>null</c> to leave it as-is).</param>
+ private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null)
{
// generate message
string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength);
message = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}";
- // log
+ // write to console
if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace))
{
- if (Monitor.ConsoleSupportsColor)
+ this.ConsoleManager.ExclusiveWriteWithoutInterception(() =>
{
- Console.ForegroundColor = color;
- Console.WriteLine(message);
- Console.ResetColor();
- }
- else
- Console.WriteLine(message);
+ if (this.ConsoleManager.SupportsColor)
+ {
+ Console.ForegroundColor = color;
+ Console.WriteLine(message);
+ Console.ResetColor();
+ }
+ else
+ Console.WriteLine(message);
+ });
}
- this.LogFile.WriteLine(message);
- }
- /// <summary>Test whether the current console supports color formatting.</summary>
- private static bool GetConsoleSupportsColor()
- {
- try
- {
- Console.ForegroundColor = Console.ForegroundColor;
- return true;
- }
- catch (Exception)
- {
- return false; // Mono bug
- }
+ // write to log file
+ if (this.WriteToFile)
+ this.LogFile.WriteLine(message);
}
}
-} \ No newline at end of file
+}
diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs
new file mode 100644
index 00000000..08204b7e
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Reflection;
+
+namespace StardewModdingAPI.Framework.Reflection
+{
+ /// <summary>A private property obtained through reflection.</summary>
+ /// <typeparam name="TValue">The property value type.</typeparam>
+ internal class PrivateProperty<TValue> : IPrivateProperty<TValue>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The type that has the field.</summary>
+ private readonly Type ParentType;
+
+ /// <summary>The object that has the instance field (if applicable).</summary>
+ private readonly object Parent;
+
+ /// <summary>The display name shown in error messages.</summary>
+ private string DisplayName => $"{this.ParentType.FullName}::{this.PropertyInfo.Name}";
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The reflection metadata.</summary>
+ public PropertyInfo PropertyInfo { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="parentType">The type that has the field.</param>
+ /// <param name="obj">The object that has the instance field (if applicable).</param>
+ /// <param name="property">The reflection metadata.</param>
+ /// <param name="isStatic">Whether the field is static.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="property"/> is null.</exception>
+ /// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception>
+ public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
+ {
+ // validate
+ if (parentType == null)
+ throw new ArgumentNullException(nameof(parentType));
+ if (property == null)
+ throw new ArgumentNullException(nameof(property));
+ if (isStatic && obj != null)
+ throw new ArgumentException("A static property cannot have an object instance.");
+ if (!isStatic && obj == null)
+ throw new ArgumentException("A non-static property must have an object instance.");
+
+ // save
+ this.ParentType = parentType;
+ this.Parent = obj;
+ this.PropertyInfo = property;
+ }
+
+ /// <summary>Get the property value.</summary>
+ public TValue GetValue()
+ {
+ try
+ {
+ return (TValue)this.PropertyInfo.GetValue(this.Parent);
+ }
+ catch (InvalidCastException)
+ {
+ throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}.");
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex);
+ }
+ }
+
+ /// <summary>Set the property value.</summary>
+ //// <param name="value">The value to set.</param>
+ public void SetValue(TValue value)
+ {
+ try
+ {
+ this.PropertyInfo.SetValue(this.Parent, value);
+ }
+ catch (InvalidCastException)
+ {
+ throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}.");
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex);
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs
index edf59b81..7a5789dc 100644
--- a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs
+++ b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs
@@ -59,6 +59,41 @@ namespace StardewModdingAPI.Framework.Reflection
}
/****
+ ** Properties
+ ****/
+ /// <summary>Get a private instance property.</summary>
+ /// <typeparam name="TValue">The property type.</typeparam>
+ /// <param name="obj">The object which has the property.</param>
+ /// <param name="name">The property name.</param>
+ /// <param name="required">Whether to throw an exception if the private property is not found.</param>
+ public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
+ {
+ // validate
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object.");
+
+ // get property from hierarchy
+ IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic);
+ if (required && property == null)
+ throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property.");
+ return property;
+ }
+
+ /// <summary>Get a private static property.</summary>
+ /// <typeparam name="TValue">The property type.</typeparam>
+ /// <param name="type">The type which has the property.</param>
+ /// <param name="name">The property name.</param>
+ /// <param name="required">Whether to throw an exception if the private property is not found.</param>
+ public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
+ {
+ // get field from hierarchy
+ IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static);
+ if (required && property == null)
+ throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property.");
+ return property;
+ }
+
+ /****
** Field values
** (shorthand since this is the most common case)
****/
@@ -192,6 +227,28 @@ namespace StardewModdingAPI.Framework.Reflection
: null;
}
+ /// <summary>Get a property from the type hierarchy.</summary>
+ /// <typeparam name="TValue">The expected property type.</typeparam>
+ /// <param name="type">The type which has the property.</param>
+ /// <param name="obj">The object which has the property.</param>
+ /// <param name="name">The property name.</param>
+ /// <param name="bindingFlags">The reflection binding which flags which indicates what type of property to find.</param>
+ private IPrivateProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
+ {
+ bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
+ PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
+ {
+ PropertyInfo propertyInfo = null;
+ for (; type != null && propertyInfo == null; type = type.BaseType)
+ propertyInfo = type.GetProperty(name, bindingFlags);
+ return propertyInfo;
+ });
+
+ return property != null
+ ? new PrivateProperty<TValue>(type, obj, property, isStatic)
+ : null;
+ }
+
/// <summary>Get a method from the type hierarchy.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="obj">The object which has the method.</param>
diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs
new file mode 100644
index 00000000..12d0ea0c
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs
@@ -0,0 +1,7 @@
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
+ /// <param name="module">The module which requested an immediate exit.</param>
+ /// <param name="reason">The reason provided for the shutdown.</param>
+ internal delegate void RequestExitDelegate(string module, string reason);
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs
new file mode 100644
index 00000000..ef5855b2
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/SContentManager.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI.AssemblyRewriters;
+using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework.Content;
+using StardewModdingAPI.Framework.Reflection;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>SMAPI's implementation of the game's content manager which lets it raise content events.</summary>
+ internal class SContentManager : LocalizedContentManager
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The possible directory separator characters in an asset key.</summary>
+ private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
+
+ /// <summary>The preferred directory separator chaeacter in an asset key.</summary>
+ private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
+
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ private readonly IMonitor Monitor;
+
+ /// <summary>The underlying content manager's asset cache.</summary>
+ private readonly IDictionary<string, object> Cache;
+
+ /// <summary>Applies platform-specific asset key normalisation so it's consistent with the underlying cache.</summary>
+ private readonly Func<string, string> NormaliseAssetNameForPlatform;
+
+ /// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary>
+ private readonly IPrivateMethod GetKeyLocale;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="rootDirectory">The root directory to search for content.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ public SContentManager(IServiceProvider serviceProvider, string rootDirectory, IMonitor monitor)
+ : this(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, null, monitor) { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="rootDirectory">The root directory to search for content.</param>
+ /// <param name="currentCulture">The current culture for which to localise content.</param>
+ /// <param name="languageCodeOverride">The current language code for which to localise content.</param>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor)
+ : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride)
+ {
+ // initialise
+ this.Monitor = monitor;
+ IReflectionHelper reflection = new ReflectionHelper();
+
+ // get underlying fields for interception
+ this.Cache = reflection.GetPrivateField<Dictionary<string, object>>(this, "loadedAssets").GetValue();
+ this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
+
+ // get asset key normalisation logic
+ if (Constants.TargetPlatform == Platform.Windows)
+ {
+ IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath");
+ this.NormaliseAssetNameForPlatform = path => method.Invoke<string>(path);
+ }
+ else
+ this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic
+ }
+
+ /// <summary>Load an asset that has been processed by the content pipeline.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ public override T Load<T>(string assetName)
+ {
+ // get normalised metadata
+ assetName = this.NormaliseAssetName(assetName);
+ string cacheLocale = this.GetCacheLocale(assetName);
+
+ // skip if already loaded
+ if (this.IsLoaded(assetName))
+ return base.Load<T>(assetName);
+
+ // load data
+ T data = base.Load<T>(assetName);
+
+ // let mods intercept content
+ IContentEventHelper helper = new ContentEventHelper(cacheLocale, assetName, data, this.NormaliseAssetName);
+ ContentEvents.InvokeAfterAssetLoaded(this.Monitor, helper);
+ this.Cache[assetName] = helper.Data;
+ return (T)helper.Data;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Normalise an asset name so it's consistent with the underlying cache.</summary>
+ /// <param name="assetName">The asset key.</param>
+ private string NormaliseAssetName(string assetName)
+ {
+ // ensure name format is consistent
+ string[] parts = assetName.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
+ assetName = string.Join(SContentManager.PreferredPathSeparator, parts);
+
+ // apply platform normalisation logic
+ return this.NormaliseAssetNameForPlatform(assetName);
+ }
+
+ /// <summary>Get whether an asset has already been loaded.</summary>
+ /// <param name="normalisedAssetName">The normalised asset name.</param>
+ private bool IsLoaded(string normalisedAssetName)
+ {
+ return this.Cache.ContainsKey(normalisedAssetName)
+ || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset
+ }
+
+ /// <summary>Get the locale for which the asset name was saved, if any.</summary>
+ /// <param name="normalisedAssetName">The normalised asset name.</param>
+ private string GetCacheLocale(string normalisedAssetName)
+ {
+ string locale = this.GetKeyLocale.Invoke<string>();
+ return this.Cache.ContainsKey($"{normalisedAssetName}.{locale}")
+ ? locale
+ : null;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Inheritance/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs
index 69c20244..5f265139 100644
--- a/src/StardewModdingAPI/Inheritance/SGame.cs
+++ b/src/StardewModdingAPI/Framework/SGame.cs
@@ -1,13 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using StardewModdingAPI.Events;
-using StardewModdingAPI.Framework;
+using StardewModdingAPI.Framework.Reflection;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Locations;
@@ -15,24 +15,28 @@ using StardewValley.Menus;
using StardewValley.Tools;
using xTile.Dimensions;
using Rectangle = Microsoft.Xna.Framework.Rectangle;
+using SFarmer = StardewValley.Farmer;
-namespace StardewModdingAPI.Inheritance
+namespace StardewModdingAPI.Framework
{
/// <summary>SMAPI's extension of the game's core <see cref="Game1"/>, used to inject events.</summary>
- public class SGame : Game1
+ internal class SGame : Game1
{
/*********
** Properties
*********/
- /// <summary>The number of ticks until SMAPI should notify mods when <see cref="Game1.hasLoadedGame"/> is set.</summary>
+ /****
+ ** SMAPI state
+ ****/
+ /// <summary>The number of ticks until SMAPI should notify mods that the game has loaded.</summary>
/// <remarks>Skipping a few frames ensures the game finishes initialising the world before mods try to change it.</remarks>
private int AfterLoadTimer = 5;
/// <summary>Whether the player has loaded a save and the world has finished initialising.</summary>
private bool IsWorldReady => this.AfterLoadTimer < 0;
- /// <summary>The debug messages to add to the next debug output.</summary>
- internal static Queue<string> DebugMessageQueue { get; private set; }
+ /// <summary>Whether the game is returning to the menu.</summary>
+ private bool IsExiting;
/// <summary>Whether the game's zoom level is at 100% (i.e. nothing should be scaled).</summary>
public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f);
@@ -40,246 +44,118 @@ namespace StardewModdingAPI.Inheritance
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
-
- /*********
- ** Accessors
- *********/
+ /****
+ ** Game state
+ ****/
/// <summary>Arrays of pressed controller buttons indexed by <see cref="PlayerIndex"/>.</summary>
- public Buttons[][] PreviouslyPressedButtons;
+ private Buttons[][] PreviouslyPressedButtons;
/// <summary>A record of the keyboard state (i.e. the up/down state for each button) as of the latest tick.</summary>
- public KeyboardState KStateNow { get; private set; }
+ private KeyboardState KStateNow;
/// <summary>A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick.</summary>
- public KeyboardState KStatePrior { get; private set; }
+ private KeyboardState KStatePrior;
/// <summary>A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the latest tick.</summary>
- public MouseState MStateNow { get; private set; }
+ private MouseState MStateNow;
/// <summary>A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick.</summary>
- public MouseState MStatePrior { get; private set; }
+ private MouseState MStatePrior;
/// <summary>The current mouse position on the screen adjusted for the zoom level.</summary>
- public Point MPositionNow { get; private set; }
+ private Point MPositionNow;
/// <summary>The previous mouse position on the screen adjusted for the zoom level.</summary>
- public Point MPositionPrior { get; private set; }
+ private Point MPositionPrior;
/// <summary>The keys that were pressed as of the latest tick.</summary>
- public Keys[] CurrentlyPressedKeys => this.KStateNow.GetPressedKeys();
+ private Keys[] CurrentlyPressedKeys => this.KStateNow.GetPressedKeys();
/// <summary>The keys that were pressed as of the previous tick.</summary>
- public Keys[] PreviouslyPressedKeys => this.KStatePrior.GetPressedKeys();
+ private Keys[] PreviouslyPressedKeys => this.KStatePrior.GetPressedKeys();
/// <summary>The keys that just entered the down state.</summary>
- public Keys[] FramePressedKeys => this.CurrentlyPressedKeys.Except(this.PreviouslyPressedKeys).ToArray();
+ private Keys[] FramePressedKeys => this.CurrentlyPressedKeys.Except(this.PreviouslyPressedKeys).ToArray();
/// <summary>The keys that just entered the up state.</summary>
- public Keys[] FrameReleasedKeys => this.PreviouslyPressedKeys.Except(this.CurrentlyPressedKeys).ToArray();
-
- /// <summary>Whether a save is currently loaded at last check.</summary>
- public bool PreviouslyLoadedGame { get; private set; }
+ private Keys[] FrameReleasedKeys => this.PreviouslyPressedKeys.Except(this.CurrentlyPressedKeys).ToArray();
/// <summary>A hash of <see cref="Game1.locations"/> at last check.</summary>
- public int PreviousGameLocations { get; private set; }
+ private int PreviousGameLocations;
/// <summary>A hash of the current location's <see cref="GameLocation.objects"/> at last check.</summary>
- public int PreviousLocationObjects { get; private set; }
+ private int PreviousLocationObjects;
/// <summary>The player's inventory at last check.</summary>
- public Dictionary<Item, int> PreviousItems { get; private set; }
+ private IDictionary<Item, int> PreviousItems;
/// <summary>The player's combat skill level at last check.</summary>
- public int PreviousCombatLevel { get; private set; }
+ private int PreviousCombatLevel;
/// <summary>The player's farming skill level at last check.</summary>
- public int PreviousFarmingLevel { get; private set; }
+ private int PreviousFarmingLevel;
/// <summary>The player's fishing skill level at last check.</summary>
- public int PreviousFishingLevel { get; private set; }
+ private int PreviousFishingLevel;
/// <summary>The player's foraging skill level at last check.</summary>
- public int PreviousForagingLevel { get; private set; }
+ private int PreviousForagingLevel;
/// <summary>The player's mining skill level at last check.</summary>
- public int PreviousMiningLevel { get; private set; }
+ private int PreviousMiningLevel;
/// <summary>The player's luck skill level at last check.</summary>
- public int PreviousLuckLevel { get; private set; }
+ private int PreviousLuckLevel;
/// <summary>The player's location at last check.</summary>
- public GameLocation PreviousGameLocation { get; private set; }
+ private GameLocation PreviousGameLocation;
/// <summary>The active game menu at last check.</summary>
- public IClickableMenu PreviousActiveMenu { get; private set; }
+ private IClickableMenu PreviousActiveMenu;
/// <summary>The mine level at last check.</summary>
- public int PreviousMineLevel { get; private set; }
+ private int PreviousMineLevel;
/// <summary>The time of day (in 24-hour military format) at last check.</summary>
- public int PreviousTimeOfDay { get; private set; }
+ private int PreviousTime;
/// <summary>The day of month (1–28) at last check.</summary>
- public int PreviousDayOfMonth { get; private set; }
+ private int PreviousDay;
/// <summary>The season name (winter, spring, summer, or fall) at last check.</summary>
- public string PreviousSeasonOfYear { get; private set; }
+ private string PreviousSeason;
/// <summary>The year number at last check.</summary>
- public int PreviousYearOfGame { get; private set; }
+ private int PreviousYear;
/// <summary>Whether the game was transitioning to a new day at last check.</summary>
- public bool PreviousIsNewDay { get; private set; }
+ private bool PreviousIsNewDay;
/// <summary>The player character at last check.</summary>
- public Farmer PreviousFarmer { get; private set; }
+ private SFarmer PreviousFarmer;
/// <summary>An index incremented on every tick and reset every 60th tick (0–59).</summary>
- public int CurrentUpdateTick { get; private set; }
+ private int CurrentUpdateTick;
/// <summary>Whether this is the very first update tick since the game started.</summary>
- public bool FirstUpdate { get; private set; }
-
- /// <summary>The game's current render target.</summary>
- public RenderTarget2D Screen
- {
- get { return this.GetBaseFieldValue<RenderTarget2D>("screen"); }
- set { this.SetBaseFieldValue<RenderTarget2D>("screen", value); }
- }
-
- /// <summary>The game's current background color.</summary>
- public Color BgColour
- {
- get { return (Color)this.GetBaseFieldValue<object>("bgColor"); }
- set { this.SetBaseFieldValue<object>("bgColor", value); }
- }
+ private bool FirstUpdate;
/// <summary>The current game instance.</summary>
- public static SGame Instance { get; private set; }
-
- /// <summary>The game's current frame rate, recalculated on each draw update.</summary>
- public static float FramesPerSecond { get; private set; }
-
- /// <summary>Whether we're in pseudo-debug mode, which shows information like FPS.</summary>
- public static bool Debug { get; private set; }
-
- /// <summary>The current player.</summary>
- [Obsolete("Use Game1.player instead")]
- public Farmer CurrentFarmer => Game1.player;
-
- /// <summary>The game method which draws the farm buildings.</summary>
- public static MethodInfo DrawFarmBuildings = typeof(Game1).GetMethod("drawFarmBuildings", BindingFlags.NonPublic | BindingFlags.Instance);
-
- /// <summary>The game method which draws the game HUD.</summary>
- public static MethodInfo DrawHUD = typeof(Game1).GetMethod("drawHUD", BindingFlags.NonPublic | BindingFlags.Instance);
-
- /// <summary>The game method which draws the current dialogue box, if any.</summary>
- public static MethodInfo DrawDialogueBox = typeof(Game1).GetMethod("drawDialogueBox", BindingFlags.NonPublic | BindingFlags.Instance);
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get the controller buttons which are currently pressed.</summary>
- /// <param name="index">The controller to check.</param>
- public Buttons[] GetButtonsDown(PlayerIndex index)
- {
- var state = GamePad.GetState(index);
- var buttons = new List<Buttons>();
- if (state.IsConnected)
- {
- if (state.Buttons.A == ButtonState.Pressed) buttons.Add(Buttons.A);
- if (state.Buttons.B == ButtonState.Pressed) buttons.Add(Buttons.B);
- if (state.Buttons.Back == ButtonState.Pressed) buttons.Add(Buttons.Back);
- if (state.Buttons.BigButton == ButtonState.Pressed) buttons.Add(Buttons.BigButton);
- if (state.Buttons.LeftShoulder == ButtonState.Pressed) buttons.Add(Buttons.LeftShoulder);
- if (state.Buttons.LeftStick == ButtonState.Pressed) buttons.Add(Buttons.LeftStick);
- if (state.Buttons.RightShoulder == ButtonState.Pressed) buttons.Add(Buttons.RightShoulder);
- if (state.Buttons.RightStick == ButtonState.Pressed) buttons.Add(Buttons.RightStick);
- if (state.Buttons.Start == ButtonState.Pressed) buttons.Add(Buttons.Start);
- if (state.Buttons.X == ButtonState.Pressed) buttons.Add(Buttons.X);
- if (state.Buttons.Y == ButtonState.Pressed) buttons.Add(Buttons.Y);
- if (state.DPad.Up == ButtonState.Pressed) buttons.Add(Buttons.DPadUp);
- if (state.DPad.Down == ButtonState.Pressed) buttons.Add(Buttons.DPadDown);
- if (state.DPad.Left == ButtonState.Pressed) buttons.Add(Buttons.DPadLeft);
- if (state.DPad.Right == ButtonState.Pressed) buttons.Add(Buttons.DPadRight);
- if (state.Triggers.Left > 0.2f) buttons.Add(Buttons.LeftTrigger);
- if (state.Triggers.Right > 0.2f) buttons.Add(Buttons.RightTrigger);
- }
- return buttons.ToArray();
- }
-
- /// <summary>Get the controller buttons which were pressed after the last update.</summary>
- /// <param name="index">The controller to check.</param>
- public Buttons[] GetFramePressedButtons(PlayerIndex index)
- {
- var state = GamePad.GetState(index);
- var buttons = new List<Buttons>();
- if (state.IsConnected)
- {
- if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A);
- if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B);
- if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back);
- if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton);
- if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder);
- if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick);
- if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder);
- if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick);
- if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start);
- if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X);
- if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y);
- if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp);
- if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown);
- if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft);
- if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight);
- if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger);
- if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger);
- }
- return buttons.ToArray();
- }
-
- /// <summary>Get the controller buttons which were released after the last update.</summary>
- /// <param name="index">The controller to check.</param>
- public Buttons[] GetFrameReleasedButtons(PlayerIndex index)
- {
- var state = GamePad.GetState(index);
- var buttons = new List<Buttons>();
- if (state.IsConnected)
- {
- if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A);
- if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B);
- if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back);
- if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton);
- if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder);
- if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick);
- if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder);
- if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick);
- if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start);
- if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X);
- if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y);
- if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp);
- if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown);
- if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft);
- if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight);
- if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger);
- if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger);
- }
- return buttons.ToArray();
- }
-
- /// <summary>Queue a message to be added to the debug output.</summary>
- /// <param name="message">The message to add.</param>
- /// <returns>Returns whether the message was successfully queued.</returns>
- public static bool QueueDebugMessage(string message)
- {
- if (!SGame.Debug)
- return false;
- if (SGame.DebugMessageQueue.Count > 32)
- return false;
-
- SGame.DebugMessageQueue.Enqueue(message);
- return true;
- }
+ private static SGame Instance;
+
+ /****
+ ** Private wrappers
+ ****/
+ // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
+ /// <summary>Used to access private fields and methods.</summary>
+ private static readonly IReflectionHelper Reflection = new ReflectionHelper();
+ private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue();
+ public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateField<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
+ public BlendState lightingBlend => SGame.Reflection.GetPrivateField<BlendState>(this, nameof(lightingBlend)).GetValue();
+ private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(new object[0]);
+ private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(new object[0]);
+ private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(new object[0]);
+ // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
/*********
@@ -294,11 +170,12 @@ namespace StardewModdingAPI.Inheritance
SGame.Instance = this;
}
+ /****
+ ** Intercepted methods & events
+ ****/
/// <summary>The method called during game launch after configuring XNA or MonoGame. The game window hasn't been opened by this point.</summary>
protected override void Initialize()
{
- //ModItems = new Dictionary<int, SObject>();
- SGame.DebugMessageQueue = new Queue<string>();
this.PreviouslyPressedButtons = new Buttons[4][];
for (var i = 0; i < 4; ++i)
this.PreviouslyPressedButtons[i] = new Buttons[0];
@@ -318,9 +195,6 @@ namespace StardewModdingAPI.Inheritance
/// <param name="gameTime">A snapshot of the game timing state.</param>
protected override void Update(GameTime gameTime)
{
- // add FPS to debug output
- SGame.QueueDebugMessage($"FPS: {SGame.FramesPerSecond}");
-
// raise game loaded
if (this.FirstUpdate)
GameEvents.InvokeGameLoaded(this.Monitor);
@@ -328,10 +202,6 @@ namespace StardewModdingAPI.Inheritance
// update SMAPI events
this.UpdateEventCalls();
- // toggle debug output
- if (this.FramePressedKeys.Contains(Keys.F3))
- SGame.Debug = !SGame.Debug;
-
// let game update
try
{
@@ -367,8 +237,7 @@ namespace StardewModdingAPI.Inheritance
this.CurrentUpdateTick = 0;
// track keyboard state
- if (this.KStatePrior != this.KStateNow)
- this.KStatePrior = this.KStateNow;
+ this.KStatePrior = this.KStateNow;
// track controller button state
for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++)
@@ -377,18 +246,22 @@ namespace StardewModdingAPI.Inheritance
/// <summary>The method called to draw everything to the screen.</summary>
/// <param name="gameTime">A snapshot of the game timing state.</param>
- /// <remarks>This implementation is identical to <see cref="Game1.Draw"/>, except for try..catch around menu draw code, minor formatting, and added events.</remarks>
+ /// <remarks>This implementation is identical to <see cref="Game1.Draw"/>, except for try..catch around menu draw code, private field references replaced by wrappers, and added events.</remarks>
+ [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")]
protected override void Draw(GameTime gameTime)
{
- // track frame rate
- SGame.FramesPerSecond = 1 / (float)gameTime.ElapsedGameTime.TotalSeconds;
-
try
{
if (!this.ZoomLevelIsOne)
- this.GraphicsDevice.SetRenderTarget(this.Screen);
+ this.GraphicsDevice.SetRenderTarget(this.screenWrapper);
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
if (Game1.options.showMenuBackground && Game1.activeClickableMenu != null && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet())
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
@@ -416,9 +289,9 @@ namespace StardewModdingAPI.Inheritance
if (!this.ZoomLevelIsOne)
{
this.GraphicsDevice.SetRenderTarget(null);
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw(this.Screen, Vector2.Zero, this.Screen.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
+ Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
return;
@@ -444,9 +317,9 @@ namespace StardewModdingAPI.Inheritance
if (!this.ZoomLevelIsOne)
{
this.GraphicsDevice.SetRenderTarget(null);
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw(this.Screen, Vector2.Zero, this.Screen.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
+ Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
return;
@@ -467,9 +340,9 @@ namespace StardewModdingAPI.Inheritance
if (!this.ZoomLevelIsOne)
{
this.GraphicsDevice.SetRenderTarget(null);
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw(this.Screen, Vector2.Zero, this.Screen.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
+ Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
return;
@@ -489,9 +362,9 @@ namespace StardewModdingAPI.Inheritance
if (!this.ZoomLevelIsOne)
{
this.GraphicsDevice.SetRenderTarget(null);
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw(this.Screen, Vector2.Zero, this.Screen.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
+ Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
return;
@@ -512,11 +385,11 @@ namespace StardewModdingAPI.Inheritance
Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(i).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(i).position) / Game1.options.lightingQuality, Game1.currentLightSources.ElementAt(i).lightTexture.Bounds, Game1.currentLightSources.ElementAt(i).color, 0f, new Vector2(Game1.currentLightSources.ElementAt(i).lightTexture.Bounds.Center.X, Game1.currentLightSources.ElementAt(i).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(i).radius / Game1.options.lightingQuality, SpriteEffects.None, 0.9f);
}
Game1.spriteBatch.End();
- this.GraphicsDevice.SetRenderTarget(this.ZoomLevelIsOne ? null : this.Screen);
+ this.GraphicsDevice.SetRenderTarget(this.ZoomLevelIsOne ? null : this.screenWrapper);
}
if (Game1.bloomDay)
Game1.bloom?.BeginDraw();
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor);
Game1.background?.draw(Game1.spriteBatch);
@@ -581,7 +454,7 @@ namespace StardewModdingAPI.Inheritance
if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))
Game1.drawTool(Game1.player);
if (Game1.currentLocation.Name.Equals("Farm"))
- SGame.DrawFarmBuildings.Invoke(Program.gamePtr, null);
+ this.drawFarmBuildings();
if (Game1.tvStation >= 0)
Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(6 * Game1.tileSize + Game1.tileSize / 4, 2 * Game1.tileSize + Game1.tileSize / 2)), new Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f);
if (Game1.panMode)
@@ -708,16 +581,14 @@ namespace StardewModdingAPI.Inheritance
if (Game1.currentBillboard != 0)
this.drawBillboard();
- GraphicsEvents.InvokeOnPreRenderHudEventNoCheck(this.Monitor);
if ((Game1.displayHUD || Game1.eventUp) && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !Game1.freezeControls && !Game1.panMode)
{
GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor);
- SGame.DrawHUD.Invoke(Program.gamePtr, null);
+ this.drawHUD();
GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor);
}
else if (Game1.activeClickableMenu == null && Game1.farmEvent == null)
Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2(Game1.getOldMouseX(), Game1.getOldMouseY()), Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16), Color.White, 0f, Vector2.Zero, 4f + Game1.dialogueButtonScale / 150f, SpriteEffects.None, 1f);
- GraphicsEvents.InvokeOnPostRenderHudEventNoCheck(this.Monitor);
if (Game1.hudMessages.Any() && (!Game1.eventUp || Game1.isFestival()))
{
@@ -727,7 +598,7 @@ namespace StardewModdingAPI.Inheritance
}
Game1.farmEvent?.draw(Game1.spriteBatch);
if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && !(Game1.activeClickableMenu is DialogueBox))
- SGame.DrawDialogueBox.Invoke(Program.gamePtr, null);
+ this.drawDialogueBox();
if (Game1.progressBar)
{
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Rectangle((Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - Game1.tileSize * 2, Game1.dialogueWidth, Game1.tileSize / 2), Color.LightGray);
@@ -746,7 +617,7 @@ namespace StardewModdingAPI.Inheritance
Game1.flashAlpha -= 0.1f;
}
if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp)
- SGame.DrawDialogueBox.Invoke(Program.gamePtr, null);
+ this.drawDialogueBox();
foreach (TemporaryAnimatedSprite current8 in Game1.screenOverlayTempSprites)
current8.draw(Game1.spriteBatch, true);
if (Game1.debugMode)
@@ -766,7 +637,6 @@ namespace StardewModdingAPI.Inheritance
if (Game1.showKeyHelp)
Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(Game1.tileSize, Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? (Game1.tileSize * 3 + (Game1.isQuestion ? (Game1.questionChoices.Count * Game1.tileSize) : 0)) : 0) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f);
- GraphicsEvents.InvokeOnPreRenderGuiEventNoCheck(this.Monitor);
if (Game1.activeClickableMenu != null)
{
GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor);
@@ -783,46 +653,113 @@ namespace StardewModdingAPI.Inheritance
}
else
Game1.farmEvent?.drawAboveEverything(Game1.spriteBatch);
- GraphicsEvents.InvokeOnPostRenderGuiEventNoCheck(this.Monitor);
GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
Game1.spriteBatch.End();
- GraphicsEvents.InvokeDrawInRenderTargetTick(this.Monitor);
-
if (!this.ZoomLevelIsOne)
{
this.GraphicsDevice.SetRenderTarget(null);
- this.GraphicsDevice.Clear(this.BgColour);
+ this.GraphicsDevice.Clear(this.bgColor);
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone);
- Game1.spriteBatch.Draw(this.Screen, Vector2.Zero, this.Screen.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
+ Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
-
- GraphicsEvents.InvokeDrawTick(this.Monitor);
}
catch (Exception ex)
{
this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error);
}
+ }
- if (SGame.Debug)
+ /****
+ ** Methods
+ ****/
+ /// <summary>Get the controller buttons which are currently pressed.</summary>
+ /// <param name="index">The controller to check.</param>
+ private Buttons[] GetButtonsDown(PlayerIndex index)
+ {
+ var state = GamePad.GetState(index);
+ var buttons = new List<Buttons>();
+ if (state.IsConnected)
{
- Game1.spriteBatch.Begin();
+ if (state.Buttons.A == ButtonState.Pressed) buttons.Add(Buttons.A);
+ if (state.Buttons.B == ButtonState.Pressed) buttons.Add(Buttons.B);
+ if (state.Buttons.Back == ButtonState.Pressed) buttons.Add(Buttons.Back);
+ if (state.Buttons.BigButton == ButtonState.Pressed) buttons.Add(Buttons.BigButton);
+ if (state.Buttons.LeftShoulder == ButtonState.Pressed) buttons.Add(Buttons.LeftShoulder);
+ if (state.Buttons.LeftStick == ButtonState.Pressed) buttons.Add(Buttons.LeftStick);
+ if (state.Buttons.RightShoulder == ButtonState.Pressed) buttons.Add(Buttons.RightShoulder);
+ if (state.Buttons.RightStick == ButtonState.Pressed) buttons.Add(Buttons.RightStick);
+ if (state.Buttons.Start == ButtonState.Pressed) buttons.Add(Buttons.Start);
+ if (state.Buttons.X == ButtonState.Pressed) buttons.Add(Buttons.X);
+ if (state.Buttons.Y == ButtonState.Pressed) buttons.Add(Buttons.Y);
+ if (state.DPad.Up == ButtonState.Pressed) buttons.Add(Buttons.DPadUp);
+ if (state.DPad.Down == ButtonState.Pressed) buttons.Add(Buttons.DPadDown);
+ if (state.DPad.Left == ButtonState.Pressed) buttons.Add(Buttons.DPadLeft);
+ if (state.DPad.Right == ButtonState.Pressed) buttons.Add(Buttons.DPadRight);
+ if (state.Triggers.Left > 0.2f) buttons.Add(Buttons.LeftTrigger);
+ if (state.Triggers.Right > 0.2f) buttons.Add(Buttons.RightTrigger);
+ }
+ return buttons.ToArray();
+ }
- int i = 0;
- while (SGame.DebugMessageQueue.Any())
- {
- string message = SGame.DebugMessageQueue.Dequeue();
- Game1.spriteBatch.DrawString(Game1.smoothFont, message, new Vector2(0, i * 14), Color.CornflowerBlue);
- i++;
- }
- GraphicsEvents.InvokeDrawDebug(this.Monitor);
+ /// <summary>Get the controller buttons which were pressed after the last update.</summary>
+ /// <param name="index">The controller to check.</param>
+ private Buttons[] GetFramePressedButtons(PlayerIndex index)
+ {
+ var state = GamePad.GetState(index);
+ var buttons = new List<Buttons>();
+ if (state.IsConnected)
+ {
+ if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A);
+ if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B);
+ if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back);
+ if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton);
+ if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder);
+ if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick);
+ if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder);
+ if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick);
+ if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start);
+ if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X);
+ if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y);
+ if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp);
+ if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown);
+ if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft);
+ if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight);
+ if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger);
+ if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger);
+ }
+ return buttons.ToArray();
+ }
- Game1.spriteBatch.End();
+ /// <summary>Get the controller buttons which were released after the last update.</summary>
+ /// <param name="index">The controller to check.</param>
+ private Buttons[] GetFrameReleasedButtons(PlayerIndex index)
+ {
+ var state = GamePad.GetState(index);
+ var buttons = new List<Buttons>();
+ if (state.IsConnected)
+ {
+ if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A);
+ if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B);
+ if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back);
+ if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton);
+ if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder);
+ if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick);
+ if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder);
+ if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick);
+ if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start);
+ if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X);
+ if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y);
+ if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp);
+ if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown);
+ if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft);
+ if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight);
+ if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger);
+ if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger);
}
- else
- SGame.DebugMessageQueue.Clear();
+ return buttons.ToArray();
}
/// <summary>Get whether a controller button was pressed since the last check.</summary>
@@ -865,16 +802,29 @@ namespace StardewModdingAPI.Inheritance
private void UpdateEventCalls()
{
// save loaded event
- if (Game1.hasLoadedGame && this.AfterLoadTimer >= 0)
+ if (Constants.IsSaveLoaded && this.AfterLoadTimer >= 0)
{
if (this.AfterLoadTimer == 0)
{
SaveEvents.InvokeAfterLoad(this.Monitor);
PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame));
+ TimeEvents.InvokeAfterDayStarted(this.Monitor);
}
this.AfterLoadTimer--;
}
+ // before exit to title
+ if (Game1.exitToTitle)
+ this.IsExiting = true;
+
+ // after exit to title
+ if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu)
+ {
+ SaveEvents.InvokeAfterReturnToTitle(this.Monitor);
+ this.AfterLoadTimer = 5;
+ this.IsExiting = false;
+ }
+
// input events
{
// get latest state
@@ -939,7 +889,10 @@ namespace StardewModdingAPI.Inheritance
if (newMenu is SaveGameMenu || newMenu is ShippingMenu)
SaveEvents.InvokeBeforeSave(this.Monitor);
else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu)
+ {
SaveEvents.InvokeAfterSave(this.Monitor);
+ TimeEvents.InvokeAfterDayStarted(this.Monitor);
+ }
// raise menu events
if (newMenu != null)
@@ -1027,25 +980,25 @@ namespace StardewModdingAPI.Inheritance
}
// raise time changed
- if (Game1.timeOfDay != this.PreviousTimeOfDay)
+ if (Game1.timeOfDay != this.PreviousTime)
{
- TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTimeOfDay, Game1.timeOfDay);
- this.PreviousTimeOfDay = Game1.timeOfDay;
+ TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay);
+ this.PreviousTime = Game1.timeOfDay;
}
- if (Game1.dayOfMonth != this.PreviousDayOfMonth)
+ if (Game1.dayOfMonth != this.PreviousDay)
{
- TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDayOfMonth, Game1.dayOfMonth);
- this.PreviousDayOfMonth = Game1.dayOfMonth;
+ TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth);
+ this.PreviousDay = Game1.dayOfMonth;
}
- if (Game1.currentSeason != this.PreviousSeasonOfYear)
+ if (Game1.currentSeason != this.PreviousSeason)
{
- TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeasonOfYear, Game1.currentSeason);
- this.PreviousSeasonOfYear = Game1.currentSeason;
+ TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason);
+ this.PreviousSeason = Game1.currentSeason;
}
- if (Game1.year != this.PreviousYearOfGame)
+ if (Game1.year != this.PreviousYear)
{
- TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYearOfGame, Game1.year);
- this.PreviousYearOfGame = Game1.year;
+ TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year);
+ this.PreviousYear = Game1.year;
}
// raise mine level changed
@@ -1059,7 +1012,7 @@ namespace StardewModdingAPI.Inheritance
// raise game day transition event (obsolete)
if (Game1.newDay != this.PreviousIsNewDay)
{
- TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDayOfMonth, Game1.dayOfMonth, Game1.newDay);
+ TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay);
this.PreviousIsNewDay = Game1.newDay;
}
}
@@ -1106,29 +1059,5 @@ namespace StardewModdingAPI.Inheritance
hash ^= v.GetHashCode();
return hash;
}
-
- /// <summary>Get reflection metadata for a private <see cref="Game1"/> field.</summary>
- /// <param name="name">The field name.</param>
- private FieldInfo GetBaseFieldInfo(string name)
- {
- return typeof(Game1).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
- }
-
- /// <summary>Get the value of a private <see cref="Game1"/> field.</summary>
- /// <typeparam name="TValue">The expected value type.</typeparam>
- /// <param name="name">The field name.</param>
- private TValue GetBaseFieldValue<TValue>(string name) where TValue : class
- {
- return this.GetBaseFieldInfo(name).GetValue(Program.gamePtr) as TValue;
- }
-
- /// <summary>Set the value of a private <see cref="Game1"/> field.</summary>
- /// <typeparam name="TValue">The expected value type.</typeparam>
- /// <param name="name">The field name.</param>
- /// <param name="value">The value to set.</param>
- public void SetBaseFieldValue<TValue>(string name, object value) where TValue : class
- {
- this.GetBaseFieldInfo(name).SetValue(Program.gamePtr, value as TValue);
- }
}
-}
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs
new file mode 100644
index 00000000..bd15c7bb
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Xna.Framework.Input;
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Framework.Serialisation
+{
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ internal class JsonHelper
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The JSON settings to use when serialising and deserialising files.</summary>
+ private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
+ {
+ Formatting = Formatting.Indented,
+ ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
+ Converters = new List<JsonConverter>
+ {
+ new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys))
+ }
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Read a JSON file.</summary>
+ /// <typeparam name="TModel">The model type.</typeparam>
+ /// <param name="fullPath">The absolete file path.</param>
+ /// <returns>Returns the deserialised model, or <c>null</c> if the file doesn't exist or is empty.</returns>
+ public TModel ReadJsonFile<TModel>(string fullPath)
+ where TModel : class
+ {
+ // read file
+ string json;
+ try
+ {
+ json = File.ReadAllText(fullPath);
+ }
+ catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException)
+ {
+ return null;
+ }
+
+ // deserialise model
+ return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings);
+ }
+
+ /// <summary>Save to a JSON file.</summary>
+ /// <typeparam name="TModel">The model type.</typeparam>
+ /// <param name="fullPath">The absolete file path.</param>
+ /// <param name="model">The model to save.</param>
+ public void WriteJsonFile<TModel>(string fullPath, TModel model)
+ where TModel : class
+ {
+ // create directory if needed
+ string dir = Path.GetDirectoryName(fullPath);
+ if (!Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ // write file
+ string json = JsonConvert.SerializeObject(model, this.JsonSettings);
+ File.WriteAllText(fullPath, json);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs
new file mode 100644
index 00000000..37108556
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json.Converters;
+
+namespace StardewModdingAPI.Framework.Serialisation
+{
+ /// <summary>A variant of <see cref="StringEnumConverter"/> which only converts certain enums.</summary>
+ internal class SelectiveStringEnumConverter : StringEnumConverter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The enum type names to convert.</summary>
+ private readonly HashSet<string> Types;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="types">The enum types to convert.</param>
+ public SelectiveStringEnumConverter(params Type[] types)
+ {
+ this.Types = new HashSet<string>(types.Select(p => p.FullName));
+ }
+
+ /// <summary>Get whether this instance can convert the specified object type.</summary>
+ /// <param name="type">The object type.</param>
+ public override bool CanConvert(Type type)
+ {
+ return
+ base.CanConvert(type)
+ && this.Types.Contains((Nullable.GetUnderlyingType(type) ?? type).FullName);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs
new file mode 100644
index 00000000..52ec999e
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs
@@ -0,0 +1,51 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace StardewModdingAPI.Framework.Serialisation
+{
+ /// <summary>Overrides how SMAPI reads and writes <see cref="ISemanticVersion"/>.</summary>
+ internal class SemanticVersionConverter : JsonConverter
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether this converter can write JSON.</summary>
+ public override bool CanWrite => false;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether this instance can convert the specified object type.</summary>
+ /// <param name="objectType">The object type.</param>
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(ISemanticVersion);
+ }
+
+ /// <summary>Reads the JSON representation of the object.</summary>
+ /// <param name="reader">The JSON reader.</param>
+ /// <param name="objectType">The object type.</param>
+ /// <param name="existingValue">The object being read.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ JObject obj = JObject.Load(reader);
+ int major = obj.Value<int>("MajorVersion");
+ int minor = obj.Value<int>("MinorVersion");
+ int patch = obj.Value<int>("PatchVersion");
+ string build = obj.Value<string>("Build");
+ return new SemanticVersion(major, minor, patch, build);
+ }
+
+ /// <summary>Writes the JSON representation of the object.</summary>
+ /// <param name="writer">The JSON writer.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new InvalidOperationException("This converter does not write JSON.");
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/StardewModdingAPI/ICommandHelper.cs
new file mode 100644
index 00000000..3a51ffb4
--- /dev/null
+++ b/src/StardewModdingAPI/ICommandHelper.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Provides an API for managing console commands.</summary>
+ public interface ICommandHelper
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Add a console command.</summary>
+ /// <param name="name">The command name, which the user must type to trigger it.</param>
+ /// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
+ /// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
+ /// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
+ /// <exception cref="ArgumentException">There's already a command with that name.</exception>
+ ICommandHelper Add(string name, string documentation, Action<string, string[]> callback);
+
+ /// <summary>Trigger a command.</summary>
+ /// <param name="name">The command name.</param>
+ /// <param name="arguments">The command arguments.</param>
+ /// <returns>Returns whether a matching command was triggered.</returns>
+ bool Trigger(string name, string[] arguments);
+ }
+}
diff --git a/src/StardewModdingAPI/IContentEventData.cs b/src/StardewModdingAPI/IContentEventData.cs
new file mode 100644
index 00000000..7e2d4df1
--- /dev/null
+++ b/src/StardewModdingAPI/IContentEventData.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Generic metadata and methods for a content asset being loaded.</summary>
+ /// <typeparam name="TValue">The expected data type.</typeparam>
+ public interface IContentEventData<TValue>
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The content's locale code, if the content is localised.</summary>
+ string Locale { get; }
+
+ /// <summary>The normalised asset name being read. The format may change between platforms; see <see cref="IsAssetName"/> to compare with a known path.</summary>
+ string AssetName { get; }
+
+ /// <summary>The content data being read.</summary>
+ TValue Data { get; }
+
+ /// <summary>The content data type.</summary>
+ Type DataType { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether the asset name being loaded matches a given name after normalisation.</summary>
+ /// <param name="path">The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation').</param>
+ bool IsAssetName(string path);
+
+ /// <summary>Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game.</summary>
+ /// <param name="value">The new content value.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="value"/> is null.</exception>
+ /// <exception cref="InvalidCastException">The <paramref name="value"/>'s type is not compatible with the loaded asset's type.</exception>
+ void ReplaceWith(TValue value);
+ }
+}
diff --git a/src/StardewModdingAPI/IContentEventHelper.cs b/src/StardewModdingAPI/IContentEventHelper.cs
new file mode 100644
index 00000000..421a1e06
--- /dev/null
+++ b/src/StardewModdingAPI/IContentEventHelper.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Encapsulates access and changes to content being read from a data file.</summary>
+ public interface IContentEventHelper : IContentEventData<object>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get a helper to manipulate the data as a dictionary.</summary>
+ /// <typeparam name="TKey">The expected dictionary key.</typeparam>
+ /// <typeparam name="TValue">The expected dictionary balue.</typeparam>
+ /// <exception cref="InvalidOperationException">The content being read isn't a dictionary.</exception>
+ IContentEventHelperForDictionary<TKey, TValue> AsDictionary<TKey, TValue>();
+
+ /// <summary>Get a helper to manipulate the data as an image.</summary>
+ /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
+ IContentEventHelperForImage AsImage();
+
+ /// <summary>Get the data as a given type.</summary>
+ /// <typeparam name="TData">The expected data type.</typeparam>
+ /// <exception cref="InvalidCastException">The data can't be converted to <typeparamref name="TData"/>.</exception>
+ TData GetData<TData>();
+ }
+}
diff --git a/src/StardewModdingAPI/IContentEventHelperForDictionary.cs b/src/StardewModdingAPI/IContentEventHelperForDictionary.cs
new file mode 100644
index 00000000..2f9d5a65
--- /dev/null
+++ b/src/StardewModdingAPI/IContentEventHelperForDictionary.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Encapsulates access and changes to dictionary content being read from a data file.</summary>
+ public interface IContentEventHelperForDictionary<TKey, TValue> : IContentEventData<IDictionary<TKey, TValue>>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Add or replace an entry in the dictionary.</summary>
+ /// <param name="key">The entry key.</param>
+ /// <param name="value">The entry value.</param>
+ void Set(TKey key, TValue value);
+
+ /// <summary>Add or replace an entry in the dictionary.</summary>
+ /// <param name="key">The entry key.</param>
+ /// <param name="value">A callback which accepts the current value and returns the new value.</param>
+ void Set(TKey key, Func<TValue, TValue> value);
+
+ /// <summary>Dynamically replace values in the dictionary.</summary>
+ /// <param name="replacer">A lambda which takes the current key and value for an entry, and returns the new value.</param>
+ void Set(Func<TKey, TValue, TValue> replacer);
+ }
+}
diff --git a/src/StardewModdingAPI/IContentEventHelperForImage.cs b/src/StardewModdingAPI/IContentEventHelperForImage.cs
new file mode 100644
index 00000000..1158c868
--- /dev/null
+++ b/src/StardewModdingAPI/IContentEventHelperForImage.cs
@@ -0,0 +1,23 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Encapsulates access and changes to dictionary content being read from a data file.</summary>
+ public interface IContentEventHelperForImage : IContentEventData<Texture2D>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Overwrite part of the image.</summary>
+ /// <param name="source">The image to patch into the content.</param>
+ /// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> texture.</param>
+ /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param>
+ /// <param name="patchMode">Indicates how an image should be patched.</param>
+ /// <exception cref="ArgumentNullException">One of the arguments is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception>
+ /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
+ void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace);
+ }
+}
diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs
index 02f9c038..ef67cd1c 100644
--- a/src/StardewModdingAPI/IModHelper.cs
+++ b/src/StardewModdingAPI/IModHelper.cs
@@ -15,6 +15,9 @@
/// <summary>Metadata about loaded mods.</summary>
IModRegistry ModRegistry { get; }
+ /// <summary>An API for managing console commands.</summary>
+ ICommandHelper ConsoleCommands { get; }
+
/*********
** Public methods
diff --git a/src/StardewModdingAPI/IPrivateProperty.cs b/src/StardewModdingAPI/IPrivateProperty.cs
new file mode 100644
index 00000000..8d67fa7a
--- /dev/null
+++ b/src/StardewModdingAPI/IPrivateProperty.cs
@@ -0,0 +1,26 @@
+using System.Reflection;
+
+namespace StardewModdingAPI
+{
+ /// <summary>A private property obtained through reflection.</summary>
+ /// <typeparam name="TValue">The property value type.</typeparam>
+ public interface IPrivateProperty<TValue>
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The reflection metadata.</summary>
+ PropertyInfo PropertyInfo { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get the property value.</summary>
+ TValue GetValue();
+
+ /// <summary>Set the property value.</summary>
+ //// <param name="value">The value to set.</param>
+ void SetValue(TValue value);
+ }
+} \ No newline at end of file
diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/StardewModdingAPI/IReflectionHelper.cs
index 5d747eda..77943c6c 100644
--- a/src/StardewModdingAPI/IReflectionHelper.cs
+++ b/src/StardewModdingAPI/IReflectionHelper.cs
@@ -22,6 +22,20 @@ namespace StardewModdingAPI
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true);
+ /// <summary>Get a private instance property.</summary>
+ /// <typeparam name="TValue">The property type.</typeparam>
+ /// <param name="obj">The object which has the property.</param>
+ /// <param name="name">The property name.</param>
+ /// <param name="required">Whether to throw an exception if the private property is not found.</param>
+ IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true);
+
+ /// <summary>Get a private static property.</summary>
+ /// <typeparam name="TValue">The property type.</typeparam>
+ /// <param name="type">The type which has the property.</param>
+ /// <param name="name">The property name.</param>
+ /// <param name="required">Whether to throw an exception if the private property is not found.</param>
+ IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true);
+
/// <summary>Get the value of a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
diff --git a/src/StardewModdingAPI/Inheritance/SObject.cs b/src/StardewModdingAPI/Inheritance/SObject.cs
deleted file mode 100644
index 0b0a7ec9..00000000
--- a/src/StardewModdingAPI/Inheritance/SObject.cs
+++ /dev/null
@@ -1,249 +0,0 @@
-using System;
-using System.Xml.Serialization;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using StardewModdingAPI.Framework;
-using StardewValley;
-using Object = StardewValley.Object;
-
-#pragma warning disable 1591
-namespace StardewModdingAPI.Inheritance
-{
- /// <summary>Provides access to the game's <see cref="Object"/> internals.</summary>
- [Obsolete("This class is deprecated and will be removed in a future version.")]
- public class SObject : Object
- {
- /*********
- ** Accessors
- *********/
- public string Description { get; set; }
- public Texture2D Texture { get; set; }
- public string CategoryName { get; set; }
- public Color CategoryColour { get; set; }
- public bool IsPassable { get; set; }
- public bool IsPlaceable { get; set; }
- public bool HasBeenRegistered { get; set; }
- public int RegisteredId { get; set; }
-
- public int MaxStackSize { get; set; }
-
- public bool WallMounted { get; set; }
- public Vector2 DrawPosition { get; set; }
-
- public bool FlaggedForPickup { get; set; }
-
- [XmlIgnore]
- public Vector2 CurrentMouse { get; protected set; }
-
- [XmlIgnore]
- public Vector2 PlacedAt { get; protected set; }
-
- public override int Stack
- {
- get { return this.stack; }
- set { this.stack = value; }
- }
-
- /*********
- ** Public methods
- *********/
- public SObject()
- {
- Program.DeprecationManager.Warn(nameof(SObject), "0.39.3", DeprecationLevel.PendingRemoval);
-
- this.name = "Modded Item Name";
- this.Description = "Modded Item Description";
- this.CategoryName = "Modded Item Category";
- this.Category = 4163;
- this.CategoryColour = Color.White;
- this.IsPassable = false;
- this.IsPlaceable = false;
- this.boundingBox = new Rectangle(0, 0, 64, 64);
- this.MaxStackSize = 999;
-
- this.type = "interactive";
- }
-
- public override string Name
- {
- get { return this.name; }
- set { this.name = value; }
- }
-
- public override string getDescription()
- {
- return this.Description;
- }
-
- public override void draw(SpriteBatch spriteBatch, int x, int y, float alpha = 1)
- {
- if (this.Texture != null)
- {
- spriteBatch.Draw(this.Texture, Game1.GlobalToLocal(Game1.viewport, new Vector2(x * Game1.tileSize + Game1.tileSize / 2 + (this.shakeTimer > 0 ? Game1.random.Next(-1, 2) : 0), y * Game1.tileSize + Game1.tileSize / 2 + (this.shakeTimer > 0 ? Game1.random.Next(-1, 2) : 0))), Game1.currentLocation.getSourceRectForObject(this.ParentSheetIndex), Color.White * alpha, 0f, new Vector2(8f, 8f), this.scale.Y > 1f ? this.getScale().Y : Game1.pixelZoom, this.flipped ? SpriteEffects.FlipHorizontally : SpriteEffects.None, (this.isPassable() ? this.getBoundingBox(new Vector2(x, y)).Top : this.getBoundingBox(new Vector2(x, y)).Bottom) / 10000f);
- }
- }
-
- public new void drawAsProp(SpriteBatch b)
- {
- }
-
- public override void drawInMenu(SpriteBatch spriteBatch, Vector2 location, float scaleSize, float transparency, float layerDepth, bool drawStackNumber)
- {
- if (this.isRecipe)
- {
- transparency = 0.5f;
- scaleSize *= 0.75f;
- }
-
- if (this.Texture != null)
- {
- var targSize = (int) (64 * scaleSize * 0.9f);
- var midX = (int) (location.X + 32);
- var midY = (int) (location.Y + 32);
-
- var targX = midX - targSize / 2;
- var targY = midY - targSize / 2;
-
- spriteBatch.Draw(this.Texture, new Rectangle(targX, targY, targSize, targSize), null, new Color(255, 255, 255, transparency), 0, Vector2.Zero, SpriteEffects.None, layerDepth);
- }
- if (drawStackNumber)
- {
- var _scale = 0.5f + scaleSize;
- Game1.drawWithBorder(this.stack.ToString(), Color.Black, Color.White, location + new Vector2(Game1.tileSize - Game1.tinyFont.MeasureString(string.Concat(this.stack.ToString())).X * _scale, Game1.tileSize - (float) ((double) Game1.tinyFont.MeasureString(string.Concat(this.stack.ToString())).Y * 3.0f / 4.0f) * _scale), 0.0f, _scale, 1f, true);
- }
- }
-
- public override void drawWhenHeld(SpriteBatch spriteBatch, Vector2 objectPosition, Farmer f)
- {
- if (this.Texture != null)
- {
- var targSize = 64;
- var midX = (int) (objectPosition.X + 32);
- var midY = (int) (objectPosition.Y + 32);
-
- var targX = midX - targSize / 2;
- var targY = midY - targSize / 2;
-
- spriteBatch.Draw(this.Texture, new Rectangle(targX, targY, targSize, targSize), null, Color.White, 0, Vector2.Zero, SpriteEffects.None, (f.getStandingY() + 2) / 10000f);
- }
- }
-
- public override Color getCategoryColor()
- {
- return this.CategoryColour;
- }
-
- public override string getCategoryName()
- {
- if (string.IsNullOrEmpty(this.CategoryName))
- return "Modded Item";
- return this.CategoryName;
- }
-
- public override bool isPassable()
- {
- return this.IsPassable;
- }
-
- public override bool isPlaceable()
- {
- return this.IsPlaceable;
- }
-
- public override int maximumStackSize()
- {
- return this.MaxStackSize;
- }
-
- public SObject Clone()
- {
- var toRet = new SObject
- {
- Name = this.Name,
- CategoryName = this.CategoryName,
- Description = this.Description,
- Texture = this.Texture,
- IsPassable = this.IsPassable,
- IsPlaceable = this.IsPlaceable,
- quality = this.quality,
- scale = this.scale,
- isSpawnedObject = this.isSpawnedObject,
- isRecipe = this.isRecipe,
- questItem = this.questItem,
- stack = 1,
- HasBeenRegistered = this.HasBeenRegistered,
- RegisteredId = this.RegisteredId
- };
-
-
- return toRet;
- }
-
- public override Item getOne()
- {
- return this.Clone();
- }
-
- public override void actionWhenBeingHeld(Farmer who)
- {
- var x = Game1.oldMouseState.X + Game1.viewport.X;
- var y = Game1.oldMouseState.Y + Game1.viewport.Y;
-
- x = x / Game1.tileSize;
- y = y / Game1.tileSize;
-
- this.CurrentMouse = new Vector2(x, y);
- //Program.LogDebug(canBePlacedHere(Game1.currentLocation, CurrentMouse));
- base.actionWhenBeingHeld(who);
- }
-
- public override bool canBePlacedHere(GameLocation l, Vector2 tile)
- {
- //Program.LogDebug(CurrentMouse.ToString().Replace("{", "").Replace("}", ""));
- if (!l.objects.ContainsKey(tile))
- return true;
-
- return false;
- }
-
- public override bool placementAction(GameLocation location, int x, int y, Farmer who = null)
- {
- if (Game1.didPlayerJustRightClick())
- return false;
-
- x = x / Game1.tileSize;
- y = y / Game1.tileSize;
-
- var key = new Vector2(x, y);
-
- if (!this.canBePlacedHere(location, key))
- return false;
-
- var s = this.Clone();
-
- s.PlacedAt = key;
- s.boundingBox = new Rectangle(x / Game1.tileSize * Game1.tileSize, y / Game1.tileSize * Game1.tileSize, this.boundingBox.Width, this.boundingBox.Height);
-
- location.objects.Add(key, s);
-
- return true;
- }
-
- public override void actionOnPlayerEntry()
- {
- //base.actionOnPlayerEntry();
- }
-
- public override void drawPlacementBounds(SpriteBatch spriteBatch, GameLocation location)
- {
- if (this.canBePlacedHere(location, this.CurrentMouse))
- {
- var targSize = Game1.tileSize;
-
- var x = Game1.oldMouseState.X + Game1.viewport.X;
- var y = Game1.oldMouseState.Y + Game1.viewport.Y;
- spriteBatch.Draw(Game1.mouseCursors, new Vector2(x / Game1.tileSize * Game1.tileSize - Game1.viewport.X, y / Game1.tileSize * Game1.tileSize - Game1.viewport.Y), new Rectangle(Utility.playerCanPlaceItemHere(location, this, x, y, Game1.player) ? 194 : 210, 388, 16, 16), Color.White, 0.0f, Vector2.Zero, Game1.pixelZoom, SpriteEffects.None, 0.01f);
- }
- }
- }
-} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs
index 5cb794f9..d58cebfe 100644
--- a/src/StardewModdingAPI/Log.cs
+++ b/src/StardewModdingAPI/Log.cs
@@ -10,18 +10,32 @@ namespace StardewModdingAPI
public static class Log
{
/*********
- ** Accessors
+ ** Properties
*********/
+ /// <summary>Manages deprecation warnings.</summary>
+ private static DeprecationManager DeprecationManager;
+
/// <summary>The underlying logger.</summary>
- internal static Monitor Monitor;
+ private static Monitor Monitor;
/// <summary>Tracks the installed mods.</summary>
- internal static ModRegistry ModRegistry;
+ private static ModRegistry ModRegistry;
/*********
** Public methods
*********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="deprecationManager">Manages deprecation warnings.</param>
+ /// <param name="monitor">The underlying logger.</param>
+ /// <param name="modRegistry">Tracks the installed mods.</param>
+ internal static void Shim(DeprecationManager deprecationManager, Monitor monitor, ModRegistry modRegistry)
+ {
+ Log.DeprecationManager = deprecationManager;
+ Log.Monitor = monitor;
+ Log.ModRegistry = modRegistry;
+ }
+
/****
** Exceptions
****/
@@ -292,7 +306,7 @@ namespace StardewModdingAPI
/// <summary>Raise a deprecation warning.</summary>
private static void WarnDeprecated()
{
- Program.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Notice);
+ Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Info);
}
/// <summary>Get the name of the mod logging a message from the stack.</summary>
diff --git a/src/StardewModdingAPI/LogInfo.cs b/src/StardewModdingAPI/LogInfo.cs
deleted file mode 100644
index ffef7cef..00000000
--- a/src/StardewModdingAPI/LogInfo.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-
-namespace StardewModdingAPI
-{
- /// <summary>A message queued for log output.</summary>
- public struct LogInfo
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The message to log.</summary>
- public string Message { get; set; }
-
- /// <summary>The log date.</summary>
- public string LogDate { get; set; }
-
- /// <summary>The log time.</summary>
- public string LogTime { get; set; }
-
- /// <summary>The message color.</summary>
- public ConsoleColor Colour { get; set; }
-
- /// <summary>Whether the message should be printed to the console.</summary>
- internal bool PrintConsole { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="message">The message to log.</param>
- /// <param name="color">The message color.</param>
- public LogInfo(string message, ConsoleColor color = ConsoleColor.Gray)
- {
- if (string.IsNullOrEmpty(message))
- message = "[null]";
- this.Message = message;
- this.LogDate = DateTime.Now.ToString("yyyy-MM-dd");
- this.LogTime = DateTime.Now.ToString("HH:mm:ss");
- this.Colour = color;
- this.PrintConsole = true;
- }
- }
-}
diff --git a/src/StardewModdingAPI/LogWriter.cs b/src/StardewModdingAPI/LogWriter.cs
deleted file mode 100644
index e22759a7..00000000
--- a/src/StardewModdingAPI/LogWriter.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using StardewModdingAPI.Framework;
-
-namespace StardewModdingAPI
-{
- /// <summary>A log writer which queues messages for output, and periodically flushes them to the console and log file.</summary>
- /// <remarks>Only one instance should be created.</remarks>
- [Obsolete("This class is internal and should not be referenced outside SMAPI. It will no longer be exposed in a future version.")]
- public class LogWriter
- {
- /*********
- ** Properties
- *********/
- /// <summary>Manages reading and writing to the log file.</summary>
- private readonly LogFileManager LogFile;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="logFile">Manages reading and writing to the log file.</param>
- internal LogWriter(LogFileManager logFile)
- {
- this.WarnDeprecated();
- this.LogFile = logFile;
- }
-
- /// <summary>Queue a message for output.</summary>
- /// <param name="message">The message to log.</param>
- public void WriteToLog(string message)
- {
- this.WarnDeprecated();
- this.WriteToLog(new LogInfo(message));
- }
-
- /// <summary>Queue a message for output.</summary>
- /// <param name="message">The message to log.</param>
- public void WriteToLog(LogInfo message)
- {
- this.WarnDeprecated();
- string output = $"[{message.LogTime}] {message.Message}";
- if (message.PrintConsole)
- {
- if (Monitor.ConsoleSupportsColor)
- {
- Console.ForegroundColor = message.Colour;
- Console.WriteLine(message);
- Console.ResetColor();
- }
- else
- Console.WriteLine(message);
- }
- this.LogFile.WriteLine(output);
- }
-
- /*********
- ** Private methods
- *********/
- /// <summary>Raise a deprecation warning.</summary>
- private void WarnDeprecated()
- {
- Program.DeprecationManager.Warn($"the {nameof(LogWriter)} class", "1.0", DeprecationLevel.PendingRemoval);
- }
- }
-} \ No newline at end of file
diff --git a/src/StardewModdingAPI/Manifest.cs b/src/StardewModdingAPI/Manifest.cs
deleted file mode 100644
index 07dd3541..00000000
--- a/src/StardewModdingAPI/Manifest.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using Newtonsoft.Json;
-
-namespace StardewModdingAPI
-{
- /// <summary>Wraps <see cref="Manifest"/> so it can implement <see cref="IManifest"/> without breaking backwards compatibility.</summary>
- [Obsolete("Use " + nameof(IManifest) + " or " + nameof(Mod) + "." + nameof(Mod.ModManifest) + " instead")]
- internal class ManifestImpl : Manifest, IManifest
- {
- /// <summary>The mod version.</summary>
- [JsonProperty("Version", ObjectCreationHandling = ObjectCreationHandling.Auto/* avoids issue where Json.NET can't determine concrete type for interface */)]
- public new ISemanticVersion Version
- {
- get { return base.Version; }
- set { base.Version = (Version)value; }
- }
- }
-
- /// <summary>A manifest which describes a mod for SMAPI.</summary>
- public class Manifest
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The mod name.</summary>
- public string Name { get; set; }
-
- /// <summary>A brief description of the mod.</summary>
- public string Description { get; set; }
-
- /// <summary>The mod author's name.</summary>
- public string Author { get; set; }
-
- /// <summary>The mod version.</summary>
- public Version Version { get; set; } = new Version(0, 0, 0, "", suppressDeprecationWarning: true);
-
- /// <summary>The minimum SMAPI version required by this mod, if any.</summary>
- public string MinimumApiVersion { get; set; }
-
- /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary>
- public string EntryDll { get; set; }
-
- /// <summary>The unique mod ID.</summary>
- public string UniqueID { get; set; }
-
-
- /****
- ** Obsolete
- ****/
- /// <summary>Whether the manifest defined the deprecated <see cref="Authour"/> field.</summary>
- [JsonIgnore]
- internal bool UsedAuthourField { get; private set; }
-
- /// <summary>Obsolete.</summary>
- [Obsolete("Use " + nameof(Manifest) + "." + nameof(Manifest.Author) + ".")]
- public virtual string Authour
- {
- get { return this.Author; }
- set
- {
- this.UsedAuthourField = true;
- this.Author = value;
- }
- }
-
- /// <summary>Whether the mod uses per-save config files.</summary>
- [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")]
- public bool PerSaveConfigs { get; set; }
- }
-}
diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs
index 0d35939d..caa20774 100644
--- a/src/StardewModdingAPI/Mod.cs
+++ b/src/StardewModdingAPI/Mod.cs
@@ -10,6 +10,9 @@ namespace StardewModdingAPI
/*********
** Properties
*********/
+ /// <summary>Manages deprecation warnings.</summary>
+ private static DeprecationManager DeprecationManager;
+
/// <summary>The backing field for <see cref="Mod.PathOnDisk"/>.</summary>
private string _pathOnDisk;
@@ -24,17 +27,6 @@ namespace StardewModdingAPI
public IMonitor Monitor { get; internal set; }
/// <summary>The mod's manifest.</summary>
- [Obsolete("Use " + nameof(Mod) + "." + nameof(ModManifest))]
- public Manifest Manifest
- {
- get
- {
- Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Manifest)}", "1.5", DeprecationLevel.Notice);
- return (Manifest)this.ModManifest;
- }
- }
-
- /// <summary>The mod's manifest.</summary>
public IManifest ModManifest { get; internal set; }
/// <summary>The full path to the mod's directory on the disk.</summary>
@@ -43,7 +35,7 @@ namespace StardewModdingAPI
{
get
{
- Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Notice);
+ Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Info);
return this._pathOnDisk;
}
internal set { this._pathOnDisk = value; }
@@ -55,25 +47,25 @@ namespace StardewModdingAPI
{
get
{
- Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Notice);
- Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings
+ Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Info);
+ Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings
return Path.Combine(this.PathOnDisk, "config.json");
}
}
- /// <summary>The full path to the per-save configs folder (if <see cref="StardewModdingAPI.Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
+ /// <summary>The full path to the per-save configs folder (if <see cref="Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
[Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")]
public string PerSaveConfigFolder => this.GetPerSaveConfigFolder();
- /// <summary>The full path to the per-save configuration file for the current save (if <see cref="StardewModdingAPI.Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
+ /// <summary>The full path to the per-save configuration file for the current save (if <see cref="Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
[Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")]
public string PerSaveConfigPath
{
get
{
- Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.Info);
- Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings
- return Constants.CurrentSavePathExists ? Path.Combine(this.PerSaveConfigFolder, Constants.SaveFolderName + ".json") : "";
+ Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.Info);
+ Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings
+ return Constants.IsSaveLoaded ? Path.Combine(this.PerSaveConfigFolder, $"{Constants.SaveFolderName}.json") : "";
}
}
@@ -81,31 +73,33 @@ namespace StardewModdingAPI
/*********
** Public methods
*********/
+ /// <summary>Injects types required for backwards compatibility.</summary>
+ /// <param name="deprecationManager">Manages deprecation warnings.</param>
+ internal static void Shim(DeprecationManager deprecationManager)
+ {
+ Mod.DeprecationManager = deprecationManager;
+ }
+
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
[Obsolete("This overload is obsolete since SMAPI 1.0.")]
public virtual void Entry(params object[] objects) { }
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
- [Obsolete("This overload is obsolete since SMAPI 1.1.")]
- public virtual void Entry(ModHelper helper) { }
-
- /// <summary>The mod entry point, called after the mod is first loaded.</summary>
- /// <param name="helper">Provides simplified APIs for writing mods.</param>
public virtual void Entry(IModHelper helper) { }
/*********
** Private methods
*********/
- /// <summary>Get the full path to the per-save configuration file for the current save (if <see cref="StardewModdingAPI.Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
+ /// <summary>Get the full path to the per-save configuration file for the current save (if <see cref="Manifest.PerSaveConfigs"/> is <c>true</c>).</summary>
[Obsolete]
private string GetPerSaveConfigFolder()
{
- Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice);
- Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings
+ Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Info);
+ Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings
- if (!((Manifest)this.Manifest).PerSaveConfigs)
+ 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 "";
diff --git a/src/StardewModdingAPI/PatchMode.cs b/src/StardewModdingAPI/PatchMode.cs
new file mode 100644
index 00000000..b4286a89
--- /dev/null
+++ b/src/StardewModdingAPI/PatchMode.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI
+{
+ /// <summary>Indicates how an image should be patched.</summary>
+ public enum PatchMode
+ {
+ /// <summary>Erase the original content within the area before drawing the new content.</summary>
+ Replace,
+
+ /// <summary>Draw the new content over the original content, so the original content shows through any transparent pixels.</summary>
+ Overlay
+ }
+}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index 45bf1238..58850dc3 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
#if SMAPI_FOR_WINDOWS
+using System.Management;
using System.Windows.Forms;
#endif
using Microsoft.Xna.Framework.Graphics;
@@ -13,76 +15,49 @@ using Newtonsoft.Json;
using StardewModdingAPI.AssemblyRewriters;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
+using StardewModdingAPI.Framework.Logging;
using StardewModdingAPI.Framework.Models;
-using StardewModdingAPI.Inheritance;
+using StardewModdingAPI.Framework.Serialisation;
using StardewValley;
using Monitor = StardewModdingAPI.Framework.Monitor;
namespace StardewModdingAPI
{
/// <summary>The main entry point for SMAPI, responsible for hooking into and launching the game.</summary>
- public class Program
+ internal class Program
{
/*********
** Properties
*********/
- /// <summary>The target game platform.</summary>
- private static readonly Platform TargetPlatform =
-#if SMAPI_FOR_WINDOWS
- Platform.Windows;
-#else
- Platform.Mono;
-#endif
-
- /// <summary>The full path to the Stardew Valley executable.</summary>
- private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, Program.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe");
-
- /// <summary>The full path to the folder containing mods.</summary>
- private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods");
-
/// <summary>The log file to which to write messages.</summary>
- private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath);
+ private readonly LogFileManager LogFile;
+
+ /// <summary>Manages console output interception.</summary>
+ private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager();
/// <summary>The core logger for SMAPI.</summary>
- private static readonly Monitor Monitor = new Monitor("SMAPI", Program.LogFile);
+ private readonly Monitor Monitor;
- /// <summary>The user settings for SMAPI.</summary>
- private static UserSettings Settings;
+ /// <summary>The SMAPI configuration settings.</summary>
+ private readonly SConfig Settings;
/// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary>
- private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
-
-
- /*********
- ** Accessors
- *********/
- /// <summary>The number of mods currently loaded by SMAPI.</summary>
- public static int ModsLoaded;
-
- /// <summary>The underlying game instance.</summary>
- public static SGame gamePtr;
+ private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
/// <summary>Whether the game is currently running.</summary>
- public static bool ready;
-
- /// <summary>The underlying game assembly.</summary>
- public static Assembly StardewAssembly;
-
- /// <summary>The underlying <see cref="StardewValley.Program"/> type.</summary>
- public static Type StardewProgramType;
+ private bool IsGameRunning;
- /// <summary>The field containing game's main instance.</summary>
- public static FieldInfo StardewGameInfo;
-
- // ReSharper disable once PossibleNullReferenceException
- /// <summary>The game's build type (i.e. GOG vs Steam).</summary>
- public static int BuildType => (int)Program.StardewProgramType.GetField("buildType", BindingFlags.Public | BindingFlags.Static).GetValue(null);
+ /// <summary>The underlying game instance.</summary>
+ private SGame GameInstance;
/// <summary>Tracks the installed mods.</summary>
- internal static readonly ModRegistry ModRegistry = new ModRegistry();
+ private readonly ModRegistry ModRegistry;
/// <summary>Manages deprecation warnings.</summary>
- internal static readonly DeprecationManager DeprecationManager = new DeprecationManager(Program.Monitor, Program.ModRegistry);
+ private readonly DeprecationManager DeprecationManager;
+
+ /// <summary>Manages console commands.</summary>
+ private readonly CommandManager CommandManager = new CommandManager();
/*********
@@ -92,101 +67,141 @@ namespace StardewModdingAPI
/// <param name="args">The command-line arguments.</param>
private static void Main(string[] args)
{
- // set log options
- Program.Monitor.WriteToConsole = !args.Contains("--no-terminal");
- Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting
-
- // add info header
- Program.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info);
+ // get flags from arguments
+ bool writeToConsole = !args.Contains("--no-terminal");
- // initialise user settings
+ // get log path from arguments
+ string logPath = null;
{
- string settingsPath = Constants.ApiConfigPath;
- if (File.Exists(settingsPath))
+ int pathIndex = Array.LastIndexOf(args, "--log-path") + 1;
+ if (pathIndex >= 1 && args.Length >= pathIndex)
{
- string json = File.ReadAllText(settingsPath);
- Program.Settings = JsonConvert.DeserializeObject<UserSettings>(json);
+ logPath = args[pathIndex];
+ if (!Path.IsPathRooted(logPath))
+ logPath = Path.Combine(Constants.LogDir, logPath);
}
- else
- Program.Settings = new UserSettings();
+ }
+ if (string.IsNullOrWhiteSpace(logPath))
+ logPath = Constants.DefaultLogPath;
- File.WriteAllText(settingsPath, JsonConvert.SerializeObject(Program.Settings, Formatting.Indented));
+ // load SMAPI
+ new Program(writeToConsole, logPath)
+ .LaunchInteractively();
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="writeToConsole">Whether to output log messages to the console.</param>
+ /// <param name="logPath">The full file path to which to write log messages.</param>
+ internal Program(bool writeToConsole, string logPath)
+ {
+ // load settings
+ this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath));
+
+ // initialise
+ this.LogFile = new LogFileManager(logPath);
+ this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole };
+ this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility);
+ this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
+ }
+
+ /// <summary>Launch SMAPI.</summary>
+ internal void LaunchInteractively()
+ {
+ // initialise logging
+ Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting
+ this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info);
+ Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}";
+
+ // inject compatibility shims
+#pragma warning disable 618
+ Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry);
+ Config.Shim(this.DeprecationManager);
+ InternalExtensions.Shim(this.ModRegistry);
+ Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry);
+ Mod.Shim(this.DeprecationManager);
+ ContentEvents.Shim(this.ModRegistry, this.Monitor);
+ PlayerEvents.Shim(this.DeprecationManager);
+ TimeEvents.Shim(this.DeprecationManager);
+#pragma warning restore 618
+
+ // redirect direct console output
+ {
+ Monitor monitor = this.GetSecondaryMonitor("Console.Out");
+ monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion
+ if (monitor.WriteToConsole)
+ this.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace);
}
// add warning headers
- if (Program.Settings.DeveloperMode)
+ if (this.Settings.DeveloperMode)
{
- Program.Monitor.ShowTraceInConsole = true;
- Program.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 or deleting {Constants.ApiConfigPath}.", LogLevel.Warn);
+ this.Monitor.ShowTraceInConsole = true;
+ this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Warn);
}
- if (!Program.Settings.CheckForUpdates)
- Program.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 editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn);
- if (!Program.Monitor.WriteToConsole)
- Program.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.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);
// print file paths
- Program.Monitor.Log($"Mods go here: {Program.ModPath}");
-
- // initialise legacy log
- Log.Monitor = Program.GetSecondaryMonitor("legacy mod");
- Log.ModRegistry = Program.ModRegistry;
+ this.Monitor.Log($"Mods go here: {Constants.ModPath}");
// hook into & launch the game
try
{
// verify version
- if (String.Compare(Game1.version, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0)
+ if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion))
{
- Program.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error);
+ this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error);
+ this.PressAnyKeyToExit();
return;
}
-
- // initialise
- Program.Monitor.Log("Loading SMAPI...");
- Console.Title = Constants.ConsoleTitle;
- Program.VerifyPath(Program.ModPath);
- Program.VerifyPath(Constants.LogDir);
- if (!File.Exists(Program.GameExecutablePath))
+ if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion))
{
- Program.Monitor.Log($"Couldn't find executable: {Program.GameExecutablePath}", LogLevel.Error);
- Program.PressAnyKeyToExit();
+ this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.GetGameDisplayVersion(Constants.MaximumGameVersion)}. Please check for a newer version of SMAPI.", LogLevel.Error);
+ this.PressAnyKeyToExit();
return;
}
+ // initialise folders
+ this.Monitor.Log("Loading SMAPI...");
+ this.VerifyPath(Constants.ModPath);
+ this.VerifyPath(Constants.LogDir);
+
// check for update when game loads
- if (Program.Settings.CheckForUpdates)
- GameEvents.GameLoaded += (sender, e) => Program.CheckForUpdateAsync();
+ if (this.Settings.CheckForUpdates)
+ GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync();
// launch game
- Program.StartGame();
+ this.StartGame();
}
catch (Exception ex)
{
- Program.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error);
}
- Program.PressAnyKeyToExit();
+ this.PressAnyKeyToExit();
}
/// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
/// <param name="module">The module which requested an immediate exit.</param>
/// <param name="reason">The reason provided for the shutdown.</param>
- internal static void ExitGameImmediately(string module, string reason)
+ internal void ExitGameImmediately(string module, string reason)
{
- Program.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}");
- Program.CancellationTokenSource.Cancel();
- if (Program.ready)
+ this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}");
+ this.CancellationTokenSource.Cancel();
+ if (this.IsGameRunning)
{
- Program.gamePtr.Exiting += (sender, e) => Program.PressAnyKeyToExit();
- Program.gamePtr.Exit();
+ this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit();
+ this.GameInstance.Exit();
}
}
/// <summary>Get a monitor for legacy code which doesn't have one passed in.</summary>
[Obsolete("This method should only be used when needed for backwards compatibility.")]
- internal static IMonitor GetLegacyMonitorForMod()
+ internal IMonitor GetLegacyMonitorForMod()
{
- string modName = Program.ModRegistry.GetModFromStack() ?? "unknown";
- return Program.GetSecondaryMonitor(modName);
+ string modName = this.ModRegistry.GetModFromStack() ?? "unknown";
+ return this.GetSecondaryMonitor(modName);
}
@@ -194,7 +209,7 @@ namespace StardewModdingAPI
** Private methods
*********/
/// <summary>Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available.</summary>
- private static void CheckForUpdateAsync()
+ private void CheckForUpdateAsync()
{
new Thread(() =>
{
@@ -203,49 +218,47 @@ namespace StardewModdingAPI
GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result;
ISemanticVersion latestVersion = new SemanticVersion(release.Tag);
if (latestVersion.IsNewerThan(Constants.ApiVersion))
- Program.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert);
+ this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert);
}
catch (Exception ex)
{
- Program.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.\n{ex.GetLogSummary()}");
}
}).Start();
}
/// <summary>Hook into Stardew Valley and launch the game.</summary>
- private static void StartGame()
+ private void StartGame()
{
try
{
- // load the game assembly
- Program.Monitor.Log("Loading game...");
- Program.StardewAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath);
- Program.StardewProgramType = Program.StardewAssembly.GetType("StardewValley.Program", true);
- Program.StardewGameInfo = Program.StardewProgramType.GetField("gamePtr");
- Game1.version += $" | SMAPI {Constants.ApiVersion}";
-
- // add error interceptors
+ this.Monitor.Log("Loading game...");
+
+ // add error handlers
#if SMAPI_FOR_WINDOWS
- Application.ThreadException += (sender, e) => Program.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error);
+ Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
#endif
- AppDomain.CurrentDomain.UnhandledException += (sender, e) => Program.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error);
+ AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error);
- // initialise game instance
- Program.gamePtr = new SGame(Program.Monitor) { IsMouseVisible = false };
- Program.gamePtr.Exiting += (sender, e) => Program.ready = false;
- Program.gamePtr.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e);
- Program.gamePtr.Window.Title = $"Stardew Valley - Version {Game1.version}";
- Program.StardewGameInfo.SetValue(Program.StardewProgramType, Program.gamePtr);
+ // override Game1 instance
+ this.GameInstance = new SGame(this.Monitor);
+ this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false;
+ this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e);
+ this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} with SMAPI {Constants.ApiVersion}";
+ {
+ Type type = typeof(Game1).Assembly.GetType("StardewValley.Program", true);
+ type.GetField("gamePtr").SetValue(null, this.GameInstance);
+ }
- // patch graphics
+ // configure
Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef;
// load mods
- Program.LoadMods();
- if (Program.CancellationTokenSource.IsCancellationRequested)
+ this.LoadMods();
+ if (this.CancellationTokenSource.IsCancellationRequested)
{
- Program.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error);
+ this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error);
return;
}
@@ -253,17 +266,18 @@ namespace StardewModdingAPI
new Thread(() =>
{
// wait for the game to load up
- while (!Program.ready) Thread.Sleep(1000);
+ while (!this.IsGameRunning)
+ Thread.Sleep(1000);
// register help command
- Command.RegisterCommand("help", "Lists all commands | 'help <cmd>' returns command description").CommandFired += Program.help_CommandFired;
+ this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help <cmd>' returns command description", this.HandleHelpCommand);
// listen for command line input
- Program.Monitor.Log("Starting console...");
- Program.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
- Thread consoleInputThread = new Thread(Program.ConsoleInputLoop);
+ this.Monitor.Log("Starting console...");
+ this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
+ Thread consoleInputThread = new Thread(this.ConsoleInputLoop);
consoleInputThread.Start();
- while (Program.ready)
+ while (this.IsGameRunning)
Thread.Sleep(1000 / 10); // Check if the game is still running 10 times a second
// abort the console thread, we're closing
@@ -272,31 +286,31 @@ namespace StardewModdingAPI
}).Start();
// start game loop
- Program.Monitor.Log("Starting game...");
- if (Program.CancellationTokenSource.IsCancellationRequested)
+ this.Monitor.Log("Starting game...");
+ if (this.CancellationTokenSource.IsCancellationRequested)
{
- Program.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error);
+ this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error);
return;
}
try
{
- Program.ready = true;
- Program.gamePtr.Run();
+ this.IsGameRunning = true;
+ this.GameInstance.Run();
}
finally
{
- Program.ready = false;
+ this.IsGameRunning = false;
}
}
catch (Exception ex)
{
- Program.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error);
}
}
/// <summary>Create a directory path if it doesn't exist.</summary>
/// <param name="path">The directory path.</param>
- private static void VerifyPath(string path)
+ private void VerifyPath(string path)
{
try
{
@@ -305,109 +319,97 @@ namespace StardewModdingAPI
}
catch (Exception ex)
{
- Program.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error);
}
}
/// <summary>Load and hook up all mods in the mod directory.</summary>
- private static void LoadMods()
+ private void LoadMods()
{
- Program.Monitor.Log("Loading mods...");
+ this.Monitor.Log("Loading mods...");
+
+ // get JSON helper
+ JsonHelper jsonHelper = new JsonHelper();
// get assembly loader
- AssemblyLoader modAssemblyLoader = new AssemblyLoader(Program.TargetPlatform, Program.Monitor);
+ AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor);
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
- // get known incompatible mods
- IDictionary<string, IncompatibleMod> incompatibleMods;
- try
- {
- incompatibleMods = File.Exists(Constants.ApiModMetadataPath)
- ? JsonConvert.DeserializeObject<IncompatibleMod[]>(File.ReadAllText(Constants.ApiModMetadataPath)).ToDictionary(p => p.ID, p => p)
- : new Dictionary<string, IncompatibleMod>(0);
- }
- catch (Exception ex)
- {
- incompatibleMods = new Dictionary<string, IncompatibleMod>();
- Program.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn);
- }
-
// load mod assemblies
+ int modsLoaded = 0;
List<Action> deprecationWarnings = new List<Action>(); // queue up deprecation warnings to show after mod list
- foreach (string directory in Directory.GetDirectories(Program.ModPath))
+ foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath))
{
- string directoryName = new DirectoryInfo(directory).Name;
+ // passthrough empty directories
+ DirectoryInfo directory = new DirectoryInfo(directoryPath);
+ while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
+ directory = directory.GetDirectories().First();
// check for cancellation
- if (Program.CancellationTokenSource.IsCancellationRequested)
+ if (this.CancellationTokenSource.IsCancellationRequested)
{
- Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
+ this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
return;
}
- // get helper
- IModHelper helper = new ModHelper(directory, Program.ModRegistry);
-
// get manifest path
- string manifestPath = Path.Combine(directory, "manifest.json");
+ string manifestPath = Path.Combine(directory.FullName, "manifest.json");
if (!File.Exists(manifestPath))
{
- Program.Monitor.Log($"Ignored folder \"{directoryName}\" which doesn't have a manifest.json.", LogLevel.Warn);
+ this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn);
continue;
}
- string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'";
+ string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}";
// read manifest
- ManifestImpl manifest;
+ Manifest manifest;
try
{
// read manifest text
string json = File.ReadAllText(manifestPath);
if (string.IsNullOrEmpty(json))
{
- Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error);
continue;
}
// deserialise manifest
- manifest = helper.ReadJsonFile<ManifestImpl>("manifest.json");
+ manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
if (manifest == null)
{
- Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error);
continue;
}
if (string.IsNullOrEmpty(manifest.EntryDll))
{
- Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error);
continue;
}
-
- // log deprecated fields
- if (manifest.UsedAuthourField)
- deprecationWarnings.Add(() => Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0", DeprecationLevel.Notice));
}
catch (Exception ex)
{
- Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
+ if (!string.IsNullOrWhiteSpace(manifest.Name))
+ skippedPrefix = $"Skipped {manifest.Name}";
- // validate known incompatible mods
- IncompatibleMod compatibility;
- if (incompatibleMods.TryGetValue(manifest.UniqueID ?? $"{manifest.Name}|{manifest.Author}|{manifest.EntryDll}", out compatibility))
+ // validate compatibility
+ ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
+ if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
{
- if (!compatibility.IsCompatible(manifest.Version))
- {
- string reasonPhrase = compatibility.ReasonPhrase ?? "this version is not compatible with the latest version of the game";
- string warning = $"Skipped {compatibility.Name} {manifest.Version} because {reasonPhrase}. Please check for a newer version of the mod here:";
- if (!string.IsNullOrWhiteSpace(compatibility.UpdateUrl))
- warning += $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
- if (!string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl))
- warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
-
- Program.Monitor.Log(warning, LogLevel.Error);
- continue;
- }
+ bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
+ bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
+
+ string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
+ string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
+ if (hasOfficialUrl)
+ warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
+ if (hasUnofficialUrl)
+ warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
+
+ this.Monitor.Log(warning, LogLevel.Error);
+ continue;
}
// validate SMAPI version
@@ -418,13 +420,13 @@ namespace StardewModdingAPI
ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
if (minVersion.IsNewerThan(Constants.ApiVersion))
{
- Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
continue;
}
}
catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
{
- Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error);
continue;
}
}
@@ -432,29 +434,29 @@ namespace StardewModdingAPI
// create per-save directory
if (manifest.PerSaveConfigs)
{
- deprecationWarnings.Add(() => Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
+ deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
try
{
- string psDir = Path.Combine(directory, "psconfigs");
+ string psDir = Path.Combine(directory.FullName, "psconfigs");
Directory.CreateDirectory(psDir);
if (!Directory.Exists(psDir))
{
- Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error);
continue;
}
}
catch (Exception ex)
{
- Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
}
// validate mod path to simplify errors
- string assemblyPath = Path.Combine(directory, manifest.EntryDll);
+ string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
if (!File.Exists(assemblyPath))
{
- Program.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error);
continue;
}
@@ -462,121 +464,139 @@ namespace StardewModdingAPI
Assembly modAssembly;
try
{
- modAssembly = modAssemblyLoader.Load(assemblyPath);
+ modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible);
+ }
+ catch (IncompatibleInstructionException ex)
+ {
+ this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error);
+ continue;
}
catch (Exception ex)
{
- Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
// validate assembly
try
{
- if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0)
+ int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
+ if (modEntries == 0)
+ {
+ this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error);
+ continue;
+ }
+ if (modEntries > 1)
{
- Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error);
continue;
}
}
catch (Exception ex)
{
- Program.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error);
+ this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
// initialise mod
- Mod mod;
try
{
// get implementation
- TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod));
- mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
+ TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
+ Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
if (mod == null)
{
- Program.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated.");
+ this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated.");
continue;
}
// inject data
+ // get helper
mod.ModManifest = manifest;
- mod.Helper = helper;
- mod.Monitor = Program.GetSecondaryMonitor(manifest.Name);
- mod.PathOnDisk = directory;
+ mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager);
+ mod.Monitor = this.GetSecondaryMonitor(manifest.Name);
+ mod.PathOnDisk = directory.FullName;
// track mod
- Program.ModRegistry.Add(mod);
- Program.ModsLoaded += 1;
- Program.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info);
+ this.ModRegistry.Add(mod);
+ modsLoaded += 1;
+ this.Monitor.Log($"Loaded {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info);
}
catch (Exception ex)
{
- Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
+ this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error);
}
}
- // log deprecation warnings
- foreach (Action warning in deprecationWarnings)
- warning();
- deprecationWarnings = null;
-
// initialise mods
- foreach (Mod mod in Program.ModRegistry.GetMods())
+ foreach (Mod mod in this.ModRegistry.GetMods())
{
try
{
// call entry methods
mod.Entry(); // deprecated since 1.0
- mod.Entry((ModHelper)mod.Helper); // deprecated since 1.1
mod.Entry(mod.Helper);
// raise deprecation warning for old Entry() methods
- if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) }))
- Program.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice);
- if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) }))
- Program.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}({nameof(ModHelper)}) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.1", DeprecationLevel.PendingRemoval);
+ if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) }))
+ deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info));
}
catch (Exception ex)
{
- Program.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn);
+ this.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn);
}
}
// print result
- Program.Monitor.Log($"Loaded {Program.ModsLoaded} mods.");
- Console.Title = Constants.ConsoleTitle;
+ this.Monitor.Log($"Loaded {modsLoaded} mods.");
+ foreach (Action warning in deprecationWarnings)
+ warning();
+ Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods";
}
- // ReSharper disable once FunctionNeverReturns
/// <summary>Run a loop handling console input.</summary>
- private static void ConsoleInputLoop()
+ [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")]
+ private void ConsoleInputLoop()
{
while (true)
- Command.CallCommand(Console.ReadLine(), Program.Monitor);
+ {
+ string input = Console.ReadLine();
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input))
+ this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error);
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
+ }
}
/// <summary>The method called when the user submits the help command in the console.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event data.</param>
- private static void help_CommandFired(object sender, EventArgsCommand e)
+ /// <param name="name">The command name.</param>
+ /// <param name="arguments">The command arguments.</param>
+ private void HandleHelpCommand(string name, string[] arguments)
{
- if (e.Command.CalledArgs.Length > 0)
+ if (arguments.Any())
{
- var command = Command.FindCommand(e.Command.CalledArgs[0]);
- if (command == null)
- Program.Monitor.Log("The specified command could't be found", LogLevel.Error);
+ Framework.Command result = this.CommandManager.Get(arguments[0]);
+ if (result == null)
+ this.Monitor.Log("There's no command with that name.", LogLevel.Error);
else
- Program.Monitor.Log(command.CommandArgs.Length > 0 ? $"{command.CommandName}: {command.CommandDesc} - {string.Join(", ", command.CommandArgs)}" : $"{command.CommandName}: {command.CommandDesc}", LogLevel.Info);
+ this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info);
}
else
- Program.Monitor.Log("Commands: " + string.Join(", ", Command.RegisteredCommands.Select(x => x.CommandName)), LogLevel.Info);
+ {
+ 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);
+ }
}
/// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
- private static void PressAnyKeyToExit()
+ private void PressAnyKeyToExit()
{
- Program.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info);
+ this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info);
Thread.Sleep(100);
Console.ReadKey();
Environment.Exit(0);
@@ -584,9 +604,27 @@ namespace StardewModdingAPI
/// <summary>Get a monitor instance derived from SMAPI's current settings.</summary>
/// <param name="name">The name of the module which will log messages with this instance.</param>
- private static Monitor GetSecondaryMonitor(string name)
+ private Monitor GetSecondaryMonitor(string name)
+ {
+ return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode };
+ }
+
+ /// <summary>Get a human-readable name for the current platform.</summary>
+ [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
+ private string GetFriendlyPlatformName()
{
- return new Monitor(name, Program.LogFile) { WriteToConsole = Program.Monitor.WriteToConsole, ShowTraceInConsole = Program.Settings.DeveloperMode };
+#if SMAPI_FOR_WINDOWS
+ try
+ {
+ return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
+ .Get()
+ .Cast<ManagementObject>()
+ .Select(entry => entry.GetPropertyValue("Caption").ToString())
+ .FirstOrDefault();
+ }
+ catch { }
+#endif
+ return Environment.OSVersion.ToString();
}
}
}
diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs
index c29f2cf7..9610562f 100644
--- a/src/StardewModdingAPI/SemanticVersion.cs
+++ b/src/StardewModdingAPI/SemanticVersion.cs
@@ -13,28 +13,21 @@ namespace StardewModdingAPI
/// <remarks>Derived from https://github.com/maxhauser/semver.</remarks>
private static readonly Regex Regex = new Regex(@"^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?(?<build>.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
- /// <summary>The backing field for <see cref="Build"/>.</summary>
- private string _build;
-
/*********
** Accessors
*********/
/// <summary>The major version incremented for major API changes.</summary>
- public int MajorVersion { get; set; }
+ public int MajorVersion { get; }
/// <summary>The minor version incremented for backwards-compatible changes.</summary>
- public int MinorVersion { get; set; }
+ public int MinorVersion { get; }
/// <summary>The patch version for backwards-compatible bug fixes.</summary>
- public int PatchVersion { get; set; }
+ public int PatchVersion { get; }
/// <summary>An optional build tag.</summary>
- public string Build
- {
- get { return this._build; }
- set { this._build = this.GetNormalisedTag(value); }
- }
+ public string Build { get; }
/*********
@@ -50,7 +43,7 @@ namespace StardewModdingAPI
this.MajorVersion = major;
this.MinorVersion = minor;
this.PatchVersion = patch;
- this.Build = build;
+ this.Build = this.GetNormalisedTag(build);
}
/// <summary>Construct an instance.</summary>
@@ -65,14 +58,18 @@ namespace StardewModdingAPI
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["build"].Success ? match.Groups["build"].Value : null;
+ this.Build = match.Groups["build"].Success ? this.GetNormalisedTag(match.Groups["build"].Value) : null;
}
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception>
/// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks>
public int CompareTo(ISemanticVersion other)
{
+ if(other == null)
+ throw new ArgumentNullException(nameof(other));
+
const int same = 0;
const int curNewer = 1;
const int curOlder = -1;
diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json
index 2abaf73a..0b6f3a37 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.config.json
+++ b/src/StardewModdingAPI/StardewModdingAPI.config.json
@@ -1,4 +1,198 @@
-{
+/*
+
+
+
+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,
- "CheckForUpdates": 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,
+
+ /**
+ * A list of mod versions SMAPI should consider compatible or broken regardless of whether it
+ * detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`.
+ * Changing this field is not recommended and may destabilise your game.
+ */
+ "ModCompatibility": [
+ {
+ "Name": "Almighty Tool",
+ "ID": "AlmightyTool.dll",
+ "UpperVersion": "1.1.1",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439",
+ "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ },
+ {
+ "Name": "Better Sprinklers",
+ "ID": "SPDSprinklersMod",
+ "UpperVersion": "2.3",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41",
+ "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ },
+ {
+ "Name": "Better Sprinklers",
+ "ID": "Speeder.BetterSprinklers",
+ "UpperVersion": "2.3",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41",
+ "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ },
+ {
+ "Name": "Chest Label System",
+ "ID": "SPDChestLabel",
+ "UpperVersion": "1.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242",
+ "Notes": "Not compatible with Stardew Valley 1.1+"
+ },
+ {
+ "Name": "CJB Cheats Menu",
+ "ID": "CJBCheatsMenu",
+ "UpperVersion": "1.12",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4",
+ "Notes": "Not compatible with Stardew Valley 1.1+"
+ },
+ {
+ "Name": "CJB Item Spawner",
+ "ID": "CJBItemSpawner",
+ "UpperVersion": "1.5",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93",
+ "Notes": "Not compatible with Stardew Valley 1.1+"
+ },
+ {
+ "Name": "CJB Show Item Sell Price",
+ "ID": "CJBShowItemSellPrice",
+ "UpperVersion": "1.6",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93",
+ "Notes": "Uses SMAPI's internal SGame class."
+ },
+ {
+ "Name": "Enemy Health Bars",
+ "ID": "SPDHealthBar",
+ "UpperVersion": "1.7",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193",
+ "Notes": "Uses obsolete GraphicsEvents.DrawTick."
+ },
+ {
+ "Name": "Entoarox Framework",
+ "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9",
+ "UpperVersion": "1.6.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/resources/4228",
+ "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')."
+ },
+ {
+ "Name": "Makeshift Multiplayer",
+ "ID": "StardewValleyMP",
+ "Compatibility": "AssumeBroken",
+ "UpperVersion": "0.2.10",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501",
+ "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck."
+ },
+ {
+ "Name": "NoSoilDecay",
+ "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610",
+ "UpperVersion": "0.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237",
+ "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ },
+ {
+ "Name": "NPC Map Locations",
+ "ID": "NPCMapLocationsMod",
+ "LowerVersion": "1.42",
+ "UpperVersion": "1.43",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239",
+ "ReasonPhrase": "this version has an update check error which crashes the game"
+ },
+ {
+ "Name": "Point-and-Plant",
+ "ID": "PointAndPlant.dll",
+ "UpperVersion": "1.0.2",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572",
+ "Notes": "Uses obsolete StardewModdingAPI.Extensions."
+ },
+ {
+ "Name": "Save Anywhere",
+ "ID": "SaveAnywhere",
+ "UpperVersion": "2.0",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444",
+ "Notes": "Depends on StarDustCore."
+ },
+ {
+ "Name": "StackSplitX",
+ "ID": "StackSplitX.dll",
+ "UpperVersion": "1.0",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798",
+ "Notes": "Uses SMAPI's internal SGame class."
+ },
+ {
+ "Name": "StarDustCore",
+ "ID": "StarDustCore",
+ "UpperVersion": "1.0",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683",
+ "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'."
+ },
+ {
+ "Name": "Zoryn's Better RNG",
+ "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6",
+ "UpperVersion": "1.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/108756",
+ "Notes": "Uses SMAPI's internal SGame class."
+ },
+ {
+ "Name": "Zoryn's Calendar Anywhere",
+ "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a",
+ "UpperVersion": "1.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/108756",
+ "Notes": "Uses SMAPI's internal SGame class."
+ },
+ {
+ "Name": "Zoryn's Health Bars",
+ "ID": "HealthBars.dll",
+ "UpperVersion": "1.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/108756",
+ "Notes": "Uses SMAPI's internal SGame class."
+ },
+ {
+ "Name": "Zoryn's Movement Mod",
+ "ID": "8a632929-8335-484f-87dd-c29d2ba3215d",
+ "UpperVersion": "1.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/108756",
+ "Notes": "Uses SMAPI's internal SGame class."
+ },
+ {
+ "Name": "Zoryn's Regen Mod",
+ "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e",
+ "UpperVersion": "1.5",
+ "Compatibility": "AssumeBroken",
+ "UpdateUrl": "http://community.playstarbound.com/threads/108756",
+ "Notes": "Uses SMAPI's internal SGame class."
+ }
+ ]
}
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 337929e2..bcd0c390 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -60,7 +60,6 @@
<OutputPath>$(SolutionDir)\..\bin\Debug\SMAPI</OutputPath>
<DocumentationFile>$(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml</DocumentationFile>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
- <LangVersion>6</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
@@ -70,7 +69,6 @@
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
- <LangVersion>6</LangVersion>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
@@ -90,10 +88,6 @@
<HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath>
<Private>True</Private>
</Reference>
- <Reference Include="Mono.Cecil.Rocks, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
- <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll</HintPath>
- <Private>True</Private>
- </Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
@@ -101,6 +95,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
+ <Reference Include="System.Management" Condition="$(OS) == 'Windows_NT'" />
<Reference Include="System.Numerics">
<Private>True</Private>
</Reference>
@@ -118,12 +113,12 @@
<Compile Include="..\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
- <Compile Include="Advanced\ConfigFile.cs" />
- <Compile Include="Advanced\IConfigFile.cs" />
<Compile Include="Command.cs" />
+ <Compile Include="Events\ContentEvents.cs" />
+ <Compile Include="Events\EventArgsValueChanged.cs" />
+ <Compile Include="Framework\Command.cs" />
<Compile Include="Config.cs" />
<Compile Include="Constants.cs" />
- <Compile Include="Entities\SPlayer.cs" />
<Compile Include="Events\ControlEvents.cs" />
<Compile Include="Events\EventArgsCommand.cs" />
<Compile Include="Events\EventArgsClickableMenuChanged.cs" />
@@ -150,6 +145,27 @@
<Compile Include="Events\GraphicsEvents.cs" />
<Compile Include="Framework\AssemblyDefinitionResolver.cs" />
<Compile Include="Framework\AssemblyParseResult.cs" />
+ <Compile Include="Framework\CommandManager.cs" />
+ <Compile Include="Framework\Content\ContentEventData.cs" />
+ <Compile Include="Framework\Content\ContentEventHelper.cs" />
+ <Compile Include="Framework\Content\ContentEventHelperForDictionary.cs" />
+ <Compile Include="Framework\Content\ContentEventHelperForImage.cs" />
+ <Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" />
+ <Compile Include="Framework\Logging\InterceptingTextWriter.cs" />
+ <Compile Include="Framework\CommandHelper.cs" />
+ <Compile Include="Framework\Models\ModCompatibilityType.cs" />
+ <Compile Include="Framework\Models\SConfig.cs" />
+ <Compile Include="Framework\Reflection\PrivateProperty.cs" />
+ <Compile Include="Framework\RequestExitDelegate.cs" />
+ <Compile Include="Framework\SContentManager.cs" />
+ <Compile Include="Framework\Serialisation\JsonHelper.cs" />
+ <Compile Include="Framework\Serialisation\SelectiveStringEnumConverter.cs" />
+ <Compile Include="Framework\Serialisation\SemanticVersionConverter.cs" />
+ <Compile Include="ICommandHelper.cs" />
+ <Compile Include="IContentEventData.cs" />
+ <Compile Include="IContentEventHelper.cs" />
+ <Compile Include="IContentEventHelperForDictionary.cs" />
+ <Compile Include="IContentEventHelperForImage.cs" />
<Compile Include="IModRegistry.cs" />
<Compile Include="Events\LocationEvents.cs" />
<Compile Include="Events\MenuEvents.cs" />
@@ -157,11 +173,10 @@
<Compile Include="Events\PlayerEvents.cs" />
<Compile Include="Events\SaveEvents.cs" />
<Compile Include="Events\TimeEvents.cs" />
- <Compile Include="Extensions.cs" />
<Compile Include="Framework\DeprecationLevel.cs" />
<Compile Include="Framework\DeprecationManager.cs" />
<Compile Include="Framework\InternalExtensions.cs" />
- <Compile Include="Framework\Models\IncompatibleMod.cs" />
+ <Compile Include="Framework\Models\ModCompatibility.cs" />
<Compile Include="Framework\AssemblyLoader.cs" />
<Compile Include="Framework\Reflection\CacheEntry.cs" />
<Compile Include="Framework\Reflection\PrivateField.cs" />
@@ -170,32 +185,29 @@
<Compile Include="IManifest.cs" />
<Compile Include="IMod.cs" />
<Compile Include="IModHelper.cs" />
- <Compile Include="Framework\LogFileManager.cs" />
+ <Compile Include="Framework\Logging\LogFileManager.cs" />
+ <Compile Include="IPrivateProperty.cs" />
<Compile Include="ISemanticVersion.cs" />
<Compile Include="LogLevel.cs" />
<Compile Include="Framework\ModRegistry.cs" />
<Compile Include="Framework\UpdateHelper.cs" />
<Compile Include="Framework\Models\GitRelease.cs" />
- <Compile Include="Framework\Models\UserSettings.cs" />
<Compile Include="IMonitor.cs" />
- <Compile Include="Inheritance\ChangeType.cs" />
- <Compile Include="Inheritance\ItemStackChange.cs" />
- <Compile Include="Inheritance\SObject.cs" />
+ <Compile Include="Events\ChangeType.cs" />
+ <Compile Include="Events\ItemStackChange.cs" />
<Compile Include="Log.cs" />
<Compile Include="Framework\Monitor.cs" />
- <Compile Include="LogInfo.cs" />
- <Compile Include="LogWriter.cs" />
- <Compile Include="Manifest.cs" />
+ <Compile Include="Framework\Manifest.cs" />
<Compile Include="Mod.cs" />
- <Compile Include="ModHelper.cs" />
+ <Compile Include="Framework\ModHelper.cs" />
+ <Compile Include="PatchMode.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="Inheritance\SGame.cs" />
+ <Compile Include="Framework\SGame.cs" />
<Compile Include="IPrivateField.cs" />
<Compile Include="IPrivateMethod.cs" />
<Compile Include="IReflectionHelper.cs" />
<Compile Include="SemanticVersion.cs" />
- <Compile Include="Version.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config">
@@ -207,9 +219,6 @@
<Content Include="StardewModdingAPI.config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
- <Content Include="StardewModdingAPI.data.json">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </Content>
<None Include="unix-launcher.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@@ -248,22 +257,27 @@
<Target Name="BeforeBuild">
<Error Condition="!Exists('$(GamePath)')" Text="Failed to find the game install path automatically; edit the *.csproj file and manually add a &lt;GamePath&gt; setting with the full directory path containing the Stardew Valley executable." />
</Target>
- <!-- copy files into game directory and enable debugging (only in debug mode, and only in Windows because I haven't tried it with Linux/Mac) -->
- <PropertyGroup Condition="$(Configuration) == 'Debug' AND $(OS) == 'Windows_NT'">
- <StartAction>Program</StartAction>
- <StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram>
- <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory>
- </PropertyGroup>
+
+ <!-- copy files into game directory and enable debugging (only in debug mode) -->
<Target Name="AfterBuild" Condition="$(Configuration) == 'Debug'">
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)" />
- <Copy SourceFiles="$(TargetDir)\$(TargetName).data.json" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe.mdb" DestinationFolder="$(GamePath)" Condition="$(OS) != 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" />
- <Copy SourceFiles="$(TargetDir)\Mono.Cecil.Rocks.dll" DestinationFolder="$(GamePath)" />
</Target>
-</Project> \ No newline at end of file
+
+ <!-- launch SMAPI on debug -->
+ <PropertyGroup Condition="$(Configuration) == 'Debug'">
+ <StartAction>Program</StartAction>
+ <StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram>
+ <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory>
+ </PropertyGroup>
+
+ <!-- Somehow this makes Visual Studio for Mac recognise the previous section. -->
+ <!-- Nobody knows why. -->
+ <PropertyGroup Condition="'$(RunConfiguration)' == 'Default'" />
+</Project>
diff --git a/src/StardewModdingAPI/StardewModdingAPI.data.json b/src/StardewModdingAPI/StardewModdingAPI.data.json
deleted file mode 100644
index 3295336f..00000000
--- a/src/StardewModdingAPI/StardewModdingAPI.data.json
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-
-
-This file contains advanced metadata for SMAPI. You shouldn't change this file.
-
-
-*/
-[
- /* versions not compatible with Stardew Valley 1.1+ */
- {
- "ID": "SPDSprinklersMod",
- "Name": "Better Sprinklers",
- "UpperVersion": "2.1",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41",
- "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031",
- "ForceCompatibleVersion": "^2.1-EntoPatch"
- },
- {
- "ID": "SPDChestLabel",
- "Name": "Chest Label System",
- "UpperVersion": "1.5",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242",
- "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031",
- "ForceCompatibleVersion": "^1.5-EntoPatch"
- },
- {
- "ID": "CJBCheatsMenu",
- "Name": "CJB Cheats Menu",
- "UpperVersion": "1.12",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4",
- "ForceCompatibleVersion": "^1.12-EntoPatch"
- },
- {
- "ID": "CJBItemSpawner",
- "Name": "CJB Item Spawner",
- "UpperVersion": "1.5",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93",
- "ForceCompatibleVersion": "^1.5-EntoPatch"
- },
-
- /* versions which crash the game */
- {
- "ID": "NPCMapLocationsMod",
- "Name": "NPC Map Locations",
- "LowerVersion": "1.42",
- "UpperVersion": "1.43",
- "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239",
- "ReasonPhrase": "this version has an update check error which crashes the game"
- }
-]
diff --git a/src/StardewModdingAPI/Version.cs b/src/StardewModdingAPI/Version.cs
deleted file mode 100644
index e66d7be5..00000000
--- a/src/StardewModdingAPI/Version.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using System;
-using Newtonsoft.Json;
-using StardewModdingAPI.Framework;
-
-namespace StardewModdingAPI
-{
- /// <summary>A semantic version with an optional release tag.</summary>
- [Obsolete("Use " + nameof(SemanticVersion) + " or " + nameof(Manifest) + "." + nameof(Manifest.Version) + " instead")]
- public struct Version : ISemanticVersion
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The major version incremented for major API changes.</summary>
- public int MajorVersion { get; set; }
-
- /// <summary>The minor version incremented for backwards-compatible changes.</summary>
- public int MinorVersion { get; set; }
-
- /// <summary>The patch version for backwards-compatible bug fixes.</summary>
- public int PatchVersion { get; set; }
-
- /// <summary>An optional build tag.</summary>
- public string Build { get; set; }
-
- /// <summary>Obsolete.</summary>
- [JsonIgnore]
- [Obsolete("Use " + nameof(Version) + "." + nameof(Version.ToString) + " instead.")]
- public string VersionString
- {
- get
- {
- Program.DeprecationManager.Warn($"{nameof(Version)}.{nameof(Version.VersionString)}", "1.0", DeprecationLevel.Info);
- return this.GetSemanticVersion().ToString();
- }
- }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="major">The major version incremented for major API changes.</param>
- /// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
- /// <param name="patch">The patch version for backwards-compatible bug fixes.</param>
- /// <param name="build">An optional build tag.</param>
- public Version(int major, int minor, int patch, string build)
- : this(major, minor, patch, build, suppressDeprecationWarning: false)
- { }
-
- /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- public int CompareTo(Version other)
- {
- return this.GetSemanticVersion().CompareTo(other);
- }
-
- /// <summary>Get whether this version is newer than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- [Obsolete("Use " + nameof(ISemanticVersion) + "." + nameof(ISemanticVersion.IsNewerThan) + " instead")]
- public bool IsNewerThan(Version other)
- {
- return this.GetSemanticVersion().IsNewerThan(other);
- }
-
- /// <summary>Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. </summary>
- /// <returns>A value that indicates the relative order of the objects being compared. The return value has these meanings: Value Meaning Less than zero This instance precedes <paramref name="other" /> in the sort order. Zero This instance occurs in the same position in the sort order as <paramref name="other" />. Greater than zero This instance follows <paramref name="other" /> in the sort order. </returns>
- /// <param name="other">An object to compare with this instance. </param>
- int IComparable<ISemanticVersion>.CompareTo(ISemanticVersion other)
- {
- return this.GetSemanticVersion().CompareTo(other);
- }
-
- /// <summary>Get whether this version is older than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- bool ISemanticVersion.IsOlderThan(ISemanticVersion other)
- {
- return this.GetSemanticVersion().IsOlderThan(other);
- }
-
- /// <summary>Get whether this version is newer than the specified version.</summary>
- /// <param name="other">The version to compare with this instance.</param>
- bool ISemanticVersion.IsNewerThan(ISemanticVersion other)
- {
- return this.GetSemanticVersion().IsNewerThan(other);
- }
-
- /// <summary>Get a string representation of the version.</summary>
- public override string ToString()
- {
- return this.GetSemanticVersion().ToString();
- }
-
- /*********
- ** Private methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="major">The major version incremented for major API changes.</param>
- /// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
- /// <param name="patch">The patch version for backwards-compatible bug fixes.</param>
- /// <param name="build">An optional build tag.</param>
- /// <param name="suppressDeprecationWarning">Whether to suppress the deprecation warning.</param>
- internal Version(int major, int minor, int patch, string build, bool suppressDeprecationWarning)
- {
- if (!suppressDeprecationWarning)
- Program.DeprecationManager.Warn($"{nameof(Version)}", "1.5", DeprecationLevel.Notice);
-
- this.MajorVersion = major;
- this.MinorVersion = minor;
- this.PatchVersion = patch;
- this.Build = build;
- }
-
- /// <summary>Get the equivalent semantic version.</summary>
- /// <remarks>This is a hack so the struct can wrap <see cref="SemanticVersion"/> without a mutable backing field, which would cause a <see cref="StackOverflowException"/> due to recreating the struct value on each change.</remarks>
- private SemanticVersion GetSemanticVersion()
- {
- return new SemanticVersion(this.MajorVersion, this.MinorVersion, this.PatchVersion, this.Build);
- }
- }
-}
diff --git a/src/TrainerMod/Framework/Extensions.cs b/src/TrainerMod/Framework/Extensions.cs
deleted file mode 100644
index da3a2f03..00000000
--- a/src/TrainerMod/Framework/Extensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace TrainerMod.Framework
-{
- /// <summary>Provides extension methods on primitive types.</summary>
- internal static class Extensions
- {
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether an object is a number.</summary>
- /// <param name="value">The object value.</param>
- public static bool IsInt(this object value)
- {
- int i;
- return int.TryParse(value.ToString(), out i);
- }
-
- /// <summary>Parse an object into a number.</summary>
- /// <param name="value">The object value.</param>
- /// <exception cref="System.FormatException">The value is not a valid number.</exception>
- public static int ToInt(this object value)
- {
- return int.Parse(value.ToString());
- }
- }
-}
diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs
index c76bb78c..7187a358 100644
--- a/src/TrainerMod/TrainerMod.cs
+++ b/src/TrainerMod/TrainerMod.cs
@@ -6,10 +6,10 @@ using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
+using StardewValley.Locations;
using StardewValley.Menus;
using StardewValley.Objects;
using StardewValley.Tools;
-using TrainerMod.Framework;
using TrainerMod.ItemData;
using Object = StardewValley.Object;
@@ -44,7 +44,7 @@ namespace TrainerMod
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
- this.RegisterCommands();
+ this.RegisterCommands(helper);
GameEvents.UpdateTick += this.ReceiveUpdateTick;
}
@@ -73,646 +73,615 @@ namespace TrainerMod
Game1.timeOfDay = this.FrozenTime;
}
- /// <summary>Register all trainer commands.</summary>
- private void RegisterCommands()
- {
- Command.RegisterCommand("types", "Lists all value types | types").CommandFired += this.HandleTypes;
-
- Command.RegisterCommand("save", "Saves the game? Doesn't seem to work. | save").CommandFired += this.HandleSave;
- Command.RegisterCommand("load", "Shows the load screen | load").CommandFired += this.HandleLoad;
-
- Command.RegisterCommand("exit", "Closes the game | exit").CommandFired += this.HandleExit;
- Command.RegisterCommand("stop", "Closes the game | stop").CommandFired += this.HandleExit;
-
- Command.RegisterCommand("player_setname", "Sets the player's name | player_setname <object> <value>", new[] { "(player, pet, farm)<object> (String)<value> The target name" }).CommandFired += this.HandlePlayerSetName;
- Command.RegisterCommand("player_setmoney", "Sets the player's money | player_setmoney <value>|inf", new[] { "(Int32)<value> The target money" }).CommandFired += this.HandlePlayerSetMoney;
- Command.RegisterCommand("player_setstamina", "Sets the player's stamina | player_setstamina <value>|inf", new[] { "(Int32)<value> The target stamina" }).CommandFired += this.HandlePlayerSetStamina;
- Command.RegisterCommand("player_setmaxstamina", "Sets the player's max stamina | player_setmaxstamina <value>", new[] { "(Int32)<value> The target max stamina" }).CommandFired += this.HandlePlayerSetMaxStamina;
- Command.RegisterCommand("player_sethealth", "Sets the player's health | player_sethealth <value>|inf", new[] { "(Int32)<value> The target health" }).CommandFired += this.HandlePlayerSetHealth;
- Command.RegisterCommand("player_setmaxhealth", "Sets the player's max health | player_setmaxhealth <value>", new[] { "(Int32)<value> The target max health" }).CommandFired += this.HandlePlayerSetMaxHealth;
- Command.RegisterCommand("player_setimmunity", "Sets the player's immunity | player_setimmunity <value>", new[] { "(Int32)<value> The target immunity" }).CommandFired += this.HandlePlayerSetImmunity;
-
- Command.RegisterCommand("player_setlevel", "Sets the player's specified skill to the specified value | player_setlevel <skill> <value>", new[] { "(luck, mining, combat, farming, fishing, foraging)<skill> (1-10)<value> The target level" }).CommandFired += this.HandlePlayerSetLevel;
- Command.RegisterCommand("player_setspeed", "Sets the player's speed to the specified value?", new[] { "(Int32)<value> The target speed [0 is normal]" }).CommandFired += this.HandlePlayerSetSpeed;
- Command.RegisterCommand("player_changecolour", "Sets the player's colour of the specified object | player_changecolor <object> <colour>", new[] { "(hair, eyes, pants)<object> (r,g,b)<colour>" }).CommandFired += this.HandlePlayerChangeColor;
- Command.RegisterCommand("player_changestyle", "Sets the player's style of the specified object | player_changecolor <object> <value>", new[] { "(hair, shirt, skin, acc, shoe, swim, gender)<object> (Int32)<value>" }).CommandFired += this.HandlePlayerChangeStyle;
-
- Command.RegisterCommand("player_additem", "Gives the player an item | player_additem <item> [count] [quality]", new[] { "(Int32)<id> (Int32)[count] (Int32)[quality]" }).CommandFired += this.HandlePlayerAddItem;
- Command.RegisterCommand("player_addmelee", "Gives the player a melee item | player_addmelee <item>", new[] { "?<item>" }).CommandFired += this.HandlePlayerAddMelee;
- Command.RegisterCommand("player_addring", "Gives the player a ring | player_addring <item>", new[] { "?<item>" }).CommandFired += this.HandlePlayerAddRing;
-
- Command.RegisterCommand("list_items", "Lists items in the game data | list_items [search]", new[] { "(String)<search>" }).CommandFired += this.HandleListItems;
-
- Command.RegisterCommand("world_settime", "Sets the time to the specified value | world_settime <value>", new[] { "(Int32)<value> The target time [06:00 AM is 600]" }).CommandFired += this.HandleWorldSetTime;
- Command.RegisterCommand("world_freezetime", "Freezes or thaws time | world_freezetime <value>", new[] { "(0 - 1)<value> Whether or not to freeze time. 0 is thawed, 1 is frozen" }).CommandFired += this.HandleWorldFreezeTime;
- Command.RegisterCommand("world_setday", "Sets the day to the specified value | world_setday <value>", new[] { "(Int32)<value> The target day [1-28]" }).CommandFired += this.world_setDay;
- Command.RegisterCommand("world_setseason", "Sets the season to the specified value | world_setseason <value>", new[] { "(winter, spring, summer, fall)<value> The target season" }).CommandFired += this.HandleWorldSetSeason;
- Command.RegisterCommand("world_downminelevel", "Goes down one mine level? | world_downminelevel", new[] { "" }).CommandFired += this.HandleWorldDownMineLevel;
- Command.RegisterCommand("world_setminelevel", "Sets mine level? | world_setminelevel", new[] { "(Int32)<value> The target level" }).CommandFired += this.HandleWorldSetMineLevel;
-
- Command.RegisterCommand("show_game_files", "Opens the game folder. | show_game_files").CommandFired += this.HandleShowGameFiles;
- Command.RegisterCommand("show_data_files", "Opens the folder containing the save and log files. | show_data_files").CommandFired += this.HandleShowDataFiles;
- }
-
/****
- ** Command handlers
+ ** Command definitions
****/
- /// <summary>The event raised when the 'types' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleTypes(object sender, EventArgsCommand e)
- {
- this.Monitor.Log($"[Int32: {int.MinValue} - {int.MaxValue}], [Int64: {long.MinValue} - {long.MaxValue}], [String: \"raw text\"], [Colour: r,g,b (EG: 128, 32, 255)]", LogLevel.Info);
- }
-
- /// <summary>The event raised when the 'save' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleSave(object sender, EventArgsCommand e)
- {
- SaveGame.Save();
- }
-
- /// <summary>The event raised when the 'load' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleLoad(object sender, EventArgsCommand e)
- {
- Game1.hasLoadedGame = false;
- Game1.activeClickableMenu = new LoadGameMenu();
- }
-
- /// <summary>The event raised when the 'exit' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleExit(object sender, EventArgsCommand e)
- {
- Program.gamePtr.Exit();
- Environment.Exit(0);
- }
-
- /// <summary>The event raised when the 'player_setName' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetName(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 1)
+ /// <summary>Register all trainer commands.</summary>
+ /// <param name="helper">Provides simplified APIs for writing mods.</param>
+ private void RegisterCommands(IModHelper helper)
+ {
+ helper.ConsoleCommands
+ .Add("save", "Saves the game? Doesn't seem to work.", this.HandleCommand)
+ .Add("load", "Shows the load screen.", this.HandleCommand)
+ .Add("player_setname", "Sets the player's name.\n\nUsage: player_setname <target> <name>\n- target: what to rename (one of 'player' or 'farm').\n- name: the new name to set.", this.HandleCommand)
+ .Add("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount, or 'inf' for infinite money.", this.HandleCommand)
+ .Add("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", this.HandleCommand)
+ .Add("player_setmaxstamina", "Sets the player's max stamina.\n\nUsage: player_setmaxstamina [value]\n- value: an integer amount.", this.HandleCommand)
+ .Add("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", this.HandleCommand)
+ .Add("player_setmaxhealth", "Sets the player's max health.\n\nUsage: player_setmaxhealth [value]\n- value: an integer amount.", this.HandleCommand)
+ .Add("player_setimmunity", "Sets the player's immunity.\n\nUsage: player_setimmunity [value]\n- value: an integer amount.", this.HandleCommand)
+
+ .Add("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel <skill> <value>\n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).", this.HandleCommand)
+ .Add("player_setspeed", "Sets the player's speed to the specified value?\n\nUsage: player_setspeed <value>\n- value: an integer amount (0 is normal).", this.HandleCommand)
+ .Add("player_changecolor", "Sets the color of a player feature.\n\nUsage: player_changecolor <target> <color>\n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- color: a color value in RGB format, like (255,255,255).", this.HandleCommand)
+ .Add("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor <target> <value>.\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.", this.HandleCommand)
+
+ .Add("player_additem", $"Gives the player an item.\n\nUsage: player_additem <item> [count] [quality]\n- item: the item ID (use the 'list_items' command to see a list).\n- count (optional): how many of the item to give.\n- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).", this.HandleCommand)
+ .Add("player_addmelee", "Gives the player a melee weapon.\n\nUsage: player_addmelee <item>\n- item: the melee weapon ID (use the 'list_items' command to see a list).", this.HandleCommand)
+ .Add("player_addring", "Gives the player a ring.\n\nUsage: player_addring <item>\n- item: the ring ID (use the 'list_items' command to see a list).", this.HandleCommand)
+
+ .Add("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.", this.HandleCommand)
+
+ .Add("world_settime", "Sets the time to the specified value.\n\nUsage: world_settime <value>\n- value: the target time in military time (like 0600 for 6am and 1800 for 6pm)", this.HandleCommand)
+ .Add("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", this.HandleCommand)
+ .Add("world_setday", "Sets the day to the specified value.\n\nUsage: world_setday <value>.\n- value: the target day (a number from 1 to 28).", this.HandleCommand)
+ .Add("world_setseason", "Sets the season to the specified value.\n\nUsage: world_setseason <season>\n- value: the target season (one of 'spring', 'summer', 'fall', 'winter').", this.HandleCommand)
+ .Add("world_downminelevel", "Goes down one mine level?", this.HandleCommand)
+ .Add("world_setminelevel", "Sets the mine level?\n\nUsage: world_setminelevel <value>\n- value: The target level (a number between 1 and 120).", this.HandleCommand)
+
+ .Add("show_game_files", "Opens the game folder.", this.HandleCommand)
+ .Add("show_data_files", "Opens the folder containing the save and log files.", this.HandleCommand);
+ }
+
+ /// <summary>Handle a TrainerMod command.</summary>
+ /// <param name="command">The command name.</param>
+ /// <param name="args">The command arguments.</param>
+ private void HandleCommand(string command, string[] args)
+ {
+ switch (command)
{
- string target = e.Command.CalledArgs[0];
- string[] validTargets = { "player", "pet", "farm" };
- if (validTargets.Contains(target))
- {
- switch (target)
+ case "save":
+ this.Monitor.Log("Saving the game...", LogLevel.Info);
+ SaveGame.Save();
+ break;
+
+ case "load":
+ this.Monitor.Log("Triggering load menu...", LogLevel.Info);
+ Game1.hasLoadedGame = false;
+ Game1.activeClickableMenu = new LoadGameMenu();
+ break;
+
+ case "player_setname":
+ if (args.Length > 1)
{
- case "player":
- Game1.player.Name = e.Command.CalledArgs[1];
- break;
- case "pet":
- this.Monitor.Log("Pets cannot currently be renamed.", LogLevel.Info);
- break;
- case "farm":
- Game1.player.farmName = e.Command.CalledArgs[1];
- break;
+ string target = args[0];
+ string[] validTargets = { "player", "farm" };
+ if (validTargets.Contains(target))
+ {
+ switch (target)
+ {
+ case "player":
+ Game1.player.Name = args[1];
+ this.Monitor.Log($"OK, your player's name is now {Game1.player.Name}.", LogLevel.Info);
+ break;
+ case "farm":
+ Game1.player.farmName = args[1];
+ this.Monitor.Log($"OK, your farm's name is now {Game1.player.Name}.", LogLevel.Info);
+ break;
+ }
+ }
+ else
+ this.LogArgumentsInvalid(command);
}
- }
- else
- this.LogObjectInvalid();
- }
- else
- this.LogObjectValueNotSpecified();
- }
+ else
+ this.Monitor.Log($"Your name is currently '{Game1.player.Name}'. Type 'help player_setname' for usage.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'player_setMoney' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetMoney(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string amountStr = e.Command.CalledArgs[0];
- if (amountStr == "inf")
- this.InfiniteMoney = true;
- else
- {
- this.InfiniteMoney = false;
- int amount;
- if (int.TryParse(amountStr, out amount))
+ case "player_setmoney":
+ if (args.Any())
{
- Game1.player.Money = amount;
- this.Monitor.Log($"Set {Game1.player.Name}'s money to {Game1.player.Money}", LogLevel.Info);
+ string amountStr = args[0];
+ if (amountStr == "inf")
+ {
+ this.InfiniteMoney = true;
+ this.Monitor.Log("OK, you now have infinite money.", LogLevel.Info);
+ }
+ else
+ {
+ this.InfiniteMoney = false;
+ int amount;
+ if (int.TryParse(amountStr, out amount))
+ {
+ Game1.player.Money = amount;
+ this.Monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
}
else
- this.LogValueNotInt32();
- }
- }
- else
- this.LogValueNotSpecified();
- }
+ this.Monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'player_setStamina' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetStamina(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string amountStr = e.Command.CalledArgs[0];
- if (amountStr == "inf")
- this.InfiniteStamina = true;
- else
- {
- this.InfiniteStamina = false;
- int amount;
- if (int.TryParse(amountStr, out amount))
+ case "player_setstamina":
+ if (args.Any())
{
- Game1.player.Stamina = amount;
- this.Monitor.Log($"Set {Game1.player.Name}'s stamina to {Game1.player.Stamina}", LogLevel.Info);
+ string amountStr = args[0];
+ if (amountStr == "inf")
+ {
+ this.InfiniteStamina = true;
+ this.Monitor.Log("OK, you now have infinite stamina.", LogLevel.Info);
+ }
+ else
+ {
+ this.InfiniteStamina = false;
+ int amount;
+ if (int.TryParse(amountStr, out amount))
+ {
+ Game1.player.Stamina = amount;
+ this.Monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
}
else
- this.LogValueNotInt32();
- }
- }
- else
- this.LogValueNotSpecified();
- }
+ this.Monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'player_setMaxStamina' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetMaxStamina(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- int amount;
- if (int.TryParse(e.Command.CalledArgs[0], out amount))
- {
- Game1.player.MaxStamina = amount;
- this.Monitor.Log($"Set {Game1.player.Name}'s max stamina to {Game1.player.MaxStamina}", LogLevel.Info);
- }
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ case "player_setmaxstamina":
+ if (args.Any())
+ {
+ int amount;
+ if (int.TryParse(args[0], out amount))
+ {
+ Game1.player.MaxStamina = amount;
+ this.Monitor.Log($"OK, you now have {Game1.player.MaxStamina} max stamina.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ else
+ this.Monitor.Log($"You currently have {Game1.player.MaxStamina} max stamina. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'player_setLevel' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetLevel(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 1)
- {
- string skill = e.Command.CalledArgs[0];
- string[] skills = { "luck", "mining", "combat", "farming", "fishing", "foraging" };
- if (skills.Contains(skill))
- {
- int level;
- if (int.TryParse(e.Command.CalledArgs[1], out level))
+ case "player_setlevel":
+ if (args.Length > 1)
{
- switch (skill)
+ string skill = args[0];
+ string[] skills = { "luck", "mining", "combat", "farming", "fishing", "foraging" };
+ if (skills.Contains(skill))
{
- case "luck":
- Game1.player.LuckLevel = level;
- break;
- case "mining":
- Game1.player.MiningLevel = level;
- break;
- case "combat":
- Game1.player.CombatLevel = level;
- break;
- case "farming":
- Game1.player.FarmingLevel = level;
- break;
- case "fishing":
- Game1.player.FishingLevel = level;
- break;
- case "foraging":
- Game1.player.ForagingLevel = level;
- break;
+ int level;
+ if (int.TryParse(args[1], out level))
+ {
+ switch (skill)
+ {
+ case "luck":
+ Game1.player.LuckLevel = level;
+ this.Monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info);
+ break;
+ case "mining":
+ Game1.player.MiningLevel = level;
+ this.Monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info);
+ break;
+ case "combat":
+ Game1.player.CombatLevel = level;
+ this.Monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info);
+ break;
+ case "farming":
+ Game1.player.FarmingLevel = level;
+ this.Monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info);
+ break;
+ case "fishing":
+ Game1.player.FishingLevel = level;
+ this.Monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info);
+ break;
+ case "foraging":
+ Game1.player.ForagingLevel = level;
+ this.Monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info);
+ break;
+ }
+ }
+ else
+ this.LogArgumentNotInt(command);
}
+ else
+ this.LogUsageError("That isn't a valid skill.", command);
}
else
- this.LogValueNotInt32();
- }
- else
- this.Monitor.Log("<skill> is invalid", LogLevel.Error);
- }
- else
- this.Monitor.Log("<skill> and <value> must be specified", LogLevel.Error);
- }
+ this.LogArgumentsInvalid(command);
+ break;
- /// <summary>The event raised when the 'player_setSpeed' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetSpeed(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string amountStr = e.Command.CalledArgs[0];
- if (amountStr.IsInt())
- {
- Game1.player.addedSpeed = amountStr.ToInt();
- this.Monitor.Log($"Set {Game1.player.Name}'s added speed to {Game1.player.addedSpeed}", LogLevel.Info);
- }
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ case "player_setspeed":
+ if (args.Any())
+ {
+ int addedSpeed;
+ if (int.TryParse(args[0], out addedSpeed))
+ {
+ Game1.player.addedSpeed = addedSpeed;
+ this.Monitor.Log($"OK, your added speed is now {Game1.player.addedSpeed}.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ else
+ this.Monitor.Log($"You currently have {Game1.player.addedSpeed} added speed. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'player_changeColour' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerChangeColor(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 1)
- {
- string target = e.Command.CalledArgs[0];
- string[] validTargets = { "hair", "eyes", "pants" };
- if (validTargets.Contains(target))
- {
- string[] colorHexes = e.Command.CalledArgs[1].Split(new[] { ',' }, 3);
- if (colorHexes[0].IsInt() && colorHexes[1].IsInt() && colorHexes[2].IsInt())
+ case "player_changecolor":
+ if (args.Length > 1)
{
- var color = new Color(colorHexes[0].ToInt(), colorHexes[1].ToInt(), colorHexes[2].ToInt());
- switch (target)
+ string target = args[0];
+ string[] validTargets = { "hair", "eyes", "pants" };
+ if (validTargets.Contains(target))
{
- case "hair":
- Game1.player.hairstyleColor = color;
- break;
- case "eyes":
- Game1.player.changeEyeColor(color);
- break;
- case "pants":
- Game1.player.pantsColor = color;
- break;
+ string[] colorHexes = args[1].Split(new[] { ',' }, 3);
+ int r, g, b;
+ if (int.TryParse(colorHexes[0], out r) && int.TryParse(colorHexes[1], out g) && int.TryParse(colorHexes[2], out b))
+ {
+ Color color = new Color(r, g, b);
+ switch (target)
+ {
+ case "hair":
+ Game1.player.hairstyleColor = color;
+ this.Monitor.Log("OK, your hair color is updated.", LogLevel.Info);
+ break;
+ case "eyes":
+ Game1.player.changeEyeColor(color);
+ this.Monitor.Log("OK, your eye color is updated.", LogLevel.Info);
+ break;
+ case "pants":
+ Game1.player.pantsColor = color;
+ this.Monitor.Log("OK, your pants color is updated.", LogLevel.Info);
+ break;
+ }
+ }
+ else
+ this.LogUsageError("The color should be an RBG value like '255,150,0'.", command);
}
+ else
+ this.LogArgumentsInvalid(command);
}
else
- this.Monitor.Log("<colour> is invalid", LogLevel.Error);
- }
- else
- this.LogObjectInvalid();
- }
- else
- this.Monitor.Log("<object> and <colour> must be specified", LogLevel.Error);
- }
+ this.LogArgumentsInvalid(command);
+ break;
- /// <summary>The event raised when the 'player_changeStyle' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerChangeStyle(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 1)
- {
- string target = e.Command.CalledArgs[0];
- string[] validTargets = { "hair", "shirt", "skin", "acc", "shoe", "swim", "gender" };
- if (validTargets.Contains(target))
- {
- if (e.Command.CalledArgs[1].IsInt())
+ case "player_changestyle":
+ if (args.Length > 1)
{
- var styleID = e.Command.CalledArgs[1].ToInt();
- switch (target)
+ string target = args[0];
+ string[] validTargets = { "hair", "shirt", "skin", "acc", "shoe", "swim", "gender" };
+ if (validTargets.Contains(target))
{
- case "hair":
- Game1.player.changeHairStyle(styleID);
- break;
- case "shirt":
- Game1.player.changeShirt(styleID);
- break;
- case "acc":
- Game1.player.changeAccessory(styleID);
- break;
- case "skin":
- Game1.player.changeSkinColor(styleID);
- break;
- case "shoe":
- Game1.player.changeShoeColor(styleID);
- break;
- case "swim":
- if (styleID == 0)
- Game1.player.changeOutOfSwimSuit();
- else if (styleID == 1)
- Game1.player.changeIntoSwimsuit();
- else
- this.Monitor.Log("<value> must be 0 or 1 for this <object>", LogLevel.Error);
- break;
- case "gender":
- if (styleID == 0)
- Game1.player.changeGender(true);
- else if (styleID == 1)
- Game1.player.changeGender(false);
- else
- this.Monitor.Log("<value> must be 0 or 1 for this <object>", LogLevel.Error);
- break;
+ int styleID;
+ if (int.TryParse(args[1], out styleID))
+ {
+ switch (target)
+ {
+ case "hair":
+ Game1.player.changeHairStyle(styleID);
+ this.Monitor.Log("OK, your hair style is updated.", LogLevel.Info);
+ break;
+ case "shirt":
+ Game1.player.changeShirt(styleID);
+ this.Monitor.Log("OK, your shirt style is updated.", LogLevel.Info);
+ break;
+ case "acc":
+ Game1.player.changeAccessory(styleID);
+ this.Monitor.Log("OK, your accessory style is updated.", LogLevel.Info);
+ break;
+ case "skin":
+ Game1.player.changeSkinColor(styleID);
+ this.Monitor.Log("OK, your skin color is updated.", LogLevel.Info);
+ break;
+ case "shoe":
+ Game1.player.changeShoeColor(styleID);
+ this.Monitor.Log("OK, your shoe style is updated.", LogLevel.Info);
+ break;
+ case "swim":
+ switch (styleID)
+ {
+ case 0:
+ Game1.player.changeOutOfSwimSuit();
+ this.Monitor.Log("OK, you're no longer in your swimming suit.", LogLevel.Info);
+ break;
+ case 1:
+ Game1.player.changeIntoSwimsuit();
+ this.Monitor.Log("OK, you're now in your swimming suit.", LogLevel.Info);
+ break;
+ default:
+ this.LogUsageError("The swim value should be 0 (no swimming suit) or 1 (swimming suit).", command);
+ break;
+ }
+ break;
+ case "gender":
+ switch (styleID)
+ {
+ case 0:
+ Game1.player.changeGender(true);
+ this.Monitor.Log("OK, you're now male.", LogLevel.Info);
+ break;
+ case 1:
+ Game1.player.changeGender(false);
+ this.Monitor.Log("OK, you're now female.", LogLevel.Info);
+ break;
+ default:
+ this.LogUsageError("The gender value should be 0 (male) or 1 (female).", command);
+ break;
+ }
+ break;
+ }
+ }
+ else
+ this.LogArgumentsInvalid(command);
}
+ else
+ this.LogArgumentsInvalid(command);
}
else
- this.LogValueInvalid();
- }
- else
- this.LogObjectInvalid();
- }
- else
- this.LogObjectValueNotSpecified();
- }
+ this.LogArgumentsInvalid(command);
+ break;
- /// <summary>The event raised when the 'world_freezeTime' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleWorldFreezeTime(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string valueStr = e.Command.CalledArgs[0];
- if (valueStr.IsInt())
- {
- int value = valueStr.ToInt();
- if (value == 0 || value == 1)
+ case "world_freezetime":
+ if (args.Any())
+ {
+ int value;
+ if (int.TryParse(args[0], out value))
+ {
+ if (value == 0 || value == 1)
+ {
+ this.FreezeTime = value == 1;
+ this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0;
+ this.Monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info);
+ }
+ else
+ this.LogUsageError("The value should be 0 (not frozen), 1 (frozen), or empty (toggle).", command);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ else
{
- this.FreezeTime = value == 1;
+ this.FreezeTime = !this.FreezeTime;
this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0;
- this.Monitor.Log("Time is now " + (this.FreezeTime ? "frozen" : "thawed"), LogLevel.Info);
+ this.Monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info);
+ }
+ break;
+
+ case "world_settime":
+ if (args.Any())
+ {
+ int time;
+ if (int.TryParse(args[0], out time))
+ {
+ if (time <= 2600 && time >= 600)
+ {
+ Game1.timeOfDay = time;
+ this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0;
+ this.Monitor.Log($"OK, the time is now {Game1.timeOfDay.ToString().PadLeft(4, '0')}.", LogLevel.Info);
+ }
+ else
+ this.LogUsageError("That isn't a valid time.", command);
+ }
+ else
+ this.LogArgumentNotInt(command);
}
else
- this.Monitor.Log("<value> should be 0 or 1", LogLevel.Error);
- }
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ this.Monitor.Log($"The current time is {Game1.timeOfDay}. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'world_setTime' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleWorldSetTime(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string timeStr = e.Command.CalledArgs[0];
- if (timeStr.IsInt())
- {
- int time = timeStr.ToInt();
+ case "world_setday":
+ if (args.Any())
+ {
+ int day;
+ if (int.TryParse(args[0], out day))
+ {
+ if (day <= 28 && day > 0)
+ {
+ Game1.dayOfMonth = day;
+ this.Monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info);
+ }
+ else
+ this.LogUsageError("That isn't a valid day.", command);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ else
+ this.Monitor.Log($"The current date is {Game1.currentSeason} {Game1.dayOfMonth}. Specify a value to change the day.", LogLevel.Info);
+ break;
- if (time <= 2600 && time >= 600)
+ case "world_setseason":
+ if (args.Any())
{
- Game1.timeOfDay = e.Command.CalledArgs[0].ToInt();
- this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0;
- this.Monitor.Log($"Time set to: {Game1.timeOfDay}", LogLevel.Info);
+ string season = args[0];
+ string[] validSeasons = { "winter", "spring", "summer", "fall" };
+ if (validSeasons.Contains(season))
+ {
+ Game1.currentSeason = season;
+ this.Monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info);
+ }
+ else
+ this.LogUsageError("That isn't a valid season name.", command);
}
else
- this.Monitor.Log("<value> should be between 600 and 2600 (06:00 AM - 02:00 AM [NEXT DAY])", LogLevel.Error);
- }
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ this.Monitor.Log($"The current season is {Game1.currentSeason}. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'world_setDay' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void world_setDay(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string dayStr = e.Command.CalledArgs[0];
+ case "player_sethealth":
+ if (args.Any())
+ {
+ string amountStr = args[0];
- if (dayStr.IsInt())
- {
- int day = dayStr.ToInt();
- if (day <= 28 && day > 0)
- Game1.dayOfMonth = day;
+ if (amountStr == "inf")
+ {
+ this.InfiniteHealth = true;
+ this.Monitor.Log("OK, you now have infinite health.", LogLevel.Info);
+ }
+ else
+ {
+ this.InfiniteHealth = false;
+ int amount;
+ if (int.TryParse(amountStr, out amount))
+ {
+ Game1.player.health = amount;
+ this.Monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ }
else
- this.Monitor.Log("<value> must be between 1 and 28", LogLevel.Error);
- }
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ this.Monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'world_setSeason' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleWorldSetSeason(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string season = e.Command.CalledArgs[0];
- string[] validSeasons = { "winter", "spring", "summer", "fall" };
- if (validSeasons.Contains(season))
- Game1.currentSeason = season;
- else
- this.LogValueInvalid();
- }
- else
- this.LogValueNotSpecified();
- }
-
- /// <summary>The event raised when the 'player_setHealth' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetHealth(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string amountStr = e.Command.CalledArgs[0];
+ case "player_setmaxhealth":
+ if (args.Any())
+ {
+ int maxHealth;
+ if (int.TryParse(args[0], out maxHealth))
+ {
+ Game1.player.maxHealth = maxHealth;
+ this.Monitor.Log($"OK, you now have {Game1.player.maxHealth} max health.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ else
+ this.Monitor.Log($"You currently have {Game1.player.maxHealth} max health. Specify a value to change it.", LogLevel.Info);
+ break;
- if (amountStr == "inf")
- this.InfiniteHealth = true;
- else
- {
- this.InfiniteHealth = false;
- if (amountStr.IsInt())
- Game1.player.health = amountStr.ToInt();
+ case "player_setimmunity":
+ if (args.Any())
+ {
+ int amount;
+ if (int.TryParse(args[0], out amount))
+ {
+ Game1.player.immunity = amount;
+ this.Monitor.Log($"OK, you now have {Game1.player.immunity} immunity.", LogLevel.Info);
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
else
- this.LogValueNotInt32();
- }
- }
- else
- this.LogValueNotSpecified();
- }
+ this.Monitor.Log($"You currently have {Game1.player.immunity} immunity. Specify a value to change it.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'player_setMaxHealth' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetMaxHealth(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string amountStr = e.Command.CalledArgs[0];
- if (amountStr.IsInt())
- Game1.player.maxHealth = amountStr.ToInt();
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ case "player_additem":
+ if (args.Any())
+ {
+ int itemID;
+ if (int.TryParse(args[0], out itemID))
+ {
+ int count = 1;
+ int quality = 0;
+ if (args.Length > 1)
+ {
+ if (!int.TryParse(args[1], out count))
+ {
+ this.LogUsageError("The optional count is invalid.", command);
+ return;
+ }
+
+ if (args.Length > 2 && !int.TryParse(args[2], out quality))
+ {
+ this.LogUsageError("The optional quality is invalid.", command);
+ return;
+ }
+ }
- /// <summary>The event raised when the 'player_setImmunity' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerSetImmunity(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string amountStr = e.Command.CalledArgs[0];
- if (amountStr.IsInt())
- Game1.player.immunity = amountStr.ToInt();
- else
- this.LogValueNotInt32();
- }
- else
- this.LogValueNotSpecified();
- }
+ var item = new Object(itemID, count) { quality = quality };
+ if (item.Name == "Error Item")
+ this.Monitor.Log("There is no such item ID.", LogLevel.Error);
+ else
+ {
+ Game1.player.addItemByMenuIfNecessary(item);
+ this.Monitor.Log($"OK, added {item.Name} to your inventory.", LogLevel.Info);
+ }
+ }
+ else
+ this.LogUsageError("The item ID must be an integer.", command);
+ }
+ else
+ this.LogArgumentsInvalid(command);
+ break;
- /// <summary>The event raised when the 'player_addItem' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerAddItem(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- string itemIdStr = e.Command.CalledArgs[0];
- if (itemIdStr.IsInt())
- {
- int itemID = itemIdStr.ToInt();
- int count = 1;
- int quality = 0;
- if (e.Command.CalledArgs.Length > 1)
+ case "player_addmelee":
+ if (args.Any())
{
- if (e.Command.CalledArgs[1].IsInt())
- count = e.Command.CalledArgs[1].ToInt();
- else
+ int weaponID;
+ if (int.TryParse(args[0], out weaponID))
{
- this.Monitor.Log("[count] is invalid", LogLevel.Error);
- return;
+ MeleeWeapon weapon = new MeleeWeapon(weaponID);
+ if (weapon.Name == null)
+ this.Monitor.Log("There is no such weapon ID.", LogLevel.Error);
+ else
+ {
+ Game1.player.addItemByMenuIfNecessary(weapon);
+ this.Monitor.Log($"OK, added {weapon.Name} to your inventory.", LogLevel.Info);
+ }
}
+ else
+ this.LogUsageError("The weapon ID must be an integer.", command);
+ }
+ else
+ this.LogArgumentsInvalid(command);
+ break;
- if (e.Command.CalledArgs.Length > 2)
+ case "player_addring":
+ if (args.Any())
+ {
+ int ringID;
+ if (int.TryParse(args[0], out ringID))
{
- if (e.Command.CalledArgs[2].IsInt())
- quality = e.Command.CalledArgs[2].ToInt();
+ if (ringID < Ring.ringLowerIndexRange || ringID > Ring.ringUpperIndexRange)
+ this.Monitor.Log($"There is no such ring ID (must be between {Ring.ringLowerIndexRange} and {Ring.ringUpperIndexRange}).", LogLevel.Error);
else
{
- this.Monitor.Log("[quality] is invalid", LogLevel.Error);
- return;
+ Ring ring = new Ring(ringID);
+ Game1.player.addItemByMenuIfNecessary(ring);
+ this.Monitor.Log($"OK, added {ring.Name} to your inventory.", LogLevel.Info);
}
}
+ else
+ this.Monitor.Log("<item> is invalid", LogLevel.Error);
}
+ else
+ this.LogArgumentsInvalid(command);
+ break;
- var item = new Object(itemID, count) { quality = quality };
+ case "list_items":
+ {
+ var matches = this.GetItems(args).ToArray();
- Game1.player.addItemByMenuIfNecessary(item);
- }
- else
- this.Monitor.Log("<item> is invalid", LogLevel.Error);
- }
- else
- this.LogObjectValueNotSpecified();
- }
+ // show matches
+ string summary = "Searching...\n";
+ if (matches.Any())
+ this.Monitor.Log(summary + this.GetTableString(matches, new[] { "type", "id", "name" }, val => new[] { val.Type.ToString(), val.ID.ToString(), val.Name }), LogLevel.Info);
+ else
+ this.Monitor.Log(summary + "No items found", LogLevel.Info);
+ }
+ break;
- /// <summary>The event raised when the 'player_addMelee' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerAddMelee(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- if (e.Command.CalledArgs[0].IsInt())
- {
- MeleeWeapon weapon = new MeleeWeapon(e.Command.CalledArgs[0].ToInt());
- Game1.player.addItemByMenuIfNecessary(weapon);
- this.Monitor.Log($"Gave {weapon.Name} to {Game1.player.Name}", LogLevel.Info);
- }
- else
- this.Monitor.Log("<item> is invalid", LogLevel.Error);
- }
- else
- this.LogObjectValueNotSpecified();
- }
+ case "world_downminelevel":
+ {
+ int level = (Game1.currentLocation as MineShaft)?.mineLevel ?? 0;
+ this.Monitor.Log($"OK, warping you to mine level {level + 1}.", LogLevel.Info);
+ Game1.enterMine(false, level + 1, "");
+ break;
+ }
- /// <summary>The event raised when the 'player_addRing' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandlePlayerAddRing(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- if (e.Command.CalledArgs[0].IsInt())
- {
- Ring ring = new Ring(e.Command.CalledArgs[0].ToInt());
- Game1.player.addItemByMenuIfNecessary(ring);
- this.Monitor.Log($"Gave {ring.Name} to {Game1.player.Name}", LogLevel.Info);
- }
- else
- this.Monitor.Log("<item> is invalid", LogLevel.Error);
- }
- else
- this.LogObjectValueNotSpecified();
- }
+ case "world_setminelevel":
+ if (args.Any())
+ {
+ int level;
+ if (int.TryParse(args[0], out level))
+ {
+ level = Math.Max(1, level);
+ this.Monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info);
+ Game1.enterMine(true, level, "");
+ }
+ else
+ this.LogArgumentNotInt(command);
+ }
+ else
+ this.LogArgumentsInvalid(command);
+ break;
- /// <summary>The event raised when the 'list_items' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleListItems(object sender, EventArgsCommand e)
- {
- var matches = this.GetItems(e.Command.CalledArgs).ToArray();
-
- // show matches
- string summary = "Searching...\n";
- if (matches.Any())
- this.Monitor.Log(summary + this.GetTableString(matches, new[] { "type", "id", "name" }, val => new[] { val.Type.ToString(), val.ID.ToString(), val.Name }), LogLevel.Info);
- else
- this.Monitor.Log(summary + "No items found", LogLevel.Info);
- }
+ case "show_game_files":
+ Process.Start(Constants.ExecutionPath);
+ this.Monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'world_downMineLevel' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleWorldDownMineLevel(object sender, EventArgsCommand e)
- {
- Game1.nextMineLevel();
- }
+ case "show_data_files":
+ Process.Start(Constants.DataPath);
+ this.Monitor.Log($"OK, opening {Constants.DataPath}.", LogLevel.Info);
+ break;
- /// <summary>The event raised when the 'world_setMineLevel' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleWorldSetMineLevel(object sender, EventArgsCommand e)
- {
- if (e.Command.CalledArgs.Length > 0)
- {
- if (e.Command.CalledArgs[0].IsInt())
- Game1.enterMine(true, e.Command.CalledArgs[0].ToInt(), "");
- else
- this.LogValueNotInt32();
+ default:
+ throw new NotImplementedException($"TrainerMod received unknown command '{command}'.");
}
- else
- this.LogValueNotSpecified();
- }
-
- /// <summary>The event raised when the 'show_game_files' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleShowGameFiles(object sender, EventArgsCommand e)
- {
- Process.Start(Constants.ExecutionPath);
- }
-
- /// <summary>The event raised when the 'show_data_files' command is triggered.</summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event arguments.</param>
- private void HandleShowDataFiles(object sender, EventArgsCommand e)
- {
- Process.Start(Constants.DataPath);
}
/****
@@ -800,34 +769,26 @@ namespace TrainerMod
/****
** Logging
****/
- /// <summary>Log an error indicating a value must be specified.</summary>
- public void LogValueNotSpecified()
+ /// <summary>Log an error indicating incorrect usage.</summary>
+ /// <param name="error">A sentence explaining the problem.</param>
+ /// <param name="command">The name of the command.</param>
+ private void LogUsageError(string error, string command)
{
- this.Monitor.Log("<value> must be specified", LogLevel.Error);
+ this.Monitor.Log($"{error} Type 'help {command}' for usage.", LogLevel.Error);
}
- /// <summary>Log an error indicating a target and value must be specified.</summary>
- public void LogObjectValueNotSpecified()
+ /// <summary>Log an error indicating a value must be an integer.</summary>
+ /// <param name="command">The name of the command.</param>
+ private void LogArgumentNotInt(string command)
{
- this.Monitor.Log("<object> and <value> must be specified", LogLevel.Error);
+ this.LogUsageError("The value must be a whole number.", command);
}
/// <summary>Log an error indicating a value is invalid.</summary>
- public void LogValueInvalid()
- {
- this.Monitor.Log("<value> is invalid", LogLevel.Error);
- }
-
- /// <summary>Log an error indicating a target is invalid.</summary>
- public void LogObjectInvalid()
- {
- this.Monitor.Log("<object> is invalid", LogLevel.Error);
- }
-
- /// <summary>Log an error indicating a value must be an integer.</summary>
- public void LogValueNotInt32()
+ /// <param name="command">The name of the command.</param>
+ private void LogArgumentsInvalid(string command)
{
- this.Monitor.Log("<value> must be a whole number (Int32)", LogLevel.Error);
+ this.LogUsageError("The arguments are invalid.", command);
}
}
}
diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj
index 1457ac2b..0bd667d4 100644
--- a/src/TrainerMod/TrainerMod.csproj
+++ b/src/TrainerMod/TrainerMod.csproj
@@ -22,7 +22,6 @@
<WarningLevel>4</WarningLevel>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
- <LangVersion>6</LangVersion>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
@@ -33,7 +32,6 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
- <LangVersion>6</LangVersion>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
@@ -53,7 +51,6 @@
<Compile Include="..\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
- <Compile Include="Framework\Extensions.cs" />
<Compile Include="ItemData\ISearchItem.cs" />
<Compile Include="ItemData\ItemType.cs" />
<Compile Include="ItemData\SearchableObject.cs" />
diff --git a/src/TrainerMod/manifest.json b/src/TrainerMod/manifest.json
index 72a3f40a..8bddf02d 100644
--- a/src/TrainerMod/manifest.json
+++ b/src/TrainerMod/manifest.json
@@ -1,13 +1,13 @@
{
"Name": "Trainer Mod",
- "Author": "Zoryn",
+ "Author": "SMAPI",
"Version": {
"MajorVersion": 1,
- "MinorVersion": 0,
+ "MinorVersion": 9,
"PatchVersion": 0,
- "Build": ""
+ "Build": null
},
- "Description": "Registers several commands to use. Most commands are trainer-like in that they offer forms of cheating.",
- "UniqueID": "Zoryn.TrainerMod",
+ "Description": "Adds SMAPI console commands that let you manipulate the game.",
+ "UniqueID": "SMAPI.TrainerMod",
"EntryDll": "TrainerMod.dll"
-} \ No newline at end of file
+}
diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets
index f411b909..709bd8d4 100644
--- a/src/prepare-install-package.targets
+++ b/src/prepare-install-package.targets
@@ -23,13 +23,11 @@
<!-- copy SMAPI files for Mono -->
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
- <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.Rocks.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe.mdb" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackageInternalPath)\Mono" />
- <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.data.json" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Numerics.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\System.Runtime.Caching.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\unix-launcher.sh" DestinationFiles="$(PackageInternalPath)\Mono\StardewModdingAPI" />
@@ -38,14 +36,12 @@
<!-- copy SMAPI files for Windows -->
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
- <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.Rocks.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.pdb" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.xml" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.config.json" DestinationFolder="$(PackageInternalPath)\Windows" />
- <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.data.json" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\steam_appid.txt" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="@(CompiledMods)" DestinationFolder="$(PackageInternalPath)\Windows\Mods\%(RecursiveDir)" />
</Target>