From 46b21d1d3a7a3109c898b380bec828abf568baa1 Mon Sep 17 00:00:00 2001
From: yangzhi <@4F!xZpJwly&KbWq>
Date: Wed, 10 Apr 2019 23:10:40 +0800
Subject: [PATCH] Transportation for ContentPatcher MOD, Renamed Namespace,
Disabled Compatible Check. Some of the mod without transportation may works
now
---
Mods/AutoFish/AutoFish.csproj | 4 +-
Mods/AutoFish/AutoFish/ModConfig.cs | 3 +-
Mods/AutoFish/AutoFish/ModEntry.cs | 14 +-
Mods/Automate/Automate.csproj | 4 +-
Mods/ContentPatcher/Common/CommonHelper.cs | 345 ++++++++
.../Common/DataParsers/CropDataParser.cs | 93 ++
.../Automate/AutomateIntegration.cs | 44 +
.../Integrations/Automate/IAutomateApi.cs | 15 +
.../Common/Integrations/BaseIntegration.cs | 82 ++
.../BetterJunimos/BetterJunimosIntegration.cs | 40 +
.../BetterJunimos/IBetterJunimosApi.cs | 9 +
.../BetterSprinklersIntegration.cs | 49 ++
.../BetterSprinklers/IBetterSprinklersApi.cs | 15 +
.../Integrations/Cobalt/CobaltIntegration.cs | 48 ++
.../Common/Integrations/Cobalt/ICobaltApi.cs | 19 +
.../CustomFarmingReduxIntegration.cs | 49 ++
.../CustomFarmingRedux/ICustomFarmingApi.cs | 20 +
.../FarmExpansion/FarmExpansionIntegration.cs | 49 ++
.../FarmExpansion/IFarmExpansionApi.cs | 16 +
.../Common/Integrations/IModIntegration.cs | 15 +
.../LineSprinklers/ILineSprinklersApi.cs | 15 +
.../LineSprinklersIntegration.cs | 49 ++
.../PelicanFiber/PelicanFiberIntegration.cs | 49 ++
.../PrismaticTools/IPrismaticToolsApi.cs | 19 +
.../PrismaticToolsIntegration.cs | 55 ++
.../SimpleSprinkler/ISimplerSprinklerApi.cs | 12 +
.../SimpleSprinklerIntegration.cs | 41 +
Mods/ContentPatcher/Common/PathUtilities.cs | 86 ++
Mods/ContentPatcher/Common/SpriteInfo.cs | 31 +
.../Common/StringEnumArrayConverter.cs | 153 ++++
Mods/ContentPatcher/Common/TileHelper.cs | 126 +++
Mods/ContentPatcher/Common/UI/BaseOverlay.cs | 214 +++++
.../ContentPatcher/Common/UI/CommonSprites.cs | 79 ++
.../Common/Utilities/ConstraintSet.cs | 141 +++
.../Common/Utilities/InvariantDictionary.cs | 30 +
.../Common/Utilities/InvariantHashSet.cs | 32 +
.../Utilities/ObjectReferenceComparer.cs | 29 +
Mods/ContentPatcher/ContentPatcher.csproj | 271 ++++++
.../Framework/CaseInsensitiveExtensions.cs | 52 ++
.../Framework/Commands/CommandHandler.cs | 356 ++++++++
.../Framework/Commands/PatchInfo.cs | 99 +++
.../Framework/Conditions/Condition.cs | 41 +
.../Conditions/ConditionDictionary.cs | 21 +
.../Framework/Conditions/ConditionType.cs | 93 ++
.../Framework/Conditions/PatchType.cs | 15 +
.../Framework/Conditions/TokenString.cs | 144 ++++
.../Framework/Conditions/Weather.cs | 21 +
.../Framework/ConfigFileHandler.cs | 206 +++++
.../Framework/ConfigModels/ConfigField.cs | 43 +
.../ConfigModels/ConfigSchemaFieldConfig.cs | 18 +
.../Framework/ConfigModels/ContentConfig.cs | 21 +
.../ConfigModels/DynamicTokenConfig.cs | 20 +
.../Framework/ConfigModels/ModConfig.cs | 39 +
.../Framework/ConfigModels/PatchConfig.cs | 83 ++
.../Framework/Constants/FarmCaveType.cs | 17 +
.../Framework/Constants/FarmType.cs | 26 +
.../Framework/Constants/Gender.cs | 12 +
.../Framework/Constants/PetType.cs | 12 +
.../Framework/Constants/Profession.cs | 113 +++
.../Framework/Constants/Skill.cs | 26 +
.../Framework/Constants/WalletItem.cs | 36 +
Mods/ContentPatcher/Framework/DebugOverlay.cs | 109 +++
.../Framework/GenericTokenContext.cs | 84 ++
.../Framework/Lexing/LexTokens/ILexToken.cs | 15 +
.../Framework/Lexing/LexTokens/LexBit.cs | 28 +
.../Framework/Lexing/LexTokens/LexBitType.cs | 21 +
.../Lexing/LexTokens/LexTokenInputArg.cs | 33 +
.../Lexing/LexTokens/LexTokenLiteral.cs | 27 +
.../Lexing/LexTokens/LexTokenPipe.cs | 27 +
.../Lexing/LexTokens/LexTokenToken.cs | 72 ++
.../Lexing/LexTokens/LexTokenType.cs | 18 +
Mods/ContentPatcher/Framework/Lexing/Lexer.cs | 305 +++++++
.../Framework/ManagedContentPack.cs | 111 +++
.../Migrations/AggregateMigration.cs | 105 +++
.../Framework/Migrations/BaseMigration.cs | 96 +++
.../Framework/Migrations/IMigration.cs | 39 +
.../Framework/Migrations/Migration_1_3.cs | 56 ++
.../Framework/Migrations/Migration_1_4.cs | 30 +
.../Framework/Migrations/Migration_1_5.cs | 65 ++
.../Framework/Migrations/Migration_1_6.cs | 46 +
.../Framework/ModTokenContext.cs | 161 ++++
Mods/ContentPatcher/Framework/PatchManager.cs | 310 +++++++
.../Framework/Patches/DisabledPatch.cs | 42 +
.../Framework/Patches/EditDataPatch.cs | 184 ++++
.../Framework/Patches/EditDataPatchField.cs | 43 +
.../Framework/Patches/EditDataPatchRecord.cs | 38 +
.../Framework/Patches/EditImagePatch.cs | 129 +++
.../Framework/Patches/IPatch.cs | 68 ++
.../Framework/Patches/LoadPatch.cs | 65 ++
.../ContentPatcher/Framework/Patches/Patch.cs | 136 +++
.../Framework/RawContentPack.cs | 40 +
Mods/ContentPatcher/Framework/TokenManager.cs | 232 +++++
.../Framework/Tokens/DynamicToken.cs | 50 ++
.../Framework/Tokens/DynamicTokenValue.cs | 36 +
.../Framework/Tokens/GenericToken.cs | 239 ++++++
.../Framework/Tokens/IContext.cs | 31 +
.../ContentPatcher/Framework/Tokens/IToken.cs | 60 ++
.../Framework/Tokens/ImmutableToken.cs | 20 +
.../Framework/Tokens/TokenName.cs | 158 ++++
.../ValueProviders/BaseValueProvider.cs | 248 ++++++
.../ConditionTypeValueProvider.cs | 90 ++
.../DynamicTokenValueProvider.cs | 77 ++
.../ValueProviders/HasFileValueProvider.cs | 100 +++
.../HasProfessionValueProvider.cs | 100 +++
.../HasWalletItemValueProvider.cs | 94 ++
.../Tokens/ValueProviders/IValueProvider.cs | 61 ++
.../ValueProviders/ImmutableValueProvider.cs | 61 ++
.../ValueProviders/SkillLevelValueProvider.cs | 77 ++
.../VillagerHeartsValueProvider.cs | 70 ++
.../VillagerRelationshipValueProvider.cs | 69 ++
.../Framework/Validators/BaseValidator.cs | 24 +
.../Framework/Validators/IAssetValidator.cs | 20 +
.../StardewValley_1_3_36_Validator.cs | 91 ++
Mods/ContentPatcher/ModEntry.cs | 811 ++++++++++++++++++
.../ContentPatcher/Properties/AssemblyInfo.cs | 36 +
Mods/ConvenientChests/ConvenientChests.csproj | 4 +-
Mods/Mods.sln | 6 +
Mods/ScytheHarvesting/ScytheHarvesting.csproj | 4 +-
.../SkullCavernElevator.csproj | 4 +-
Mods/TimeSpeed/TimeSpeed.csproj | 4 +-
PatchStep.txt | 2 +-
src/Mod.csproj | 4 +-
src/ModEntry.cs | 1 +
src/SMAPI/Constants.cs | 6 +-
.../ContentManagers/BaseContentManager.cs | 4 +
.../ContentManagers/ModContentManager.cs | 96 ++-
src/SMAPI/Framework/SCore.cs | 2 +-
src/SMAPI/Framework/SGame.cs | 12 +-
128 files changed, 9356 insertions(+), 30 deletions(-)
create mode 100644 Mods/ContentPatcher/Common/CommonHelper.cs
create mode 100644 Mods/ContentPatcher/Common/DataParsers/CropDataParser.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/Automate/AutomateIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/Automate/IAutomateApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/BaseIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/BetterJunimos/BetterJunimosIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/BetterJunimos/IBetterJunimosApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/BetterSprinklers/BetterSprinklersIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/BetterSprinklers/IBetterSprinklersApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/Cobalt/CobaltIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/Cobalt/ICobaltApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/CustomFarmingReduxIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/ICustomFarmingApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/FarmExpansion/FarmExpansionIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/FarmExpansion/IFarmExpansionApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/IModIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/LineSprinklers/ILineSprinklersApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/LineSprinklers/LineSprinklersIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/PelicanFiber/PelicanFiberIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/PrismaticTools/IPrismaticToolsApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/PrismaticTools/PrismaticToolsIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/ISimplerSprinklerApi.cs
create mode 100644 Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/SimpleSprinklerIntegration.cs
create mode 100644 Mods/ContentPatcher/Common/PathUtilities.cs
create mode 100644 Mods/ContentPatcher/Common/SpriteInfo.cs
create mode 100644 Mods/ContentPatcher/Common/StringEnumArrayConverter.cs
create mode 100644 Mods/ContentPatcher/Common/TileHelper.cs
create mode 100644 Mods/ContentPatcher/Common/UI/BaseOverlay.cs
create mode 100644 Mods/ContentPatcher/Common/UI/CommonSprites.cs
create mode 100644 Mods/ContentPatcher/Common/Utilities/ConstraintSet.cs
create mode 100644 Mods/ContentPatcher/Common/Utilities/InvariantDictionary.cs
create mode 100644 Mods/ContentPatcher/Common/Utilities/InvariantHashSet.cs
create mode 100644 Mods/ContentPatcher/Common/Utilities/ObjectReferenceComparer.cs
create mode 100644 Mods/ContentPatcher/ContentPatcher.csproj
create mode 100644 Mods/ContentPatcher/Framework/CaseInsensitiveExtensions.cs
create mode 100644 Mods/ContentPatcher/Framework/Commands/CommandHandler.cs
create mode 100644 Mods/ContentPatcher/Framework/Commands/PatchInfo.cs
create mode 100644 Mods/ContentPatcher/Framework/Conditions/Condition.cs
create mode 100644 Mods/ContentPatcher/Framework/Conditions/ConditionDictionary.cs
create mode 100644 Mods/ContentPatcher/Framework/Conditions/ConditionType.cs
create mode 100644 Mods/ContentPatcher/Framework/Conditions/PatchType.cs
create mode 100644 Mods/ContentPatcher/Framework/Conditions/TokenString.cs
create mode 100644 Mods/ContentPatcher/Framework/Conditions/Weather.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigFileHandler.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigModels/ConfigField.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigModels/ConfigSchemaFieldConfig.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigModels/ContentConfig.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigModels/DynamicTokenConfig.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigModels/ModConfig.cs
create mode 100644 Mods/ContentPatcher/Framework/ConfigModels/PatchConfig.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/FarmCaveType.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/FarmType.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/Gender.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/PetType.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/Profession.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/Skill.cs
create mode 100644 Mods/ContentPatcher/Framework/Constants/WalletItem.cs
create mode 100644 Mods/ContentPatcher/Framework/DebugOverlay.cs
create mode 100644 Mods/ContentPatcher/Framework/GenericTokenContext.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/ILexToken.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexBit.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexBitType.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexTokenInputArg.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexTokenLiteral.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexTokenPipe.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexTokenToken.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/LexTokens/LexTokenType.cs
create mode 100644 Mods/ContentPatcher/Framework/Lexing/Lexer.cs
create mode 100644 Mods/ContentPatcher/Framework/ManagedContentPack.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/AggregateMigration.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/BaseMigration.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/IMigration.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/Migration_1_3.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/Migration_1_4.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/Migration_1_5.cs
create mode 100644 Mods/ContentPatcher/Framework/Migrations/Migration_1_6.cs
create mode 100644 Mods/ContentPatcher/Framework/ModTokenContext.cs
create mode 100644 Mods/ContentPatcher/Framework/PatchManager.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/DisabledPatch.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/EditDataPatch.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/EditDataPatchField.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/EditDataPatchRecord.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/EditImagePatch.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/IPatch.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/LoadPatch.cs
create mode 100644 Mods/ContentPatcher/Framework/Patches/Patch.cs
create mode 100644 Mods/ContentPatcher/Framework/RawContentPack.cs
create mode 100644 Mods/ContentPatcher/Framework/TokenManager.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/DynamicToken.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/DynamicTokenValue.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/GenericToken.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/IContext.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/IToken.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ImmutableToken.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/TokenName.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/BaseValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/ConditionTypeValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/DynamicTokenValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/HasFileValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/HasProfessionValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/HasWalletItemValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/IValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/ImmutableValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/SkillLevelValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/VillagerHeartsValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Tokens/ValueProviders/VillagerRelationshipValueProvider.cs
create mode 100644 Mods/ContentPatcher/Framework/Validators/BaseValidator.cs
create mode 100644 Mods/ContentPatcher/Framework/Validators/IAssetValidator.cs
create mode 100644 Mods/ContentPatcher/Framework/Validators/StardewValley_1_3_36_Validator.cs
create mode 100644 Mods/ContentPatcher/ModEntry.cs
create mode 100644 Mods/ContentPatcher/Properties/AssemblyInfo.cs
diff --git a/Mods/AutoFish/AutoFish.csproj b/Mods/AutoFish/AutoFish.csproj
index bc33fade..5241dcfd 100644
--- a/Mods/AutoFish/AutoFish.csproj
+++ b/Mods/AutoFish/AutoFish.csproj
@@ -31,8 +31,8 @@
4
-
- ..\assemblies\Mod.dll
+
+ ..\assemblies\StardewModdingAPI.dll
..\assemblies\StardewValley.dll
diff --git a/Mods/AutoFish/AutoFish/ModConfig.cs b/Mods/AutoFish/AutoFish/ModConfig.cs
index 3604f5cd..7f13085b 100644
--- a/Mods/AutoFish/AutoFish/ModConfig.cs
+++ b/Mods/AutoFish/AutoFish/ModConfig.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -12,5 +12,6 @@ namespace AutoFish
public bool autoHit { get; set; } = true;
public bool fastBite { get; set; } = false;
public bool catchTreasure { get; set; } = true;
+ public bool autoPlay { get; set; } = true;
}
}
diff --git a/Mods/AutoFish/AutoFish/ModEntry.cs b/Mods/AutoFish/AutoFish/ModEntry.cs
index 0d33b57e..0ded242c 100644
--- a/Mods/AutoFish/AutoFish/ModEntry.cs
+++ b/Mods/AutoFish/AutoFish/ModEntry.cs
@@ -25,6 +25,12 @@ namespace AutoFish
public override List GetConfigMenuItems()
{
List options = new List();
+ ModOptionsCheckbox _optionsCheckboxPlay = new ModOptionsCheckbox("自动钓鱼", 0x8765, delegate (bool value) {
+ this.Config.autoPlay = value;
+ this.Helper.WriteConfig(this.Config);
+ }, -1, -1);
+ _optionsCheckboxPlay.isChecked = this.Config.autoPlay;
+ options.Add(_optionsCheckboxPlay);
ModOptionsCheckbox _optionsCheckboxAutoHit = new ModOptionsCheckbox("自动起钩", 0x8765, delegate (bool value) {
this.Config.autoHit = value;
this.Helper.WriteConfig(this.Config);
@@ -43,12 +49,6 @@ namespace AutoFish
}, -1, -1);
_optionsCheckboxFastBite.isChecked = this.Config.fastBite;
options.Add(_optionsCheckboxFastBite);
- ModOptionsCheckbox _optionsCheckboxCatchTreasure = new ModOptionsCheckbox("钓取宝箱", 0x8765, delegate (bool value) {
- this.Config.catchTreasure = value;
- this.Helper.WriteConfig(this.Config);
- }, -1, -1);
- _optionsCheckboxCatchTreasure.isChecked = this.Config.catchTreasure;
- options.Add(_optionsCheckboxCatchTreasure);
return options;
}
@@ -71,7 +71,7 @@ namespace AutoFish
currentTool.castingPower = 1;
}
- if (Game1.activeClickableMenu is BobberBar) // 自动小游戏
+ if (this.Config.autoPlay && Game1.activeClickableMenu is BobberBar) // 自动小游戏
{
BobberBar bar = Game1.activeClickableMenu as BobberBar;
float barPos = this.Helper.Reflection.GetField(bar, "bobberBarPos").GetValue();
diff --git a/Mods/Automate/Automate.csproj b/Mods/Automate/Automate.csproj
index 295083d6..4ee34b50 100644
--- a/Mods/Automate/Automate.csproj
+++ b/Mods/Automate/Automate.csproj
@@ -33,8 +33,8 @@
7.2
-
- ..\assemblies\Mod.dll
+
+ ..\assemblies\StardewModdingAPI.dll
..\assemblies\StardewValley.dll
diff --git a/Mods/ContentPatcher/Common/CommonHelper.cs b/Mods/ContentPatcher/Common/CommonHelper.cs
new file mode 100644
index 00000000..720736e4
--- /dev/null
+++ b/Mods/ContentPatcher/Common/CommonHelper.cs
@@ -0,0 +1,345 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Pathoschild.Stardew.Common.UI;
+using StardewModdingAPI;
+using StardewValley;
+using StardewValley.Locations;
+using StardewValley.Menus;
+
+namespace Pathoschild.Stardew.Common
+{
+ /// Provides common utility methods for interacting with the game code shared by my various mods.
+ internal static class CommonHelper
+ {
+ /*********
+ ** Fields
+ *********/
+ /// A blank pixel which can be colorised and stretched to draw geometric shapes.
+ private static readonly Lazy LazyPixel = new Lazy(() =>
+ {
+ Texture2D pixel = new Texture2D(Game1.graphics.GraphicsDevice, 1, 1);
+ pixel.SetData(new[] { Color.White });
+ return pixel;
+ });
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// A blank pixel which can be colorised and stretched to draw geometric shapes.
+ public static Texture2D Pixel => CommonHelper.LazyPixel.Value;
+
+ /// The width of the horizontal and vertical scroll edges (between the origin position and start of content padding).
+ public static readonly Vector2 ScrollEdgeSize = new Vector2(CommonSprites.Scroll.TopLeft.Width * Game1.pixelZoom, CommonSprites.Scroll.TopLeft.Height * Game1.pixelZoom);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /****
+ ** Game
+ ****/
+ /// Get all game locations.
+ public static IEnumerable GetLocations()
+ {
+ return Game1.locations
+ .Concat(
+ from location in Game1.locations.OfType()
+ from building in location.buildings
+ where building.indoors.Value != null
+ select building.indoors.Value
+ );
+ }
+
+ /****
+ ** Fonts
+ ****/
+ /// Get the dimensions of a space character.
+ /// The font to measure.
+ public static float GetSpaceWidth(SpriteFont font)
+ {
+ return font.MeasureString("A B").X - font.MeasureString("AB").X;
+ }
+
+ /****
+ ** UI
+ ****/
+ /// Draw a pretty hover box for the given text.
+ /// The sprite batch being drawn.
+ /// The text to display.
+ /// The position at which to draw the text.
+ /// The maximum width to display.
+ public static Vector2 DrawHoverBox(SpriteBatch spriteBatch, string label, in Vector2 position, float wrapWidth)
+ {
+ const int paddingSize = 27;
+ const int gutterSize = 20;
+
+ Vector2 labelSize = spriteBatch.DrawTextBlock(Game1.smallFont, label, position + new Vector2(gutterSize), wrapWidth); // draw text to get wrapped text dimensions
+ IClickableMenu.drawTextureBox(spriteBatch, Game1.menuTexture, new Rectangle(0, 256, 60, 60), (int)position.X, (int)position.Y, (int)labelSize.X + paddingSize + gutterSize, (int)labelSize.Y + paddingSize, Color.White);
+ spriteBatch.DrawTextBlock(Game1.smallFont, label, position + new Vector2(gutterSize), wrapWidth); // draw again over texture box
+
+ return labelSize + new Vector2(paddingSize);
+ }
+
+ /// Draw a button background.
+ /// The sprite batch to which to draw.
+ /// The top-left pixel coordinate at which to draw the button.
+ /// The button content's pixel size.
+ /// The pixel position at which the content begins.
+ /// The button's outer bounds.
+ /// The padding between the content and border.
+ public static void DrawButton(SpriteBatch spriteBatch, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding = 0)
+ {
+ CommonHelper.DrawContentBox(
+ spriteBatch: spriteBatch,
+ texture: CommonSprites.Button.Sheet,
+ background: CommonSprites.Button.Background,
+ top: CommonSprites.Button.Top,
+ right: CommonSprites.Button.Right,
+ bottom: CommonSprites.Button.Bottom,
+ left: CommonSprites.Button.Left,
+ topLeft: CommonSprites.Button.TopLeft,
+ topRight: CommonSprites.Button.TopRight,
+ bottomRight: CommonSprites.Button.BottomRight,
+ bottomLeft: CommonSprites.Button.BottomLeft,
+ position: position,
+ contentSize: contentSize,
+ contentPos: out contentPos,
+ bounds: out bounds,
+ padding: padding
+ );
+ }
+
+ /// Draw a scroll background.
+ /// The sprite batch to which to draw.
+ /// The top-left pixel coordinate at which to draw the scroll.
+ /// The scroll content's pixel size.
+ /// The pixel position at which the content begins.
+ /// The scroll's outer bounds.
+ /// The padding between the content and border.
+ public static void DrawScroll(SpriteBatch spriteBatch, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding = 5)
+ {
+ CommonHelper.DrawContentBox(
+ spriteBatch: spriteBatch,
+ texture: CommonSprites.Scroll.Sheet,
+ background: in CommonSprites.Scroll.Background,
+ top: CommonSprites.Scroll.Top,
+ right: CommonSprites.Scroll.Right,
+ bottom: CommonSprites.Scroll.Bottom,
+ left: CommonSprites.Scroll.Left,
+ topLeft: CommonSprites.Scroll.TopLeft,
+ topRight: CommonSprites.Scroll.TopRight,
+ bottomRight: CommonSprites.Scroll.BottomRight,
+ bottomLeft: CommonSprites.Scroll.BottomLeft,
+ position: position,
+ contentSize: contentSize,
+ contentPos: out contentPos,
+ bounds: out bounds,
+ padding: padding
+ );
+ }
+
+ /// Draw a generic content box like a scroll or button.
+ /// The sprite batch to which to draw.
+ /// The texture to draw.
+ /// The source rectangle for the background.
+ /// The source rectangle for the top border.
+ /// The source rectangle for the right border.
+ /// The source rectangle for the bottom border.
+ /// The source rectangle for the left border.
+ /// The source rectangle for the top-left corner.
+ /// The source rectangle for the top-right corner.
+ /// The source rectangle for the bottom-right corner.
+ /// The source rectangle for the bottom-left corner.
+ /// The top-left pixel coordinate at which to draw the button.
+ /// The button content's pixel size.
+ /// The pixel position at which the content begins.
+ /// The box's outer bounds.
+ /// The padding between the content and border.
+ public static void DrawContentBox(SpriteBatch spriteBatch, Texture2D texture, in Rectangle background, in Rectangle top, in Rectangle right, in Rectangle bottom, in Rectangle left, in Rectangle topLeft, in Rectangle topRight, in Rectangle bottomRight, in Rectangle bottomLeft, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding)
+ {
+ int cornerWidth = topLeft.Width * Game1.pixelZoom;
+ int cornerHeight = topLeft.Height * Game1.pixelZoom;
+ int innerWidth = (int)(contentSize.X + padding * 2);
+ int innerHeight = (int)(contentSize.Y + padding * 2);
+ int outerWidth = innerWidth + cornerWidth * 2;
+ int outerHeight = innerHeight + cornerHeight * 2;
+ int x = (int)position.X;
+ int y = (int)position.Y;
+
+ // draw scroll background
+ spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y + cornerHeight, innerWidth, innerHeight), background, Color.White);
+
+ // draw borders
+ spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y, innerWidth, cornerHeight), top, Color.White);
+ spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y + cornerHeight + innerHeight, innerWidth, cornerHeight), bottom, Color.White);
+ spriteBatch.Draw(texture, new Rectangle(x, y + cornerHeight, cornerWidth, innerHeight), left, Color.White);
+ spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y + cornerHeight, cornerWidth, innerHeight), right, Color.White);
+
+ // draw corners
+ spriteBatch.Draw(texture, new Rectangle(x, y, cornerWidth, cornerHeight), topLeft, Color.White);
+ spriteBatch.Draw(texture, new Rectangle(x, y + cornerHeight + innerHeight, cornerWidth, cornerHeight), bottomLeft, Color.White);
+ spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y, cornerWidth, cornerHeight), topRight, Color.White);
+ spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y + cornerHeight + innerHeight, cornerWidth, cornerHeight), bottomRight, Color.White);
+
+ // set out params
+ contentPos = new Vector2(x + cornerWidth + padding, y + cornerHeight + padding);
+ bounds = new Rectangle(x, y, outerWidth, outerHeight);
+ }
+
+ /// Show an informational message to the player.
+ /// The message to show.
+ /// The number of milliseconds during which to keep the message on the screen before it fades (or null for the default time).
+ public static void ShowInfoMessage(string message, int? duration = null)
+ {
+ Game1.addHUDMessage(new HUDMessage(message, 3) { noIcon = true, timeLeft = duration ?? HUDMessage.defaultTime });
+ }
+
+ /// Show an error message to the player.
+ /// The message to show.
+ public static void ShowErrorMessage(string message)
+ {
+ Game1.addHUDMessage(new HUDMessage(message, 3));
+ }
+
+ /****
+ ** Drawing
+ ****/
+ /// Draw a sprite to the screen.
+ /// The sprite batch.
+ /// The X-position at which to start the line.
+ /// The X-position at which to start the line.
+ /// The line dimensions.
+ /// The color to tint the sprite.
+ public static void DrawLine(this SpriteBatch batch, float x, float y, in Vector2 size, in Color? color = null)
+ {
+ batch.Draw(CommonHelper.Pixel, new Rectangle((int)x, (int)y, (int)size.X, (int)size.Y), color ?? Color.White);
+ }
+
+ /// Draw a block of text to the screen with the specified wrap width.
+ /// The sprite batch.
+ /// The sprite font.
+ /// The block of text to write.
+ /// The position at which to draw the text.
+ /// The width at which to wrap the text.
+ /// The text color.
+ /// Whether to draw bold text.
+ /// The font scale.
+ /// Returns the text dimensions.
+ public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, string text, in Vector2 position, float wrapWidth, in Color? color = null, bool bold = false, float scale = 1)
+ {
+ if (text == null)
+ return new Vector2(0, 0);
+
+ // get word list
+ List words = new List();
+ foreach (string word in text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
+ {
+ // split on newlines
+ string wordPart = word;
+ int newlineIndex;
+ while ((newlineIndex = wordPart.IndexOf(Environment.NewLine, StringComparison.InvariantCulture)) >= 0)
+ {
+ if (newlineIndex == 0)
+ {
+ words.Add(Environment.NewLine);
+ wordPart = wordPart.Substring(Environment.NewLine.Length);
+ }
+ else if (newlineIndex > 0)
+ {
+ words.Add(wordPart.Substring(0, newlineIndex));
+ words.Add(Environment.NewLine);
+ wordPart = wordPart.Substring(newlineIndex + Environment.NewLine.Length);
+ }
+ }
+
+ // add remaining word (after newline split)
+ if (wordPart.Length > 0)
+ words.Add(wordPart);
+ }
+
+ // track draw values
+ float xOffset = 0;
+ float yOffset = 0;
+ float lineHeight = font.MeasureString("ABC").Y * scale;
+ float spaceWidth = CommonHelper.GetSpaceWidth(font) * scale;
+ float blockWidth = 0;
+ float blockHeight = lineHeight;
+ foreach (string word in words)
+ {
+ // check wrap width
+ float wordWidth = font.MeasureString(word).X * scale;
+ if (word == Environment.NewLine || ((wordWidth + xOffset) > wrapWidth && (int)xOffset != 0))
+ {
+ xOffset = 0;
+ yOffset += lineHeight;
+ blockHeight += lineHeight;
+ }
+ if (word == Environment.NewLine)
+ continue;
+
+ // draw text
+ Vector2 wordPosition = new Vector2(position.X + xOffset, position.Y + yOffset);
+ if (bold)
+ Utility.drawBoldText(batch, word, font, wordPosition, color ?? Color.Black, scale);
+ else
+ batch.DrawString(font, word, wordPosition, color ?? Color.Black, 0, Vector2.Zero, scale, SpriteEffects.None, 1);
+
+ // update draw values
+ if (xOffset + wordWidth > blockWidth)
+ blockWidth = xOffset + wordWidth;
+ xOffset += wordWidth + spaceWidth;
+ }
+
+ // return text position & dimensions
+ return new Vector2(blockWidth, blockHeight);
+ }
+
+ /****
+ ** Error handling
+ ****/
+ /// Intercept errors thrown by the action.
+ /// Encapsulates monitoring and logging.
+ /// The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font.
+ /// The action to invoke.
+ /// A callback invoked if an error is intercepted.
+ public static void InterceptErrors(this IMonitor monitor, string verb, Action action, Action onError = null)
+ {
+ monitor.InterceptErrors(verb, null, action, onError);
+ }
+
+ /// Intercept errors thrown by the action.
+ /// Encapsulates monitoring and logging.
+ /// The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font.
+ /// A more detailed form of if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font.
+ /// The action to invoke.
+ /// A callback invoked if an error is intercepted.
+ public static void InterceptErrors(this IMonitor monitor, string verb, string detailedVerb, Action action, Action onError = null)
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ monitor.InterceptError(ex, verb, detailedVerb);
+ onError?.Invoke(ex);
+ }
+ }
+
+ /// Log an error and warn the user.
+ /// Encapsulates monitoring and logging.
+ /// The exception to handle.
+ /// The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font.
+ /// A more detailed form of if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font.
+ public static void InterceptError(this IMonitor monitor, Exception ex, string verb, string detailedVerb = null)
+ {
+ detailedVerb = detailedVerb ?? verb;
+ monitor.Log($"Something went wrong {detailedVerb}:\n{ex}", LogLevel.Error);
+ CommonHelper.ShowErrorMessage($"Huh. Something went wrong {verb}. The error log has the technical details.");
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/DataParsers/CropDataParser.cs b/Mods/ContentPatcher/Common/DataParsers/CropDataParser.cs
new file mode 100644
index 00000000..a84f4226
--- /dev/null
+++ b/Mods/ContentPatcher/Common/DataParsers/CropDataParser.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Linq;
+using StardewModdingAPI.Utilities;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Common.DataParsers
+{
+ /// Analyses crop data for a tile.
+ internal class CropDataParser
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The crop.
+ public Crop Crop { get; }
+
+ /// The seasons in which the crop grows.
+ public string[] Seasons { get; }
+
+ /// The phase index in when the crop can be harvested.
+ public int HarvestablePhase { get; }
+
+ /// The number of days needed between planting and first harvest.
+ public int DaysToFirstHarvest { get; }
+
+ /// The number of days needed between harvests, after the first harvest.
+ public int DaysToSubsequentHarvest { get; }
+
+ /// Whether the crop can be harvested multiple times.
+ public bool HasMultipleHarvests { get; }
+
+ /// Whether the crop is ready to harvest now.
+ public bool CanHarvestNow { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The crop.
+ public CropDataParser(Crop crop)
+ {
+ this.Crop = crop;
+ if (crop != null)
+ {
+ this.Seasons = crop.seasonsToGrowIn.ToArray();
+ this.HasMultipleHarvests = crop.regrowAfterHarvest.Value == -1;
+ this.HarvestablePhase = crop.phaseDays.Count - 1;
+ this.CanHarvestNow = (crop.currentPhase.Value >= this.HarvestablePhase) && (!crop.fullyGrown.Value || crop.dayOfCurrentPhase.Value <= 0);
+ this.DaysToFirstHarvest = crop.phaseDays.Take(crop.phaseDays.Count - 1).Sum(); // ignore harvestable phase
+ this.DaysToSubsequentHarvest = crop.regrowAfterHarvest.Value;
+ }
+ }
+
+ /// Get the date when the crop will next be ready to harvest.
+ public SDate GetNextHarvest()
+ {
+ // get crop
+ Crop crop = this.Crop;
+ if (crop == null)
+ throw new InvalidOperationException("Can't get the harvest date because there's no crop.");
+
+ // ready now
+ if (this.CanHarvestNow)
+ return SDate.Now();
+
+ // growing: days until next harvest
+ if (!crop.fullyGrown.Value)
+ {
+ int daysUntilLastPhase = this.DaysToFirstHarvest - this.Crop.dayOfCurrentPhase.Value - crop.phaseDays.Take(crop.currentPhase.Value).Sum();
+ return SDate.Now().AddDays(daysUntilLastPhase);
+ }
+
+ // regrowable crop harvested today
+ if (crop.dayOfCurrentPhase.Value >= crop.regrowAfterHarvest.Value)
+ return SDate.Now().AddDays(crop.regrowAfterHarvest.Value);
+
+ // regrowable crop
+ // dayOfCurrentPhase decreases to 0 when fully grown, where <=0 is harvestable
+ return SDate.Now().AddDays(crop.dayOfCurrentPhase.Value);
+ }
+
+ /// Get a sample item acquired by harvesting the crop.
+ public Item GetSampleDrop()
+ {
+ if (this.Crop == null)
+ throw new InvalidOperationException("Can't get a sample drop because there's no crop.");
+
+ return new SObject(this.Crop.indexOfHarvest.Value, 1);
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/Automate/AutomateIntegration.cs b/Mods/ContentPatcher/Common/Integrations/Automate/AutomateIntegration.cs
new file mode 100644
index 00000000..9ee33a2d
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/Automate/AutomateIntegration.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Common.Integrations.Automate
+{
+ /// Handles the logic for integrating with the Automate mod.
+ internal class AutomateIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly IAutomateApi ModApi;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public AutomateIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Automate", "Pathoschild.Automate", "1.11.0", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ }
+
+ /// Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.
+ /// The location for which to display data.
+ /// The tile area for which to display data.
+ public IDictionary GetMachineStates(GameLocation location, Rectangle tileArea)
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetMachineStates(location, tileArea);
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/Automate/IAutomateApi.cs b/Mods/ContentPatcher/Common/Integrations/Automate/IAutomateApi.cs
new file mode 100644
index 00000000..15801325
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/Automate/IAutomateApi.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Common.Integrations.Automate
+{
+ /// The API provided by the Automate mod.
+ public interface IAutomateApi
+ {
+ /// Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.
+ /// The location for which to display data.
+ /// The tile area for which to display data.
+ IDictionary GetMachineStates(GameLocation location, Rectangle tileArea);
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/BaseIntegration.cs b/Mods/ContentPatcher/Common/Integrations/BaseIntegration.cs
new file mode 100644
index 00000000..13898dbc
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/BaseIntegration.cs
@@ -0,0 +1,82 @@
+using System;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations
+{
+ /// The base implementation for a mod integration.
+ internal abstract class BaseIntegration : IModIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's unique ID.
+ protected string ModID { get; }
+
+ /// An API for fetching metadata about loaded mods.
+ protected IModRegistry ModRegistry { get; }
+
+ /// Encapsulates monitoring and logging.
+ protected IMonitor Monitor { get; }
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// A human-readable name for the mod.
+ public string Label { get; }
+
+ /// Whether the mod is available.
+ public bool IsLoaded { get; protected set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// A human-readable name for the mod.
+ /// The mod's unique ID.
+ /// The minimum version of the mod that's supported.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ protected BaseIntegration(string label, string modID, string minVersion, IModRegistry modRegistry, IMonitor monitor)
+ {
+ // init
+ this.Label = label;
+ this.ModID = modID;
+ this.ModRegistry = modRegistry;
+ this.Monitor = monitor;
+
+ // validate mod
+ IManifest manifest = modRegistry.Get(this.ModID)?.Manifest;
+ if (manifest == null)
+ return;
+ if (manifest.Version.IsOlderThan(minVersion))
+ {
+ monitor.Log($"Detected {label} {manifest.Version}, but need {minVersion} or later. Disabled integration with this mod.", LogLevel.Warn);
+ return;
+ }
+ this.IsLoaded = true;
+ }
+
+ /// Get an API for the mod, and show a message if it can't be loaded.
+ /// The API type.
+ protected TInterface GetValidatedApi() where TInterface : class
+ {
+ TInterface api = this.ModRegistry.GetApi(this.ModID);
+ if (api == null)
+ {
+ this.Monitor.Log($"Detected {this.Label}, but couldn't fetch its API. Disabled integration with this mod.", LogLevel.Warn);
+ return null;
+ }
+ return api;
+ }
+
+ /// Assert that the integration is loaded.
+ /// The integration isn't loaded.
+ protected void AssertLoaded()
+ {
+ if (!this.IsLoaded)
+ throw new InvalidOperationException($"The {this.Label} integration isn't loaded.");
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/BetterJunimos/BetterJunimosIntegration.cs b/Mods/ContentPatcher/Common/Integrations/BetterJunimos/BetterJunimosIntegration.cs
new file mode 100644
index 00000000..6c649fca
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/BetterJunimos/BetterJunimosIntegration.cs
@@ -0,0 +1,40 @@
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations.BetterJunimos
+{
+ /// Handles the logic for integrating with the Better Junimos mod.
+ internal class BetterJunimosIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly IBetterJunimosApi ModApi;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// The Junimo Hut coverage radius.
+ public int MaxRadius { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public BetterJunimosIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Better Junimos", "hawkfalcon.BetterJunimos", "0.5.0", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ this.MaxRadius = this.ModApi?.GetJunimoHutMaxRadius() ?? 0;
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/BetterJunimos/IBetterJunimosApi.cs b/Mods/ContentPatcher/Common/Integrations/BetterJunimos/IBetterJunimosApi.cs
new file mode 100644
index 00000000..6081e89b
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/BetterJunimos/IBetterJunimosApi.cs
@@ -0,0 +1,9 @@
+namespace Pathoschild.Stardew.Common.Integrations.BetterJunimos
+{
+ /// The API provided by the Better Junimos mod.
+ public interface IBetterJunimosApi
+ {
+ /// Get the maximum radius for Junimo Huts.
+ int GetJunimoHutMaxRadius();
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/BetterSprinklers/BetterSprinklersIntegration.cs b/Mods/ContentPatcher/Common/Integrations/BetterSprinklers/BetterSprinklersIntegration.cs
new file mode 100644
index 00000000..f7f48248
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/BetterSprinklers/BetterSprinklersIntegration.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations.BetterSprinklers
+{
+ /// Handles the logic for integrating with the Better Sprinklers mod.
+ internal class BetterSprinklersIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly IBetterSprinklersApi ModApi;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// The maximum possible sprinkler radius.
+ public int MaxRadius { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public BetterSprinklersIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Better Sprinklers", "Speeder.BetterSprinklers", "2.3.1-unofficial.6-pathoschild", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ this.MaxRadius = this.ModApi?.GetMaxGridSize() ?? 0;
+ }
+
+ /// Get the configured Sprinkler tiles relative to (0, 0).
+ public IDictionary GetSprinklerTiles()
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetSprinklerCoverage();
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/BetterSprinklers/IBetterSprinklersApi.cs b/Mods/ContentPatcher/Common/Integrations/BetterSprinklers/IBetterSprinklersApi.cs
new file mode 100644
index 00000000..c213f02e
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/BetterSprinklers/IBetterSprinklersApi.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace Pathoschild.Stardew.Common.Integrations.BetterSprinklers
+{
+ /// The API provided by the Better Sprinklers mod.
+ public interface IBetterSprinklersApi
+ {
+ /// Get the maximum supported coverage width or height.
+ int GetMaxGridSize();
+
+ /// Get the relative tile coverage by supported sprinkler ID.
+ IDictionary GetSprinklerCoverage();
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/Cobalt/CobaltIntegration.cs b/Mods/ContentPatcher/Common/Integrations/Cobalt/CobaltIntegration.cs
new file mode 100644
index 00000000..4cb7c36d
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/Cobalt/CobaltIntegration.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations.Cobalt
+{
+ /// Handles the logic for integrating with the Cobalt mod.
+ internal class CobaltIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly ICobaltApi ModApi;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public CobaltIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Cobalt", "spacechase0.Cobalt", "1.1", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ }
+
+ /// Get the cobalt sprinkler's object ID.
+ public int GetSprinklerId()
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetSprinklerId();
+ }
+
+ /// Get the configured Sprinkler tiles relative to (0, 0).
+ public IEnumerable GetSprinklerTiles()
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetSprinklerCoverage(Vector2.Zero);
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/Cobalt/ICobaltApi.cs b/Mods/ContentPatcher/Common/Integrations/Cobalt/ICobaltApi.cs
new file mode 100644
index 00000000..4952043f
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/Cobalt/ICobaltApi.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace Pathoschild.Stardew.Common.Integrations.Cobalt
+{
+ /// The API provided by the Cobalt mod.
+ public interface ICobaltApi
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Get the cobalt sprinkler's object ID.
+ int GetSprinklerId();
+
+ /// Get the cobalt sprinkler coverage.
+ /// The tile position containing the sprinkler.
+ IEnumerable GetSprinklerCoverage(Vector2 origin);
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/CustomFarmingReduxIntegration.cs b/Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/CustomFarmingReduxIntegration.cs
new file mode 100644
index 00000000..277c95c6
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/CustomFarmingReduxIntegration.cs
@@ -0,0 +1,49 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Common.Integrations.CustomFarmingRedux
+{
+ /// Handles the logic for integrating with the Custom Farming Redux mod.
+ internal class CustomFarmingReduxIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly ICustomFarmingApi ModApi;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public CustomFarmingReduxIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Custom Farming Redux", "Platonymous.CustomFarming", "2.8.5", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ }
+
+ /// Get the sprite info for a custom object, or null if the object isn't custom.
+ /// The custom object.
+ public SpriteInfo GetSprite(SObject obj)
+ {
+ this.AssertLoaded();
+
+ Tuple- data = this.ModApi.getRealItemAndTexture(obj);
+ return data != null
+ ? new SpriteInfo(data.Item2, data.Item3)
+ : null;
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/ICustomFarmingApi.cs b/Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/ICustomFarmingApi.cs
new file mode 100644
index 00000000..14b80ffb
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/CustomFarmingRedux/ICustomFarmingApi.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Common.Integrations.CustomFarmingRedux
+{
+ /// The API provided by the Custom Farming Redux mod.
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "The naming convention is defined by the Custom Farming Redux mod.")]
+ public interface ICustomFarmingApi
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Get metadata for a custom machine and draw metadata for an object.
+ /// The item that would be replaced by the custom item.
+ Tuple
- getRealItemAndTexture(StardewValley.Object dummy);
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/FarmExpansion/FarmExpansionIntegration.cs b/Mods/ContentPatcher/Common/Integrations/FarmExpansion/FarmExpansionIntegration.cs
new file mode 100644
index 00000000..a41135e5
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/FarmExpansion/FarmExpansionIntegration.cs
@@ -0,0 +1,49 @@
+using StardewModdingAPI;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Common.Integrations.FarmExpansion
+{
+ /// Handles the logic for integrating with the Farm Expansion mod.
+ internal class FarmExpansionIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly IFarmExpansionApi ModApi;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public FarmExpansionIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Farm Expansion", "Advize.FarmExpansion", "3.3", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ }
+
+ /// Add a blueprint to all future carpenter menus for the farm area.
+ /// The blueprint to add.
+ public void AddFarmBluePrint(BluePrint blueprint)
+ {
+ this.AssertLoaded();
+ this.ModApi.AddFarmBluePrint(blueprint);
+ }
+
+ /// Add a blueprint to all future carpenter menus for the expansion area.
+ /// The blueprint to add.
+ public void AddExpansionBluePrint(BluePrint blueprint)
+ {
+ this.AssertLoaded();
+ this.ModApi.AddExpansionBluePrint(blueprint);
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/FarmExpansion/IFarmExpansionApi.cs b/Mods/ContentPatcher/Common/Integrations/FarmExpansion/IFarmExpansionApi.cs
new file mode 100644
index 00000000..2c4d92a1
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/FarmExpansion/IFarmExpansionApi.cs
@@ -0,0 +1,16 @@
+using StardewValley;
+
+namespace Pathoschild.Stardew.Common.Integrations.FarmExpansion
+{
+ /// The API provided by the Farm Expansion mod.
+ public interface IFarmExpansionApi
+ {
+ /// Add a blueprint to all future carpenter menus for the farm area.
+ /// The blueprint to add.
+ void AddFarmBluePrint(BluePrint blueprint);
+
+ /// Add a blueprint to all future carpenter menus for the expansion area.
+ /// The blueprint to add.
+ void AddExpansionBluePrint(BluePrint blueprint);
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/IModIntegration.cs b/Mods/ContentPatcher/Common/Integrations/IModIntegration.cs
new file mode 100644
index 00000000..17327ed8
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/IModIntegration.cs
@@ -0,0 +1,15 @@
+namespace Pathoschild.Stardew.Common.Integrations
+{
+ /// Handles integration with a given mod.
+ internal interface IModIntegration
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// A human-readable name for the mod.
+ string Label { get; }
+
+ /// Whether the mod is available.
+ bool IsLoaded { get; }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/LineSprinklers/ILineSprinklersApi.cs b/Mods/ContentPatcher/Common/Integrations/LineSprinklers/ILineSprinklersApi.cs
new file mode 100644
index 00000000..a945c8c3
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/LineSprinklers/ILineSprinklersApi.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace Pathoschild.Stardew.Common.Integrations.LineSprinklers
+{
+ /// The API provided by the Line Sprinklers mod.
+ public interface ILineSprinklersApi
+ {
+ /// Get the maximum supported coverage width or height.
+ int GetMaxGridSize();
+
+ /// Get the relative tile coverage by supported sprinkler ID.
+ IDictionary GetSprinklerCoverage();
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/LineSprinklers/LineSprinklersIntegration.cs b/Mods/ContentPatcher/Common/Integrations/LineSprinklers/LineSprinklersIntegration.cs
new file mode 100644
index 00000000..d5aa4fce
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/LineSprinklers/LineSprinklersIntegration.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations.LineSprinklers
+{
+ /// Handles the logic for integrating with the Line Sprinklers mod.
+ internal class LineSprinklersIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly ILineSprinklersApi ModApi;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// The maximum possible sprinkler radius.
+ public int MaxRadius { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public LineSprinklersIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Line Sprinklers", "hootless.LineSprinklers", "1.1.0", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ this.MaxRadius = this.ModApi?.GetMaxGridSize() ?? 0;
+ }
+
+ /// Get the configured Sprinkler tiles relative to (0, 0).
+ public IDictionary GetSprinklerTiles()
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetSprinklerCoverage();
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/PelicanFiber/PelicanFiberIntegration.cs b/Mods/ContentPatcher/Common/Integrations/PelicanFiber/PelicanFiberIntegration.cs
new file mode 100644
index 00000000..f90cfb74
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/PelicanFiber/PelicanFiberIntegration.cs
@@ -0,0 +1,49 @@
+using StardewModdingAPI;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Common.Integrations.PelicanFiber
+{
+ /// Handles the logic for integrating with the Pelican Fiber mod.
+ internal class PelicanFiberIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The full type name of the Pelican Fiber mod's build menu.
+ private readonly string MenuTypeName = "PelicanFiber.Framework.ConstructionMenu";
+
+ /// An API for accessing private code.
+ private readonly IReflectionHelper Reflection;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// An API for accessing private code.
+ /// Encapsulates monitoring and logging.
+ public PelicanFiberIntegration(IModRegistry modRegistry, IReflectionHelper reflection, IMonitor monitor)
+ : base("Pelican Fiber", "jwdred.PelicanFiber", "3.0.2", modRegistry, monitor)
+ {
+ this.Reflection = reflection;
+ }
+
+ /// Get whether the Pelican Fiber build menu is open.
+ public bool IsBuildMenuOpen()
+ {
+ this.AssertLoaded();
+ return Game1.activeClickableMenu?.GetType().FullName == this.MenuTypeName;
+ }
+
+ /// Get the selected blueprint from the Pelican Fiber build menu, if it's open.
+ public BluePrint GetBuildMenuBlueprint()
+ {
+ this.AssertLoaded();
+ if (!this.IsBuildMenuOpen())
+ return null;
+
+ return this.Reflection.GetProperty(Game1.activeClickableMenu, "CurrentBlueprint").GetValue();
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/PrismaticTools/IPrismaticToolsApi.cs b/Mods/ContentPatcher/Common/Integrations/PrismaticTools/IPrismaticToolsApi.cs
new file mode 100644
index 00000000..b2a61ed3
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/PrismaticTools/IPrismaticToolsApi.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace Pathoschild.Stardew.Common.Integrations.PrismaticTools
+{
+ /// The API provided by the Prismatic Tools mod.
+ public interface IPrismaticToolsApi
+ {
+ /// Whether prismatic sprinklers also act as scarecrows.
+ bool ArePrismaticSprinklersScarecrows { get; }
+
+ /// The prismatic sprinkler object ID.
+ int SprinklerIndex { get; }
+
+ /// Get the relative tile coverage for a prismatic sprinkler.
+ /// The sprinkler tile.
+ IEnumerable GetSprinklerCoverage(Vector2 origin);
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/PrismaticTools/PrismaticToolsIntegration.cs b/Mods/ContentPatcher/Common/Integrations/PrismaticTools/PrismaticToolsIntegration.cs
new file mode 100644
index 00000000..b35e6f35
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/PrismaticTools/PrismaticToolsIntegration.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations.PrismaticTools
+{
+ /// Handles the logic for integrating with the Prismatic Tools mod.
+ internal class PrismaticToolsIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly IPrismaticToolsApi ModApi;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public PrismaticToolsIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Prismatic Tools", "stokastic.PrismaticTools", "1.3.0", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ }
+
+ /// Get whether prismatic sprinklers also act as scarecrows.
+ public bool ArePrismaticSprinklersScarecrows()
+ {
+ this.AssertLoaded();
+ return this.ModApi.ArePrismaticSprinklersScarecrows;
+ }
+
+ /// Get the prismatic sprinkler object ID.
+ public int GetSprinklerID()
+ {
+ this.AssertLoaded();
+ return this.ModApi.SprinklerIndex;
+ }
+
+ /// Get the relative tile coverage for a prismatic sprinkler.
+ public IEnumerable GetSprinklerCoverage()
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetSprinklerCoverage(Vector2.Zero);
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/ISimplerSprinklerApi.cs b/Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/ISimplerSprinklerApi.cs
new file mode 100644
index 00000000..68d8e05a
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/ISimplerSprinklerApi.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace Pathoschild.Stardew.Common.Integrations.SimpleSprinkler
+{
+ /// The API provided by the Simple Sprinkler mod.
+ public interface ISimplerSprinklerApi
+ {
+ /// Get the relative tile coverage for supported sprinkler IDs (additive to the game's default coverage).
+ IDictionary GetNewSprinklerCoverage();
+ }
+}
diff --git a/Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/SimpleSprinklerIntegration.cs b/Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/SimpleSprinklerIntegration.cs
new file mode 100644
index 00000000..ef21dd31
--- /dev/null
+++ b/Mods/ContentPatcher/Common/Integrations/SimpleSprinkler/SimpleSprinklerIntegration.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Common.Integrations.SimpleSprinkler
+{
+ /// Handles the logic for integrating with the Simple Sprinkler mod.
+ internal class SimpleSprinklerIntegration : BaseIntegration
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod's public API.
+ private readonly ISimplerSprinklerApi ModApi;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// An API for fetching metadata about loaded mods.
+ /// Encapsulates monitoring and logging.
+ public SimpleSprinklerIntegration(IModRegistry modRegistry, IMonitor monitor)
+ : base("Simple Sprinklers", "tZed.SimpleSprinkler", "1.6.0", modRegistry, monitor)
+ {
+ if (!this.IsLoaded)
+ return;
+
+ // get mod API
+ this.ModApi = this.GetValidatedApi();
+ this.IsLoaded = this.ModApi != null;
+ }
+
+ /// Get the Sprinkler tiles relative to (0, 0), additive to the game's default sprinkler coverage.
+ public IDictionary GetNewSprinklerTiles()
+ {
+ this.AssertLoaded();
+ return this.ModApi.GetNewSprinklerCoverage();
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/PathUtilities.cs b/Mods/ContentPatcher/Common/PathUtilities.cs
new file mode 100644
index 00000000..40b174f0
--- /dev/null
+++ b/Mods/ContentPatcher/Common/PathUtilities.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Pathoschild.Stardew.Common
+{
+ /// Provides utilities for normalising file paths.
+ /// This class is duplicated from StardewModdingAPI.Toolkit.Utilities.
+ internal static class PathUtilities
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The possible directory separator characters in a file path.
+ private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
+
+ /// The preferred directory separator chaeacter in an asset key.
+ private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop).
+ /// The path to split.
+ /// The number of segments to match. Any additional segments will be merged into the last returned part.
+ public static string[] GetSegments(string path, int? limit = null)
+ {
+ return limit.HasValue
+ ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries)
+ : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ /// Normalise path separators in a file path.
+ /// The file path to normalise.
+ [Pure]
+ public static string NormalisePathSeparators(string path)
+ {
+ string[] parts = PathUtilities.GetSegments(path);
+ string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts);
+ if (path.StartsWith(PathUtilities.PreferredPathSeparator))
+ normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash
+ return normalised;
+ }
+
+ /// Get a directory or file path relative to a given source path.
+ /// The source folder path.
+ /// The target folder or file path.
+ [Pure]
+ public static string GetRelativePath(string sourceDir, string targetPath)
+ {
+ // convert to URIs
+ Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
+ Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
+ if (from.Scheme != to.Scheme)
+ throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'.");
+
+ // get relative path
+ string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()));
+ if (relative == "")
+ relative = "./";
+ return relative;
+ }
+
+ /// Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain ../).
+ /// The path to check.
+ public static bool IsSafeRelativePath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return true;
+
+ return
+ !Path.IsPathRooted(path)
+ && PathUtilities.GetSegments(path).All(segment => segment.Trim() != "..");
+ }
+
+ /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).
+ /// The string to check.
+ public static bool IsSlug(string str)
+ {
+ return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase);
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/SpriteInfo.cs b/Mods/ContentPatcher/Common/SpriteInfo.cs
new file mode 100644
index 00000000..b7c3be5e
--- /dev/null
+++ b/Mods/ContentPatcher/Common/SpriteInfo.cs
@@ -0,0 +1,31 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Pathoschild.Stardew.Common
+{
+ /// Represents a single sprite in a spritesheet.
+ internal class SpriteInfo
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The spritesheet texture.
+ public Texture2D Spritesheet { get; }
+
+ /// The area in the spritesheet containing the sprite.
+ public Rectangle SourceRectangle { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The spritesheet texture.
+ /// The area in the spritesheet containing the sprite.
+ public SpriteInfo(Texture2D spritesheet, Rectangle sourceRectangle)
+ {
+ this.Spritesheet = spritesheet;
+ this.SourceRectangle = sourceRectangle;
+ }
+ }
+}
diff --git a/Mods/ContentPatcher/Common/StringEnumArrayConverter.cs b/Mods/ContentPatcher/Common/StringEnumArrayConverter.cs
new file mode 100644
index 00000000..29e78167
--- /dev/null
+++ b/Mods/ContentPatcher/Common/StringEnumArrayConverter.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+
+namespace Pathoschild.Stardew.Common
+{
+ /// A variant of which represents arrays in JSON as a comma-delimited string.
+ internal class StringEnumArrayConverter : StringEnumConverter
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Whether to return null values for missing data instead of an empty array.
+ public bool AllowNull { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Get whether this instance can convert the specified object type.
+ /// The object type.
+ public override bool CanConvert(Type type)
+ {
+ if (!type.IsArray)
+ return false;
+
+ Type elementType = this.GetElementType(type);
+ return elementType != null && base.CanConvert(elementType);
+ }
+
+ /// Read a JSON representation.
+ /// The JSON reader from which to read.
+ /// The value type.
+ /// The raw value of the object being read.
+ /// The calling serializer.
+ public override object ReadJson(JsonReader reader, Type valueType, object rawValue, JsonSerializer serializer)
+ {
+ // get element type
+ Type elementType = this.GetElementType(valueType);
+ if (elementType == null)
+ throw new InvalidOperationException("Couldn't extract enum array element type."); // should never happen since we validate in CanConvert
+
+ // parse
+ switch (reader.TokenType)
+ {
+ case JsonToken.Null:
+ return this.GetNullOrEmptyArray(elementType);
+
+ case JsonToken.StartArray:
+ {
+ string[] elements = JArray.Load(reader).Values().ToArray();
+ object[] parsed = elements.Select(raw => this.ParseOne(raw, elementType)).ToArray();
+ return this.Cast(parsed, elementType);
+ }
+
+ case JsonToken.String:
+ {
+ string value = (string)JToken.Load(reader);
+
+ if (string.IsNullOrWhiteSpace(value))
+ return this.GetNullOrEmptyArray(elementType);
+
+ object[] parsed = this.ParseMany(value, elementType).ToArray();
+ return this.Cast(parsed, elementType);
+ }
+
+ default:
+ return base.ReadJson(reader, valueType, rawValue, serializer);
+ }
+ }
+
+ /// Write a JSON representation.
+ /// The JSON writer to which to write.
+ /// The value.
+ /// The calling serializer.
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ if (value == null)
+ writer.WriteNull();
+ else if (value is IEnumerable list)
+ {
+ string[] array = (from object element in list where element != null select element.ToString()).ToArray();
+ writer.WriteValue(string.Join(", ", array));
+ }
+ else
+ base.WriteJson(writer, value, serializer);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get the underlying array element type (bypassing if necessary).
+ /// The array type.
+ private Type GetElementType(Type type)
+ {
+ if (!type.IsArray)
+ return null;
+
+ type = type.GetElementType();
+ if (type == null)
+ return null;
+
+ type = Nullable.GetUnderlyingType(type) ?? type;
+
+ return type;
+ }
+
+ /// Parse a string into individual values.
+ /// The input string.
+ /// The enum type.
+ private IEnumerable