From 44ddc4ca42de69fe481d004dffa728c7f539083a Mon Sep 17 00:00:00 2001
From: yangzhi <@4F!xZpJwly&KbWq>
Date: Wed, 10 Apr 2019 01:22:10 +0800
Subject: [PATCH 1/6] Android Mod Transportation
---
Mods/AutoFish/AutoFish.csproj | 162 ++++++++
Mods/AutoFish/AutoFish/ModConfig.cs | 16 +
Mods/AutoFish/AutoFish/ModEntry.cs | 138 +++++++
Mods/AutoFish/Properties/AssemblyInfo.cs | 36 ++
Mods/AutoSpeed/AutoSpeed.csproj | 175 +++++++++
Mods/AutoSpeed/AutoSpeed/AutoSpeed.cs | 58 +++
.../AutoSpeed/Framework/ModConfig.cs | 9 +
Mods/AutoSpeed/AutoSpeed/ModConfig.cs | 14 +
Mods/AutoSpeed/AutoSpeed/ModEntry.cs | 58 +++
Mods/AutoSpeed/AutoSpeed/README.md | 31 ++
Mods/AutoSpeed/AutoSpeed/manifest.json | 10 +
Mods/AutoSpeed/Properties/AssemblyInfo.cs | 36 ++
Mods/Automate/Automate.csproj | 277 ++++++++++++++
.../Automate/Framework/AutomateAPI.cs | 98 +++++
.../Automate/Framework/AutomationFactory.cs | 247 +++++++++++++
.../Automate/Framework/BaseMachine.cs | 90 +++++
Mods/Automate/Automate/Framework/Connector.cs | 37 ++
.../Automate/Automate/Framework/Consumable.cs | 50 +++
.../Framework/GenericObjectMachine.cs | 63 ++++
.../Automate/Framework/MachineGroup.cs | 91 +++++
.../Automate/Framework/MachineGroupBuilder.cs | 90 +++++
.../Automate/Framework/MachineGroupFactory.cs | 176 +++++++++
.../Machines/Buildings/JunimoHutMachine.cs | 88 +++++
.../Machines/Buildings/MillMachine.cs | 173 +++++++++
.../Machines/Buildings/ShippingBinMachine.cs | 70 ++++
.../Machines/Objects/AutoGrabberMachine.cs | 92 +++++
.../Machines/Objects/BeeHouseMachine.cs | 103 ++++++
.../Framework/Machines/Objects/CaskMachine.cs | 99 +++++
.../Machines/Objects/CharcoalKilnMachine.cs | 49 +++
.../Machines/Objects/CheesePressMachine.cs | 75 ++++
.../Machines/Objects/CoopIncubatorMachine.cs | 80 ++++
.../Machines/Objects/CrabPotMachine.cs | 131 +++++++
.../Machines/Objects/CrystalariumMachine.cs | 63 ++++
.../Machines/Objects/FeedHopperMachine.cs | 93 +++++
.../Machines/Objects/FurnaceMachine.cs | 92 +++++
.../Framework/Machines/Objects/KegMachine.cs | 103 ++++++
.../Machines/Objects/LightningRodMachine.cs | 35 ++
.../Framework/Machines/Objects/LoomMachine.cs | 56 +++
.../Machines/Objects/MayonnaiseMachine.cs | 86 +++++
.../Machines/Objects/MushroomBoxMachine.cs | 54 +++
.../Machines/Objects/OilMakerMachine.cs | 68 ++++
.../Machines/Objects/PreservesJarMachine.cs | 74 ++++
.../Machines/Objects/RecyclingMachine.cs | 86 +++++
.../Machines/Objects/SeedMakerMachine.cs | 85 +++++
.../Machines/Objects/SlimeEggPressMachine.cs | 50 +++
.../Machines/Objects/SlimeIncubatorMachine.cs | 90 +++++
.../Framework/Machines/Objects/SodaMachine.cs | 36 ++
.../Objects/StatueOfEndlessFortuneMachine.cs | 36 ++
.../Objects/StatueOfPerfectionMachine.cs | 36 ++
.../Machines/Objects/TapperMachine.cs | 84 +++++
.../Machines/Objects/WormBinMachine.cs | 42 +++
.../TerrainFeatures/FruitTreeMachine.cs | 72 ++++
.../Machines/Tiles/TrashCanMachine.cs | 149 ++++++++
.../Automate/Framework/Models/ModConfig.cs | 37 ++
.../Framework/Models/ModConfigObject.cs | 16 +
.../Automate/Framework/Models/ObjectType.cs | 15 +
.../Automate/Framework/OverlayMenu.cs | 126 +++++++
Mods/Automate/Automate/Framework/Recipe.cs | 49 +++
.../Framework/Storage/ChestContainer.cs | 146 ++++++++
.../Framework/Storage/ContainerExtensions.cs | 47 +++
.../Automate/Framework/StorageManager.cs | 206 +++++++++++
Mods/Automate/Automate/IAutomatable.cs | 18 +
Mods/Automate/Automate/IAutomateAPI.cs | 19 +
Mods/Automate/Automate/IAutomationFactory.cs | 43 +++
Mods/Automate/Automate/IConsumable.cs | 34 ++
Mods/Automate/Automate/IContainer.cs | 31 ++
Mods/Automate/Automate/IMachine.cs | 28 ++
Mods/Automate/Automate/IRecipe.cs | 33 ++
Mods/Automate/Automate/IStorage.cs | 62 ++++
Mods/Automate/Automate/ITrackedStack.cs | 30 ++
Mods/Automate/Automate/MachineState.cs | 18 +
Mods/Automate/Automate/ModEntry.cs | 285 +++++++++++++++
Mods/Automate/Automate/TrackedItem.cs | 102 ++++++
.../Automate/TrackedItemCollection.cs | 84 +++++
.../screenshots/chests-anywhere-config.png | Bin 0 -> 29371 bytes
.../Automate/screenshots/connectors.png | Bin 0 -> 16106 bytes
.../Automate/screenshots/crab-pot-factory.png | Bin 0 -> 8351 bytes
.../Automate/screenshots/example-overlay.png | Bin 0 -> 40618 bytes
.../extensibility-machine-groups.png | Bin 0 -> 5737 bytes
.../screenshots/iridium-bar-factory.png | Bin 0 -> 5295 bytes
.../screenshots/iridium-cheese-factory.png | Bin 0 -> 6067 bytes
.../screenshots/iridium-mead-factory.png | Bin 0 -> 7990 bytes
.../screenshots/refined-quartz-factory.png | Bin 0 -> 4957 bytes
Mods/Automate/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/Automate/Common/PathUtilities.cs | 86 +++++
Mods/Automate/Common/SpriteInfo.cs | 31 ++
.../Common/StringEnumArrayConverter.cs | 153 ++++++++
Mods/Automate/Common/TileHelper.cs | 126 +++++++
Mods/Automate/Common/UI/BaseOverlay.cs | 214 +++++++++++
Mods/Automate/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/Automate/Properties/AssemblyInfo.cs | 36 ++
.../CategorizeChestsModule.cs | 110 ++++++
.../CategorizeChests/Framework/ChestData.cs | 52 +++
.../Framework/ChestDataManager.cs | 12 +
.../Framework/ChestExtension.cs | 86 +++++
.../CategorizeChests/Framework/ChestFinder.cs | 53 +++
.../Framework/DiscoveredItem.cs | 16 +
.../Framework/IChestDataManager.cs | 13 +
.../Framework/IChestFiller.cs | 13 +
.../Framework/IChestFinder.cs | 13 +
.../Framework/IItemDataManager.cs | 17 +
.../Framework/ItemBlacklist.cs | 97 +++++
.../Framework/ItemDataManager.cs | 162 ++++++++
.../CategorizeChests/Framework/ItemKey.cs | 77 ++++
.../Framework/ItemNotImplementedException.cs | 17 +
.../CategorizeChests/Framework/ItemType.cs | 18 +
.../Framework/Persistence/ChestAddress.cs | 40 ++
.../Framework/Persistence/ChestEntry.cs | 43 +++
.../Persistence/ChestLocationType.cs | 9 +
.../Framework/Persistence/ISaveManager.cs | 8 +
.../Persistence/InvalidSaveDataException.cs | 14 +
.../Framework/Persistence/SaveData.cs | 12 +
.../Framework/Persistence/SaveManager.cs | 46 +++
.../CategorizeChests/Framework/Saver.cs | 81 ++++
.../Interface/ITooltipManager.cs | 11 +
.../Interface/InterfaceHost.cs | 216 +++++++++++
.../CategorizeChests/Interface/NineSlice.cs | 77 ++++
.../CategorizeChests/Interface/Sprites.cs | 77 ++++
.../Interface/TextureRegion.cs | 28 ++
.../Interface/TooltipManager.cs | 34 ++
.../CategorizeChests/Interface/WidgetHost.cs | 40 ++
.../Interface/Widgets/Background.cs | 30 ++
.../Interface/Widgets/Button.cs | 19 +
.../Interface/Widgets/CategoryMenu.cs | 171 +++++++++
.../Interface/Widgets/ChestOverlay.cs | 127 +++++++
.../Interface/Widgets/ItemToggle.cs | 48 +++
.../Interface/Widgets/ItemTooltip.cs | 26 ++
.../Interface/Widgets/Label.cs | 52 +++
.../Interface/Widgets/LabeledCheckbox.cs | 49 +++
.../Interface/Widgets/SpriteButton.cs | 25 ++
.../Interface/Widgets/Stamp.cs | 25 ++
.../Interface/Widgets/TextButton.cs | 54 +++
.../Interface/Widgets/Widget.cs | 243 ++++++++++++
.../Interface/Widgets/WrapBag.cs | 41 +++
.../CategorizeChests/ItemHelper.cs | 103 ++++++
.../CategorizeChests/Utility.cs | 46 +++
Mods/ConvenientChests/Config.cs | 15 +
Mods/ConvenientChests/ConvenientChests.csproj | 210 +++++++++++
Mods/ConvenientChests/ModEntry.cs | 47 +++
Mods/ConvenientChests/Module.cs | 17 +
.../Properties/AssemblyInfo.cs | 36 ++
.../StackToNearbyChests/StackLogic.cs | 87 +++++
.../StashToNearbyChestsModule.cs | 59 +++
Mods/Mods.sln | 61 ++++
Mods/ScytheHarvesting/ModConfig.cs | 16 +
.../Properties/AssemblyInfo.cs | 36 ++
Mods/ScytheHarvesting/ScytheHarvesting.cs | 252 +++++++++++++
Mods/ScytheHarvesting/ScytheHarvesting.csproj | 162 ++++++++
Mods/SkullCavernElevator/ModConfig.cs | 13 +
Mods/SkullCavernElevator/ModEntry.cs | 120 ++++++
.../MyElevatorMenuWithScrollbar.cs | 250 +++++++++++++
.../Properties/AssemblyInfo.cs | 36 ++
.../SkullCavernElevator.csproj | 164 +++++++++
.../SkullCavernElevator/MyElevatorMenu.cs | 109 ++++++
Mods/TimeSpeed/Framework/LocationType.cs | 15 +
Mods/TimeSpeed/Framework/ModConfig.cs | 127 +++++++
Mods/TimeSpeed/Framework/ModControlsConfig.cs | 23 ++
Mods/TimeSpeed/Framework/Notifier.cs | 25 ++
.../Framework/TickProgressChangedEventArgs.cs | 33 ++
Mods/TimeSpeed/Framework/TimeHelper.cs | 53 +++
Mods/TimeSpeed/ModEntry.cs | 336 +++++++++++++++++
Mods/TimeSpeed/Properties/AssemblyInfo.cs | 36 ++
Mods/TimeSpeed/TimeSpeed.csproj | 167 +++++++++
PatchStep.txt | 21 ++
SMAPI | 1 +
191 files changed, 13024 insertions(+)
create mode 100644 Mods/AutoFish/AutoFish.csproj
create mode 100644 Mods/AutoFish/AutoFish/ModConfig.cs
create mode 100644 Mods/AutoFish/AutoFish/ModEntry.cs
create mode 100644 Mods/AutoFish/Properties/AssemblyInfo.cs
create mode 100644 Mods/AutoSpeed/AutoSpeed.csproj
create mode 100644 Mods/AutoSpeed/AutoSpeed/AutoSpeed.cs
create mode 100644 Mods/AutoSpeed/AutoSpeed/Framework/ModConfig.cs
create mode 100644 Mods/AutoSpeed/AutoSpeed/ModConfig.cs
create mode 100644 Mods/AutoSpeed/AutoSpeed/ModEntry.cs
create mode 100644 Mods/AutoSpeed/AutoSpeed/README.md
create mode 100644 Mods/AutoSpeed/AutoSpeed/manifest.json
create mode 100644 Mods/AutoSpeed/Properties/AssemblyInfo.cs
create mode 100644 Mods/Automate/Automate.csproj
create mode 100644 Mods/Automate/Automate/Framework/AutomateAPI.cs
create mode 100644 Mods/Automate/Automate/Framework/AutomationFactory.cs
create mode 100644 Mods/Automate/Automate/Framework/BaseMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Connector.cs
create mode 100644 Mods/Automate/Automate/Framework/Consumable.cs
create mode 100644 Mods/Automate/Automate/Framework/GenericObjectMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/MachineGroup.cs
create mode 100644 Mods/Automate/Automate/Framework/MachineGroupBuilder.cs
create mode 100644 Mods/Automate/Automate/Framework/MachineGroupFactory.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Buildings/JunimoHutMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Buildings/MillMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/AutoGrabberMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/BeeHouseMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/CaskMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/CharcoalKilnMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/CheesePressMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/CoopIncubatorMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/CrabPotMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/CrystalariumMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/FeedHopperMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/FurnaceMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/KegMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/LightningRodMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/LoomMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/MayonnaiseMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/MushroomBoxMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/OilMakerMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/PreservesJarMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/RecyclingMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/SeedMakerMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/SlimeEggPressMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/SlimeIncubatorMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/SodaMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/StatueOfEndlessFortuneMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/StatueOfPerfectionMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/TapperMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Objects/WormBinMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/TerrainFeatures/FruitTreeMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Machines/Tiles/TrashCanMachine.cs
create mode 100644 Mods/Automate/Automate/Framework/Models/ModConfig.cs
create mode 100644 Mods/Automate/Automate/Framework/Models/ModConfigObject.cs
create mode 100644 Mods/Automate/Automate/Framework/Models/ObjectType.cs
create mode 100644 Mods/Automate/Automate/Framework/OverlayMenu.cs
create mode 100644 Mods/Automate/Automate/Framework/Recipe.cs
create mode 100644 Mods/Automate/Automate/Framework/Storage/ChestContainer.cs
create mode 100644 Mods/Automate/Automate/Framework/Storage/ContainerExtensions.cs
create mode 100644 Mods/Automate/Automate/Framework/StorageManager.cs
create mode 100644 Mods/Automate/Automate/IAutomatable.cs
create mode 100644 Mods/Automate/Automate/IAutomateAPI.cs
create mode 100644 Mods/Automate/Automate/IAutomationFactory.cs
create mode 100644 Mods/Automate/Automate/IConsumable.cs
create mode 100644 Mods/Automate/Automate/IContainer.cs
create mode 100644 Mods/Automate/Automate/IMachine.cs
create mode 100644 Mods/Automate/Automate/IRecipe.cs
create mode 100644 Mods/Automate/Automate/IStorage.cs
create mode 100644 Mods/Automate/Automate/ITrackedStack.cs
create mode 100644 Mods/Automate/Automate/MachineState.cs
create mode 100644 Mods/Automate/Automate/ModEntry.cs
create mode 100644 Mods/Automate/Automate/TrackedItem.cs
create mode 100644 Mods/Automate/Automate/TrackedItemCollection.cs
create mode 100644 Mods/Automate/Automate/screenshots/chests-anywhere-config.png
create mode 100644 Mods/Automate/Automate/screenshots/connectors.png
create mode 100644 Mods/Automate/Automate/screenshots/crab-pot-factory.png
create mode 100644 Mods/Automate/Automate/screenshots/example-overlay.png
create mode 100644 Mods/Automate/Automate/screenshots/extensibility-machine-groups.png
create mode 100644 Mods/Automate/Automate/screenshots/iridium-bar-factory.png
create mode 100644 Mods/Automate/Automate/screenshots/iridium-cheese-factory.png
create mode 100644 Mods/Automate/Automate/screenshots/iridium-mead-factory.png
create mode 100644 Mods/Automate/Automate/screenshots/refined-quartz-factory.png
create mode 100644 Mods/Automate/Common/CommonHelper.cs
create mode 100644 Mods/Automate/Common/DataParsers/CropDataParser.cs
create mode 100644 Mods/Automate/Common/Integrations/Automate/AutomateIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/Automate/IAutomateApi.cs
create mode 100644 Mods/Automate/Common/Integrations/BaseIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/BetterJunimos/BetterJunimosIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/BetterJunimos/IBetterJunimosApi.cs
create mode 100644 Mods/Automate/Common/Integrations/BetterSprinklers/BetterSprinklersIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/BetterSprinklers/IBetterSprinklersApi.cs
create mode 100644 Mods/Automate/Common/Integrations/Cobalt/CobaltIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/Cobalt/ICobaltApi.cs
create mode 100644 Mods/Automate/Common/Integrations/CustomFarmingRedux/CustomFarmingReduxIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/CustomFarmingRedux/ICustomFarmingApi.cs
create mode 100644 Mods/Automate/Common/Integrations/FarmExpansion/FarmExpansionIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/FarmExpansion/IFarmExpansionApi.cs
create mode 100644 Mods/Automate/Common/Integrations/IModIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/LineSprinklers/ILineSprinklersApi.cs
create mode 100644 Mods/Automate/Common/Integrations/LineSprinklers/LineSprinklersIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/PelicanFiber/PelicanFiberIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/PrismaticTools/IPrismaticToolsApi.cs
create mode 100644 Mods/Automate/Common/Integrations/PrismaticTools/PrismaticToolsIntegration.cs
create mode 100644 Mods/Automate/Common/Integrations/SimpleSprinkler/ISimplerSprinklerApi.cs
create mode 100644 Mods/Automate/Common/Integrations/SimpleSprinkler/SimpleSprinklerIntegration.cs
create mode 100644 Mods/Automate/Common/PathUtilities.cs
create mode 100644 Mods/Automate/Common/SpriteInfo.cs
create mode 100644 Mods/Automate/Common/StringEnumArrayConverter.cs
create mode 100644 Mods/Automate/Common/TileHelper.cs
create mode 100644 Mods/Automate/Common/UI/BaseOverlay.cs
create mode 100644 Mods/Automate/Common/UI/CommonSprites.cs
create mode 100644 Mods/Automate/Common/Utilities/ConstraintSet.cs
create mode 100644 Mods/Automate/Common/Utilities/InvariantDictionary.cs
create mode 100644 Mods/Automate/Common/Utilities/InvariantHashSet.cs
create mode 100644 Mods/Automate/Common/Utilities/ObjectReferenceComparer.cs
create mode 100644 Mods/Automate/Properties/AssemblyInfo.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/CategorizeChestsModule.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ChestData.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ChestDataManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ChestExtension.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ChestFinder.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/DiscoveredItem.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/IChestDataManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/IChestFiller.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/IChestFinder.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/IItemDataManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ItemBlacklist.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ItemDataManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ItemKey.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ItemNotImplementedException.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/ItemType.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/ChestAddress.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/ChestEntry.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/ChestLocationType.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/ISaveManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/InvalidSaveDataException.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/SaveData.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Persistence/SaveManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Framework/Saver.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/ITooltipManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/InterfaceHost.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/NineSlice.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Sprites.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/TextureRegion.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/TooltipManager.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/WidgetHost.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/Background.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/Button.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/CategoryMenu.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/ChestOverlay.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/ItemToggle.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/ItemTooltip.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/Label.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/LabeledCheckbox.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/SpriteButton.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/Stamp.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/TextButton.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/Widget.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Interface/Widgets/WrapBag.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/ItemHelper.cs
create mode 100644 Mods/ConvenientChests/CategorizeChests/Utility.cs
create mode 100644 Mods/ConvenientChests/Config.cs
create mode 100644 Mods/ConvenientChests/ConvenientChests.csproj
create mode 100644 Mods/ConvenientChests/ModEntry.cs
create mode 100644 Mods/ConvenientChests/Module.cs
create mode 100644 Mods/ConvenientChests/Properties/AssemblyInfo.cs
create mode 100644 Mods/ConvenientChests/StackToNearbyChests/StackLogic.cs
create mode 100644 Mods/ConvenientChests/StackToNearbyChests/StashToNearbyChestsModule.cs
create mode 100644 Mods/Mods.sln
create mode 100644 Mods/ScytheHarvesting/ModConfig.cs
create mode 100644 Mods/ScytheHarvesting/Properties/AssemblyInfo.cs
create mode 100644 Mods/ScytheHarvesting/ScytheHarvesting.cs
create mode 100644 Mods/ScytheHarvesting/ScytheHarvesting.csproj
create mode 100644 Mods/SkullCavernElevator/ModConfig.cs
create mode 100644 Mods/SkullCavernElevator/ModEntry.cs
create mode 100644 Mods/SkullCavernElevator/MyElevatorMenuWithScrollbar.cs
create mode 100644 Mods/SkullCavernElevator/Properties/AssemblyInfo.cs
create mode 100644 Mods/SkullCavernElevator/SkullCavernElevator.csproj
create mode 100644 Mods/SkullCavernElevator/SkullCavernElevator/MyElevatorMenu.cs
create mode 100644 Mods/TimeSpeed/Framework/LocationType.cs
create mode 100644 Mods/TimeSpeed/Framework/ModConfig.cs
create mode 100644 Mods/TimeSpeed/Framework/ModControlsConfig.cs
create mode 100644 Mods/TimeSpeed/Framework/Notifier.cs
create mode 100644 Mods/TimeSpeed/Framework/TickProgressChangedEventArgs.cs
create mode 100644 Mods/TimeSpeed/Framework/TimeHelper.cs
create mode 100644 Mods/TimeSpeed/ModEntry.cs
create mode 100644 Mods/TimeSpeed/Properties/AssemblyInfo.cs
create mode 100644 Mods/TimeSpeed/TimeSpeed.csproj
create mode 100644 PatchStep.txt
create mode 160000 SMAPI
diff --git a/Mods/AutoFish/AutoFish.csproj b/Mods/AutoFish/AutoFish.csproj
new file mode 100644
index 000000000..bc33fadea
--- /dev/null
+++ b/Mods/AutoFish/AutoFish.csproj
@@ -0,0 +1,162 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {8B08A816-6125-4277-A9EE-CA6AF9E279FC}
+ Library
+ Properties
+ AutoFish
+ AutoFish
+ v4.5.2
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\assemblies\Mod.dll
+
+
+ ..\assemblies\StardewValley.dll
+
+
+ False
+ ..\assemblies\BmFont.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Expansion.Downloader.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Licensing.dll
+
+
+ False
+ ..\assemblies\Java.Interop.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Analytics.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Crashes.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Mono.Android.dll
+
+
+ False
+ ..\assemblies\Mono.Security.dll
+
+
+ False
+ ..\assemblies\MonoGame.Framework.dll
+
+
+ ..\assemblies\mscorlib.dll
+
+
+ ..\assemblies\System.dll
+
+
+ ..\assemblies\System.Xml
+
+
+ ..\assemblies\System.Net.Http
+
+
+ ..\assemblies\System.Runtime.Serialization
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Core.Common.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Annotations.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Compat.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Core.UI.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Core.Utils.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Fragment.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Media.Compat.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.v4.dll
+
+
+ False
+ ..\assemblies\xTile.dll
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mods/AutoFish/AutoFish/ModConfig.cs b/Mods/AutoFish/AutoFish/ModConfig.cs
new file mode 100644
index 000000000..3604f5cd4
--- /dev/null
+++ b/Mods/AutoFish/AutoFish/ModConfig.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AutoFish
+{
+ class ModConfig
+ {
+ public bool maxCastPower { get; set; } = true;
+ public bool autoHit { get; set; } = true;
+ public bool fastBite { get; set; } = false;
+ public bool catchTreasure { get; set; } = true;
+ }
+}
diff --git a/Mods/AutoFish/AutoFish/ModEntry.cs b/Mods/AutoFish/AutoFish/ModEntry.cs
new file mode 100644
index 000000000..0d33b57e5
--- /dev/null
+++ b/Mods/AutoFish/AutoFish/ModEntry.cs
@@ -0,0 +1,138 @@
+using SMDroid.Options;
+using StardewModdingAPI;
+using StardewModdingAPI.Events;
+using StardewValley;
+using StardewValley.Menus;
+using StardewValley.Tools;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AutoFish
+{
+ public class ModEntry : Mod
+ {
+ private ModConfig Config;
+ private bool catching = false;
+
+ public override void Entry(IModHelper helper)
+ {
+ this.Config = this.Helper.ReadConfig();
+ helper.Events.GameLoop.UpdateTicked += this.UpdateTick;
+ }
+ public override List GetConfigMenuItems()
+ {
+ List options = new List();
+ ModOptionsCheckbox _optionsCheckboxAutoHit = new ModOptionsCheckbox("自动起钩", 0x8765, delegate (bool value) {
+ this.Config.autoHit = value;
+ this.Helper.WriteConfig(this.Config);
+ }, -1, -1);
+ _optionsCheckboxAutoHit.isChecked = this.Config.autoHit;
+ options.Add(_optionsCheckboxAutoHit);
+ ModOptionsCheckbox _optionsCheckboxMaxCastPower = new ModOptionsCheckbox("最大抛竿", 0x8765, delegate (bool value) {
+ this.Config.maxCastPower = value;
+ this.Helper.WriteConfig(this.Config);
+ }, -1, -1);
+ _optionsCheckboxMaxCastPower.isChecked = this.Config.maxCastPower;
+ options.Add(_optionsCheckboxMaxCastPower);
+ ModOptionsCheckbox _optionsCheckboxFastBite = new ModOptionsCheckbox("快速咬钩", 0x8765, delegate (bool value) {
+ this.Config.fastBite = value;
+ this.Helper.WriteConfig(this.Config);
+ }, -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;
+ }
+
+
+ private void UpdateTick(object sender, EventArgs e)
+ {
+ if (Game1.player == null)
+ return;
+
+ if (Game1.player.CurrentTool is FishingRod)
+ {
+ FishingRod currentTool = Game1.player.CurrentTool as FishingRod;
+ if (this.Config.fastBite && currentTool.timeUntilFishingBite > 0)
+ currentTool.timeUntilFishingBite /= 2; // 快速咬钩
+
+ if (this.Config.autoHit && currentTool.isNibbling && !currentTool.isReeling && !currentTool.hit && !currentTool.pullingOutOfWater && !currentTool.fishCaught)
+ currentTool.DoFunction(Game1.player.currentLocation, 1, 1, 1, Game1.player); // 自动咬钩
+
+ if (this.Config.maxCastPower)
+ currentTool.castingPower = 1;
+ }
+
+ if (Game1.activeClickableMenu is BobberBar) // 自动小游戏
+ {
+ BobberBar bar = Game1.activeClickableMenu as BobberBar;
+ float barPos = this.Helper.Reflection.GetField(bar, "bobberBarPos").GetValue();
+ float barHeight = this.Helper.Reflection.GetField(bar, "bobberBarHeight").GetValue();
+ float fishPos = this.Helper.Reflection.GetField(bar, "bobberPosition").GetValue();
+ float treasurePos = this.Helper.Reflection.GetField(bar, "treasurePosition").GetValue();
+ float distanceFromCatching = this.Helper.Reflection.GetField(bar, "distanceFromCatching").GetValue();
+
+ bool treasureCaught = this.Helper.Reflection.GetField(bar, "treasureCaught").GetValue();
+ bool hasTreasure = this.Helper.Reflection.GetField(bar, "treasure").GetValue();
+ float treasureScale = this.Helper.Reflection.GetField(bar, "treasureScale").GetValue();
+ float bobberBarSpeed = this.Helper.Reflection.GetField(bar, "bobberBarSpeed").GetValue();
+ float barPosMax = 568 - barHeight;
+
+ float min = barPos + barHeight / 4,
+ max = barPos + barHeight / 1.5f;
+
+ if (this.Config.catchTreasure && hasTreasure && !treasureCaught && (distanceFromCatching > 0.75 || this.catching))
+ {
+ this.catching = true;
+ fishPos = treasurePos;
+ }
+ if (this.catching && distanceFromCatching < 0.15)
+ {
+ this.catching = false;
+ fishPos = this.Helper.Reflection.GetField(bar, "bobberPosition").GetValue();
+ }
+
+ if (fishPos < min)
+ {
+ bobberBarSpeed -= 0.35f + (min - fishPos) / 20;
+ this.Helper.Reflection.GetField(bar, "bobberBarSpeed").SetValue(bobberBarSpeed);
+ } else if (fishPos > max)
+ {
+ bobberBarSpeed += 0.35f + (fishPos - max) / 20;
+ this.Helper.Reflection.GetField(bar, "bobberBarSpeed").SetValue(bobberBarSpeed);
+ } else
+ {
+ float target = 0.1f;
+ if (bobberBarSpeed > target)
+ {
+ bobberBarSpeed -= 0.1f + (bobberBarSpeed - target) / 25;
+ if (barPos + bobberBarSpeed > barPosMax)
+ bobberBarSpeed /= 2; // 减小触底反弹
+ if (bobberBarSpeed < target)
+ bobberBarSpeed = target;
+ } else
+ {
+ bobberBarSpeed += 0.1f + (target - bobberBarSpeed) / 25;
+ if (barPos + bobberBarSpeed < 0)
+ bobberBarSpeed /= 2; // 减小触顶反弹
+ if (bobberBarSpeed > target)
+ bobberBarSpeed = target;
+ }
+ this.Helper.Reflection.GetField(bar, "bobberBarSpeed").SetValue(bobberBarSpeed);
+ }
+ }
+ else
+ {
+ this.catching = false;
+ }
+ }
+ }
+}
diff --git a/Mods/AutoFish/Properties/AssemblyInfo.cs b/Mods/AutoFish/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..322b9e55b
--- /dev/null
+++ b/Mods/AutoFish/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("AutoFish")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("AutoFish")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
+[assembly: Guid("8b08a816-6125-4277-a9ee-ca6af9e279fc")]
+
+// 程序集的版本信息由下列四个值组成:
+//
+// 主版本
+// 次版本
+// 生成号
+// 修订号
+//
+// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
+//通过使用 "*",如下所示:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Mods/AutoSpeed/AutoSpeed.csproj b/Mods/AutoSpeed/AutoSpeed.csproj
new file mode 100644
index 000000000..d29f361ca
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed.csproj
@@ -0,0 +1,175 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {5B089EEE-F22C-4753-B90D-16D4CD3F5D61}
+ Library
+ Properties
+ AutoSpeed
+ AutoSpeed
+ v4.6.1
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\assemblies\Mod.dll
+
+
+ ..\assemblies\StardewValley.dll
+
+
+ False
+ ..\assemblies\BmFont.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Expansion.Downloader.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Licensing.dll
+
+
+ False
+ ..\assemblies\Java.Interop.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Analytics.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Crashes.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Mono.Android.dll
+
+
+ False
+ ..\assemblies\Mono.Security.dll
+
+
+ False
+ ..\assemblies\MonoGame.Framework.dll
+
+
+ ..\assemblies\mscorlib.dll
+
+
+ ..\assemblies\System.dll
+
+
+ ..\assemblies\System.Xml
+
+
+ ..\assemblies\System.Net.Http
+
+
+ ..\assemblies\System.Runtime.Serialization
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Core.Common.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Annotations.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Compat.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Core.UI.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Core.Utils.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Fragment.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Media.Compat.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.v4.dll
+
+
+ False
+ ..\assemblies\xTile.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mods/AutoSpeed/AutoSpeed/AutoSpeed.cs b/Mods/AutoSpeed/AutoSpeed/AutoSpeed.cs
new file mode 100644
index 000000000..55ee33c66
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed/AutoSpeed.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using Omegasis.AutoSpeed.Framework;
+using SMDroid.Options;
+using StardewModdingAPI;
+using StardewModdingAPI.Events;
+using StardewValley;
+using StardewValley.Menus;
+
+namespace Omegasis.AutoSpeed
+{
+ /// The mod entry point.
+ public class AutoSpeed : Mod
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod configuration.
+ private ModConfig Config;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// The mod entry point, called after the mod is first loaded.
+ /// Provides simplified APIs for writing mods.
+ public override void Entry(IModHelper helper)
+ {
+ helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
+ this.Config = helper.ReadConfig();
+ }
+
+ public override List GetConfigMenuItems()
+ {
+ List options = new List();
+ ModOptionsSlider _optionsSliderSpeed = new ModOptionsSlider("移动加速", 0x8765, delegate (int value) {
+ this.Config.Speed = value;
+ this.Helper.WriteConfig(this.Config);
+ }, -1, -1);
+ _optionsSliderSpeed.sliderMinValue = 0;
+ _optionsSliderSpeed.sliderMaxValue = 10;
+ _optionsSliderSpeed.value = this.Config.Speed;
+ options.Add(_optionsSliderSpeed);
+ return options;
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// Raised after the game state is updated (≈60 times per second).
+ /// The event sender.
+ /// The event arguments.
+ private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
+ {
+ if (Context.IsPlayerFree)
+ Game1.player.addedSpeed = this.Config.Speed;
+ }
+ }
+}
diff --git a/Mods/AutoSpeed/AutoSpeed/Framework/ModConfig.cs b/Mods/AutoSpeed/AutoSpeed/Framework/ModConfig.cs
new file mode 100644
index 000000000..2e6dc10fb
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed/Framework/ModConfig.cs
@@ -0,0 +1,9 @@
+namespace Omegasis.AutoSpeed.Framework
+{
+ /// The mod configuration.
+ internal class ModConfig
+ {
+ /// The added speed.
+ public int Speed { get; set; } = 5;
+ }
+}
diff --git a/Mods/AutoSpeed/AutoSpeed/ModConfig.cs b/Mods/AutoSpeed/AutoSpeed/ModConfig.cs
new file mode 100644
index 000000000..b1f5b73b5
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed/ModConfig.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace AutoSpeed
+{
+ class ModConfig
+ {
+ /// The added speed.
+ public int Speed { get; set; } = 5;
+ }
+}
diff --git a/Mods/AutoSpeed/AutoSpeed/ModEntry.cs b/Mods/AutoSpeed/AutoSpeed/ModEntry.cs
new file mode 100644
index 000000000..818ba5ea5
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed/ModEntry.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using StardewModdingAPI;
+using StardewModdingAPI.Events;
+using StardewValley;
+using StardewValley.Menus;
+using SMDroid.Options;
+
+namespace AutoSpeed
+{
+ /// The mod entry point.
+ class ModEntry : StardewModdingAPI.Mod
+ {
+ /// The mod configuration.
+ private ModConfig Config;
+
+ /*********
+ ** Public methods
+ *********/
+ /// The mod entry point, called after the mod is first loaded.
+ /// Provides simplified APIs for writing mods.
+ public override void Entry(IModHelper helper)
+ {
+ this.Config = helper.ReadConfig();
+ helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
+ }
+
+ public override List GetConfigMenuItems()
+ {
+ List options = new List();
+ ModOptionsSlider _optionsSliderSpeed = new ModOptionsSlider("移动加速", 0x8765, delegate (int value) {
+ Config.Speed = value;
+ Helper.WriteConfig(Config);
+ }, -1, -1);
+ _optionsSliderSpeed.sliderMinValue = 0;
+ _optionsSliderSpeed.sliderMaxValue = 10;
+ _optionsSliderSpeed.value = Config.Speed;
+ options.Add(_optionsSliderSpeed);
+ return options;
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// Raised after the game state is updated (≈60 times per second).
+ /// The event sender.
+ /// The event arguments.
+ private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
+ {
+ if (Context.IsPlayerFree)
+ Game1.player.addedSpeed = Config.Speed;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Mods/AutoSpeed/AutoSpeed/README.md b/Mods/AutoSpeed/AutoSpeed/README.md
new file mode 100644
index 000000000..50f2e1051
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed/README.md
@@ -0,0 +1,31 @@
+**Auto Speed** is a [Stardew Valley](http://stardewvalley.net/) mod which lets you move faster
+without the need to enter commands in the console.
+
+Compatible with Stardew Valley 1.2+ on Linux, Mac, and Windows.
+
+## Installation
+1. [Install the latest version of SMAPI](https://github.com/Pathoschild/SMAPI/releases).
+2. Install [this mod from Nexus mods](http://www.nexusmods.com/stardewvalley/mods/443).
+3. Run the game using SMAPI.
+
+## Usage
+Launch the game with the mod installed to generate the config file, then edit the `config.json` to
+set the speed you want (higher values are faster).
+
+## Versions
+1.0
+* Initial release.
+
+1.1:
+* Updated to Stardew Valley 1.1 and SMAPI 0.40 1.1-3.
+
+1.3:
+* Updated to Stardew Valley 1.2 and SMAPI 1.12.
+
+1.4:
+* Switched to standard JSON config file.
+* Fixed config defaulting to normal speed.
+* Internal refactoring.
+
+1.4.1:
+* Enabled update checks in SMAPI 2.0+.
diff --git a/Mods/AutoSpeed/AutoSpeed/manifest.json b/Mods/AutoSpeed/AutoSpeed/manifest.json
new file mode 100644
index 000000000..b5c3f9e33
--- /dev/null
+++ b/Mods/AutoSpeed/AutoSpeed/manifest.json
@@ -0,0 +1,10 @@
+{
+ "Name": "Auto Speed",
+ "Author": "Alpha_Omegasis",
+ "Version": "1.8.0",
+ "Description": "Got to go fast!",
+ "UniqueID": "Omegasis.AutoSpeed",
+ "EntryDll": "AutoSpeed.dll",
+ "MinimumApiVersion": "2.10.1",
+ "UpdateKeys": [ "Nexus:443" ]
+}
diff --git a/Mods/AutoSpeed/Properties/AssemblyInfo.cs b/Mods/AutoSpeed/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..1907a05dd
--- /dev/null
+++ b/Mods/AutoSpeed/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("AutoSpeed")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("AutoSpeed")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
+[assembly: Guid("5b089eee-f22c-4753-b90d-16d4cd3f5d61")]
+
+// 程序集的版本信息由下列四个值组成:
+//
+// 主版本
+// 次版本
+// 生成号
+// 修订号
+//
+// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
+//通过使用 "*",如下所示:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Mods/Automate/Automate.csproj b/Mods/Automate/Automate.csproj
new file mode 100644
index 000000000..295083d6f
--- /dev/null
+++ b/Mods/Automate/Automate.csproj
@@ -0,0 +1,277 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {5EF944E3-D54B-4936-B507-A40C17B17B8E}
+ Library
+ Properties
+ Automate
+ Automate
+ v4.5.2
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ 7.2
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ 7.2
+
+
+
+ ..\assemblies\Mod.dll
+
+
+ ..\assemblies\StardewValley.dll
+
+
+ False
+ ..\assemblies\BmFont.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Expansion.Downloader.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll
+
+
+ False
+ ..\assemblies\Google.Android.Vending.Licensing.dll
+
+
+ False
+ ..\assemblies\Java.Interop.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Analytics.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Crashes.dll
+
+
+ False
+ ..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll
+
+
+ False
+ ..\assemblies\Mono.Android.dll
+
+
+ False
+ ..\assemblies\Mono.Security.dll
+
+
+ False
+ ..\assemblies\MonoGame.Framework.dll
+
+
+ ..\assemblies\mscorlib.dll
+
+
+ ..\assemblies\System.dll
+
+
+ ..\assemblies\System.Xml
+
+
+ ..\assemblies\System.Net.Http
+
+
+ ..\assemblies\System.Runtime.Serialization
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Core.Common.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Annotations.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Compat.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Core.UI.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Core.Utils.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Fragment.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.Media.Compat.dll
+
+
+ False
+ ..\assemblies\Xamarin.Android.Support.v4.dll
+
+
+ False
+ ..\assemblies\xTile.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mods/Automate/Automate/Framework/AutomateAPI.cs b/Mods/Automate/Automate/Framework/AutomateAPI.cs
new file mode 100644
index 000000000..0508d2490
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/AutomateAPI.cs
@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Pathoschild.Stardew.Common;
+using StardewModdingAPI;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// The API which lets other mods interact with Automate.
+ public class AutomateAPI : IAutomateAPI
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Encapsulates monitoring and logging.
+ private readonly IMonitor Monitor;
+
+ /// Constructs machine groups.
+ private readonly MachineGroupFactory MachineGroupFactory;
+
+ /// The active machine groups recognised by Automate.
+ private readonly IDictionary ActiveMachineGroups;
+
+ /// The disabled machine groups recognised by Automate (e.g. machines not connected to a chest).
+ private readonly IDictionary DisabledMachineGroups;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// Encapsulates monitoring and logging.
+ /// Constructs machine groups.
+ /// The active machine groups recognised by Automate.
+ /// The disabled machine groups recognised by Automate (e.g. machines not connected to a chest).
+ internal AutomateAPI(IMonitor monitor, MachineGroupFactory machineGroupFactory, IDictionary activeMachineGroups, IDictionary disabledMachineGroups)
+ {
+ this.Monitor = monitor;
+ this.MachineGroupFactory = machineGroupFactory;
+ this.ActiveMachineGroups = activeMachineGroups;
+ this.DisabledMachineGroups = disabledMachineGroups;
+ }
+
+ /// Add an automation factory.
+ /// An automation factory which construct machines, containers, and connectors.
+ public void AddFactory(IAutomationFactory factory)
+ {
+ this.Monitor.Log($"Adding automation factory: {factory.GetType().AssemblyQualifiedName}", LogLevel.Trace);
+ this.MachineGroupFactory.Add(factory);
+ }
+
+ /// 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)
+ {
+ IDictionary data = new Dictionary();
+ foreach (IMachine machine in this.GetMachineGroups(location).SelectMany(group => group.Machines))
+ {
+ if (machine.TileArea.Intersects(tileArea))
+ {
+ int state = (int)machine.GetState();
+ foreach (Vector2 tile in machine.TileArea.GetTiles())
+ {
+ if (tileArea.Contains((int)tile.X, (int)tile.Y))
+ data[tile] = state;
+ }
+ }
+ }
+
+ return data;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get all machines in a location.
+ /// The location whose maches to fetch.
+ private IEnumerable GetMachineGroups(GameLocation location)
+ {
+ // active groups
+ if (this.ActiveMachineGroups.TryGetValue(location, out MachineGroup[] activeGroups))
+ {
+ foreach (MachineGroup machineGroup in activeGroups)
+ yield return machineGroup;
+ }
+
+ // disabled groups
+ if (this.DisabledMachineGroups.TryGetValue(location, out MachineGroup[] disabledGroups))
+ {
+ foreach (MachineGroup machineGroup in disabledGroups)
+ yield return machineGroup;
+ }
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/AutomationFactory.cs b/Mods/Automate/Automate/Framework/AutomationFactory.cs
new file mode 100644
index 000000000..613bb4b12
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/AutomationFactory.cs
@@ -0,0 +1,247 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Pathoschild.Stardew.Automate.Framework.Machines.Buildings;
+using Pathoschild.Stardew.Automate.Framework.Machines.Objects;
+using Pathoschild.Stardew.Automate.Framework.Machines.TerrainFeatures;
+using Pathoschild.Stardew.Automate.Framework.Machines.Tiles;
+using Pathoschild.Stardew.Automate.Framework.Models;
+using Pathoschild.Stardew.Automate.Framework.Storage;
+using StardewModdingAPI;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Locations;
+using StardewValley.Objects;
+using StardewValley.TerrainFeatures;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// Constructs machines, containers, or connectors which can be added to a machine group.
+ internal class AutomationFactory : IAutomationFactory
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Encapsulates monitoring and logging.
+ private readonly IMonitor Monitor;
+
+ /// Simplifies access to private game code.
+ private readonly IReflectionHelper Reflection;
+
+ /// The object IDs through which machines can connect, but which have no other automation properties.
+ private readonly IDictionary> Connectors;
+
+ /// Whether to treat the shipping bin as a machine that can be automated.
+ private readonly bool AutomateShippingBin;
+
+ /// The tile area on the farm matching the shipping bin.
+ private readonly Rectangle ShippingBinArea = new Rectangle(71, 14, 2, 1);
+
+ /// Whether the Better Junimos mod is installed.
+ private readonly bool HasBetterJunimos;
+
+ /// Whether the Deluxe Auto-Grabber mod is installed.
+ private readonly bool HasDeluxeAutoGrabber;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The objects through which machines can connect, but which have no other automation properties.
+ /// Whether to treat the shipping bin as a machine that can be automated.
+ /// Encapsulates monitoring and logging.
+ /// Simplifies access to private game code.
+ /// Whether the Better Junimos mod is installed.
+ /// Whether the Deluxe Auto-Grabber mod is installed.
+ public AutomationFactory(ModConfigObject[] connectors, bool automateShippingBin, IMonitor monitor, IReflectionHelper reflection, bool hasBetterJunimos, bool hasDeluxeAutoGrabber)
+ {
+ this.Connectors = connectors
+ .GroupBy(connector => connector.Type)
+ .ToDictionary(group => group.Key, group => new HashSet(group.Select(p => p.ID)));
+ this.AutomateShippingBin = automateShippingBin;
+ this.Monitor = monitor;
+ this.Reflection = reflection;
+ this.HasBetterJunimos = hasBetterJunimos;
+ this.HasDeluxeAutoGrabber = hasDeluxeAutoGrabber;
+ }
+
+ /// Get a machine, container, or connector instance for a given object.
+ /// The in-game object.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ public IAutomatable GetFor(SObject obj, GameLocation location, in Vector2 tile)
+ {
+ // chest container
+ if (obj is Chest chest)
+ return new ChestContainer(chest, location, tile);
+
+ // machine
+ if (obj.ParentSheetIndex == 165)
+ return new AutoGrabberMachine(obj, location, tile, ignoreSeedOutput: this.HasDeluxeAutoGrabber);
+ if (obj.name == "Bee House")
+ return new BeeHouseMachine(obj, location, tile);
+ if (obj is Cask cask)
+ return new CaskMachine(cask, location, tile);
+ if (obj.name == "Charcoal Kiln")
+ return new CharcoalKilnMachine(obj, location, tile);
+ if (obj.name == "Cheese Press")
+ return new CheesePressMachine(obj, location, tile);
+ if (obj is CrabPot pot)
+ return new CrabPotMachine(pot, location, tile, this.Monitor, this.Reflection);
+ if (obj.Name == "Crystalarium")
+ return new CrystalariumMachine(obj, location, tile, this.Reflection);
+ if (obj.name == "Feed Hopper")
+ return new FeedHopperMachine(location, tile);
+ if (obj.Name == "Furnace")
+ return new FurnaceMachine(obj, location, tile);
+ if (obj.name == "Incubator")
+ return new CoopIncubatorMachine(obj, location, tile);
+ if (obj.Name == "Keg")
+ return new KegMachine(obj, location, tile);
+ if (obj.name == "Lightning Rod")
+ return new LightningRodMachine(obj, location, tile);
+ if (obj.name == "Loom")
+ return new LoomMachine(obj, location, tile);
+ if (obj.name == "Mayonnaise Machine")
+ return new MayonnaiseMachine(obj, location, tile);
+ if (obj.Name == "Mushroom Box")
+ return new MushroomBoxMachine(obj, location, tile);
+ if (obj.name == "Oil Maker")
+ return new OilMakerMachine(obj, location, tile);
+ if (obj.name == "Preserves Jar")
+ return new PreservesJarMachine(obj, location, tile);
+ if (obj.name == "Recycling Machine")
+ return new RecyclingMachine(obj, location, tile);
+ if (obj.name == "Seed Maker")
+ return new SeedMakerMachine(obj, location, tile);
+ if (obj.name == "Slime Egg-Press")
+ return new SlimeEggPressMachine(obj, location, tile);
+ if (obj.name == "Slime Incubator")
+ return new SlimeIncubatorMachine(obj, location, tile);
+ if (obj.name == "Soda Machine")
+ return new SodaMachine(obj, location, tile);
+ if (obj.name == "Statue Of Endless Fortune")
+ return new StatueOfEndlessFortuneMachine(obj, location, tile);
+ if (obj.name == "Statue Of Perfection")
+ return new StatueOfPerfectionMachine(obj, location, tile);
+ if (obj.name == "Tapper")
+ {
+ if (location.terrainFeatures.TryGetValue(tile, out TerrainFeature terrainFeature) && terrainFeature is Tree tree)
+ return new TapperMachine(obj, location, tile, tree.treeType.Value);
+ }
+ if (obj.name == "Worm Bin")
+ return new WormBinMachine(obj, location, tile);
+
+ // connector
+ if (this.IsConnector(obj.bigCraftable.Value ? ObjectType.BigCraftable : ObjectType.Object, this.GetItemID(obj)))
+ return new Connector(location, tile);
+
+ return null;
+ }
+
+ /// Get a machine, container, or connector instance for a given terrain feature.
+ /// The terrain feature.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ public IAutomatable GetFor(TerrainFeature feature, GameLocation location, in Vector2 tile)
+ {
+ // machine
+ if (feature is FruitTree fruitTree)
+ return new FruitTreeMachine(fruitTree, location, tile);
+
+ // connector
+ if (feature is Flooring floor && this.IsConnector(ObjectType.Floor, floor.whichFloor.Value))
+ return new Connector(location, tile);
+
+ return null;
+ }
+
+ /// Get a machine, container, or connector instance for a given building.
+ /// The building.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ public IAutomatable GetFor(Building building, BuildableGameLocation location, in Vector2 tile)
+ {
+ // machine
+ if (building is JunimoHut hut)
+ return new JunimoHutMachine(hut, location, ignoreSeedOutput: this.HasBetterJunimos);
+ if (building is Mill mill)
+ return new MillMachine(mill, location);
+ if (this.AutomateShippingBin && building is ShippingBin bin)
+ return new ShippingBinMachine(bin, location, Game1.getFarm());
+ if (building.buildingType.Value == "Silo")
+ return new FeedHopperMachine(building, location);
+ return null;
+ }
+
+ /// Get a machine, container, or connector instance for a given tile position.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ /// Shipping bin logic from , garbage can logic from .
+ public IAutomatable GetForTile(GameLocation location, in Vector2 tile)
+ {
+ // shipping bin
+ if (this.AutomateShippingBin && location is Farm farm && (int)tile.X == this.ShippingBinArea.X && (int)tile.Y == this.ShippingBinArea.Y)
+ {
+ return new ShippingBinMachine(farm, this.ShippingBinArea);
+ }
+
+ // garbage can
+ if (location is Town town)
+ {
+ string action = town.doesTileHaveProperty((int)tile.X, (int)tile.Y, "Action", "Buildings");
+ if (!string.IsNullOrWhiteSpace(action) && action.StartsWith("Garbage ") && int.TryParse(action.Split(' ')[1], out int trashCanIndex))
+ return new TrashCanMachine(town, tile, trashCanIndex, this.Reflection);
+ }
+
+ return null;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get whether a given object should be treated as a connector.
+ /// The object type.
+ /// The object iD.
+ private bool IsConnector(ObjectType type, int id)
+ {
+ return
+ this.Connectors.Count != 0
+ && this.Connectors.TryGetValue(type, out HashSet ids)
+ && ids.Contains(id);
+ }
+
+ /// Get the object ID for a given object.
+ /// The object instance.
+ private int GetItemID(SObject obj)
+ {
+ // get object ID from fence ID
+ if (obj is Fence fence)
+ {
+ if (fence.isGate.Value)
+ return 325;
+ switch (fence.whichType.Value)
+ {
+ case Fence.wood:
+ return 322;
+ case Fence.stone:
+ return 323;
+ case Fence.steel:
+ return 324;
+ case Fence.gold:
+ return 298;
+ }
+ }
+
+ // else obj ID
+ return obj.ParentSheetIndex;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/BaseMachine.cs b/Mods/Automate/Automate/Framework/BaseMachine.cs
new file mode 100644
index 000000000..485b4ade8
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/BaseMachine.cs
@@ -0,0 +1,90 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Buildings;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// The base implementation for a machine.
+ internal abstract class BaseMachine : IMachine
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// A unique ID for the machine type.
+ /// This value should be identical for two machines if they have the exact same behavior and input logic. For example, if one machine in a group can't process input due to missing items, Automate will skip any other empty machines of that type in the same group since it assumes they need the same inputs.
+ public string MachineTypeID { get; protected set; }
+
+ /// The location which contains the machine.
+ public GameLocation Location { get; }
+
+ /// The tile area covered by the machine.
+ public Rectangle TileArea { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Get the machine's processing state.
+ public abstract MachineState GetState();
+
+ /// Get the output item.
+ public abstract ITrackedStack GetOutput();
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public abstract bool SetInput(IStorage input);
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// Construct an instance.
+ /// The machine's in-game location.
+ /// The tile area covered by the machine.
+ protected BaseMachine(GameLocation location, in Rectangle tileArea)
+ {
+ this.MachineTypeID = this.GetType().FullName;
+ this.Location = location;
+ this.TileArea = tileArea;
+ }
+
+ /// Get the tile area for a building.
+ /// The building.
+ protected static Rectangle GetTileAreaFor(Building building)
+ {
+ return new Rectangle(building.tileX.Value, building.tileY.Value, building.tilesWide.Value, building.tilesHigh.Value);
+ }
+
+ /// Get the tile area for a placed object.
+ /// The tile position.
+ protected static Rectangle GetTileAreaFor(in Vector2 tile)
+ {
+ return new Rectangle((int)tile.X, (int)tile.Y, 1, 1);
+ }
+ }
+
+ /// The base implementation for a machine.
+ internal abstract class BaseMachine : BaseMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The underlying entity automated by this machine. This is only stored for the machine instance, and can be null if not applicable.
+ protected TMachine Machine { get; }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// Construct an instance.
+ /// The underlying entity automated by this machine. This is only stored for the machine instance, and can be null if not applicable.
+ /// The machine's in-game location.
+ /// The tile area covered by the machine.
+ protected BaseMachine(TMachine machine, GameLocation location, in Rectangle tileArea)
+ : base(location, tileArea)
+ {
+ this.Machine = machine;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Connector.cs b/Mods/Automate/Automate/Framework/Connector.cs
new file mode 100644
index 000000000..d92b51ab9
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Connector.cs
@@ -0,0 +1,37 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// An entity which connects machines and chests in a machine group, but otherwise has no logic of its own.
+ internal class Connector : IAutomatable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The location which contains the machine.
+ public GameLocation Location { get; }
+
+ /// The tile area covered by the machine.
+ public Rectangle TileArea { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The location which contains the machine.
+ /// The tile area covered by the machine.
+ public Connector(GameLocation location, Rectangle tileArea)
+ {
+ this.Location = location;
+ this.TileArea = tileArea;
+ }
+
+ /// Construct an instance.
+ /// The location which contains the machine.
+ /// The tile covered by the machine.
+ public Connector(GameLocation location, Vector2 tile)
+ : this(location, new Rectangle((int)tile.X, (int)tile.Y, 1, 1)) { }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Consumable.cs b/Mods/Automate/Automate/Framework/Consumable.cs
new file mode 100644
index 000000000..d2135e576
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Consumable.cs
@@ -0,0 +1,50 @@
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// An ingredient stack (or stacks) which can be consumed by a machine.
+ internal class Consumable : IConsumable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The items available to consumable.
+ public ITrackedStack Consumables { get; }
+
+ /// A sample item for comparison.
+ /// This should not be a reference to the original stack.
+ public Item Sample => this.Consumables.Sample;
+
+ /// The number of items needed for the recipe.
+ public int CountNeeded { get; }
+
+ /// Whether the consumables needed for this requirement are ready.
+ public bool IsMet { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The matching items available to consume.
+ /// The number of items needed for the recipe.
+ public Consumable(ITrackedStack consumables, int countNeeded)
+ {
+ this.Consumables = consumables;
+ this.CountNeeded = countNeeded;
+ this.IsMet = consumables.Count >= countNeeded;
+ }
+
+ /// Remove the needed number of this item from the stack.
+ public void Reduce()
+ {
+ this.Consumables.Reduce(this.CountNeeded);
+ }
+
+ /// Remove the needed number of this item from the stack and return a new stack matching the count.
+ public Item Take()
+ {
+ return this.Consumables.Take(this.CountNeeded);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/GenericObjectMachine.cs b/Mods/Automate/Automate/Framework/GenericObjectMachine.cs
new file mode 100644
index 000000000..0992709fa
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/GenericObjectMachine.cs
@@ -0,0 +1,63 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// A generic machine instance.
+ internal abstract class GenericObjectMachine : BaseMachine where TMachine : SObject
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.Machine.heldObject.Value == null)
+ return MachineState.Empty;
+
+ return this.Machine.readyForHarvest.Value
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ return new TrackedItem(this.Machine.heldObject.Value, onEmpty: this.GenericReset);
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The in-game location.
+ /// The tile covered by the machine.
+ protected GenericObjectMachine(TMachine machine, GameLocation location, Vector2 tile)
+ : base(machine, location, BaseMachine.GetTileAreaFor(tile)) { }
+
+ /// Reset the machine so it's ready to accept a new input.
+ /// The output item that was taken.
+ protected void GenericReset(Item item)
+ {
+ this.Machine.heldObject.Value = null;
+ this.Machine.readyForHarvest.Value = false;
+ }
+
+ /// Generic logic to pull items from storage based on the given recipes.
+ /// The available items.
+ /// The recipes to match.
+ protected bool GenericPullRecipe(IStorage storage, IRecipe[] recipes)
+ {
+ if (storage.TryGetIngredient(recipes, out IConsumable consumable, out IRecipe recipe))
+ {
+ this.Machine.heldObject.Value = recipe.Output(consumable.Take());
+ this.Machine.MinutesUntilReady = recipe.Minutes;
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/MachineGroup.cs b/Mods/Automate/Automate/Framework/MachineGroup.cs
new file mode 100644
index 000000000..2267974c8
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/MachineGroup.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// A collection of machines and storage which work as one unit.
+ internal class MachineGroup
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The location containing the group.
+ public GameLocation Location { get; }
+
+ /// The machines in the group.
+ public IMachine[] Machines { get; }
+
+ /// The containers in the group.
+ public IContainer[] Containers { get; }
+
+ /// The storage manager for the group.
+ public IStorage StorageManager { get; }
+
+ /// The tiles comprising the group.
+ public Vector2[] Tiles { get; }
+
+ /// Whether the group has the minimum requirements to enable internal automation (i.e., at least one chest and one machine).
+ public bool HasInternalAutomation => this.Machines.Length > 0 && this.Containers.Length > 0;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Create an instance.
+ /// The location containing the group.
+ /// The machines in the group.
+ /// The containers in the group.
+ /// The tiles comprising the group.
+ public MachineGroup(GameLocation location, IMachine[] machines, IContainer[] containers, Vector2[] tiles)
+ {
+ this.Location = location;
+ this.Machines = machines;
+ this.Containers = containers;
+ this.Tiles = tiles;
+ this.StorageManager = new StorageManager(containers);
+ }
+
+ /// Automate the machines inside the group.
+ public void Automate()
+ {
+ // get machines ready for input/output
+ IList outputReady = new List();
+ IList inputReady = new List();
+ foreach (IMachine machine in this.Machines)
+ {
+ switch (machine.GetState())
+ {
+ case MachineState.Done:
+ outputReady.Add(machine);
+ break;
+
+ case MachineState.Empty:
+ inputReady.Add(machine);
+ break;
+ }
+ }
+ if (!outputReady.Any() && !inputReady.Any())
+ return;
+
+ // process output
+ foreach (IMachine machine in outputReady)
+ {
+ if (this.StorageManager.TryPush(machine.GetOutput()) && machine.GetState() == MachineState.Empty)
+ inputReady.Add(machine);
+ }
+
+ // process input
+ HashSet ignoreMachines = new HashSet();
+ foreach (IMachine machine in inputReady)
+ {
+ if (ignoreMachines.Contains(machine.MachineTypeID))
+ continue;
+
+ if (!machine.SetInput(this.StorageManager))
+ ignoreMachines.Add(machine.MachineTypeID); // if the machine can't process available input, no need to ask every instance of its type
+ }
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/MachineGroupBuilder.cs b/Mods/Automate/Automate/Framework/MachineGroupBuilder.cs
new file mode 100644
index 000000000..6c62c42ef
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/MachineGroupBuilder.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Pathoschild.Stardew.Common;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// Handles logic for building a .
+ internal class MachineGroupBuilder
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The location containing the group.
+ private readonly GameLocation Location;
+
+ /// The machines in the group.
+ private readonly HashSet Machines = new HashSet();
+
+ /// The containers in the group.
+ private readonly HashSet Containers = new HashSet();
+
+ /// The tiles comprising the group.
+ private readonly HashSet Tiles = new HashSet();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// The tile areas added to the machine group since the queue was last cleared.
+ internal IList NewTileAreas { get; } = new List();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Create an instance.
+ /// The location containing the group.
+ public MachineGroupBuilder(GameLocation location)
+ {
+ this.Location = location;
+ }
+
+ /// Add a machine to the group.
+ /// The machine to add.
+ public void Add(IMachine machine)
+ {
+ this.Machines.Add(machine);
+ this.Add(machine.TileArea);
+ }
+
+ /// Add a container to the group.
+ /// The container to add.
+ public void Add(IContainer container)
+ {
+ this.Containers.Add(container);
+ this.Add(container.TileArea);
+ }
+
+ /// Add connector tiles to the group.
+ /// The tile area to add.
+ public void Add(Rectangle tileArea)
+ {
+ foreach (Vector2 tile in tileArea.GetTiles())
+ this.Tiles.Add(tile);
+ this.NewTileAreas.Add(tileArea);
+ }
+
+ /// Get whether any tiles were added to the builder.
+ public bool HasTiles()
+ {
+ return this.Tiles.Count > 0;
+ }
+
+ /// Create a group from the saved data.
+ public MachineGroup Build()
+ {
+ return new MachineGroup(this.Location, this.Machines.ToArray(), this.Containers.ToArray(), this.Tiles.ToArray());
+ }
+
+ /// Clear the saved data.
+ public void Reset()
+ {
+ this.Machines.Clear();
+ this.Containers.Clear();
+ this.Tiles.Clear();
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/MachineGroupFactory.cs b/Mods/Automate/Automate/Framework/MachineGroupFactory.cs
new file mode 100644
index 000000000..f8f8f4931
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/MachineGroupFactory.cs
@@ -0,0 +1,176 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Pathoschild.Stardew.Automate.Framework.Storage;
+using Pathoschild.Stardew.Common;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Locations;
+using StardewValley.TerrainFeatures;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// Constructs machine groups.
+ internal class MachineGroupFactory
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The automation factories which construct machines, containers, and connectors.
+ private readonly IList AutomationFactories = new List();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Add an automation factory.
+ /// An automation factory which construct machines, containers, and connectors.
+ public void Add(IAutomationFactory factory)
+ {
+ this.AutomationFactories.Add(factory);
+ }
+
+ /// Get all machine groups in a location.
+ /// The location to search.
+ public IEnumerable GetMachineGroups(GameLocation location)
+ {
+ MachineGroupBuilder builder = new MachineGroupBuilder(location);
+ ISet visited = new HashSet();
+ foreach (Vector2 tile in location.GetTiles())
+ {
+ this.FloodFillGroup(builder, location, tile, visited);
+ if (builder.HasTiles())
+ {
+ yield return builder.Build();
+ builder.Reset();
+ }
+ }
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Extend the given machine group to include all machines and containers connected to the given tile, if any.
+ /// The machine group to extend.
+ /// The location to search.
+ /// The first tile to check.
+ /// A lookup of visited tiles.
+ private void FloodFillGroup(MachineGroupBuilder machineGroup, GameLocation location, in Vector2 origin, ISet visited)
+ {
+ // skip if already visited
+ if (visited.Contains(origin))
+ return;
+
+ // flood-fill connected machines & containers
+ Queue queue = new Queue();
+ queue.Enqueue(origin);
+ while (queue.Any())
+ {
+ // get tile
+ Vector2 tile = queue.Dequeue();
+ if (!visited.Add(tile))
+ continue;
+
+ // add machines, containers, or connectors which covers this tile
+ if (this.TryAddEntity(machineGroup, location, tile))
+ {
+ foreach (Rectangle tileArea in machineGroup.NewTileAreas)
+ {
+ // mark visited
+ foreach (Vector2 cur in tileArea.GetTiles())
+ visited.Add(cur);
+
+ // connect entities on surrounding tiles
+ foreach (Vector2 next in tileArea.GetSurroundingTiles())
+ {
+ if (!visited.Contains(next))
+ queue.Enqueue(next);
+ }
+ }
+ machineGroup.NewTileAreas.Clear();
+ }
+ }
+ }
+
+ /// Add any machine, container, or connector on the given tile to the machine group.
+ /// The machine group to extend.
+ /// The location to search.
+ /// The tile to search.
+ private bool TryAddEntity(MachineGroupBuilder group, GameLocation location, in Vector2 tile)
+ {
+ switch (this.GetEntity(location, tile))
+ {
+ case IMachine machine:
+ group.Add(machine);
+ return true;
+
+ case IContainer container:
+ if (!container.ShouldIgnore())
+ {
+ group.Add(container);
+ return true;
+ }
+ return false;
+
+ case IAutomatable connector:
+ group.Add(connector.TileArea);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ /// Get a machine, container, or connector from the given tile, if any.
+ /// The location to search.
+ /// The tile to search.
+ private IAutomatable GetEntity(GameLocation location, Vector2 tile)
+ {
+ foreach (IAutomationFactory factory in this.AutomationFactories)
+ {
+ // from object
+ if (location.objects.TryGetValue(tile, out SObject obj))
+ {
+ IAutomatable entity = factory.GetFor(obj, location, tile);
+ if (entity != null)
+ return entity;
+ }
+
+ // from terrain feature
+ if (location.terrainFeatures.TryGetValue(tile, out TerrainFeature feature))
+ {
+ IAutomatable entity = factory.GetFor(feature, location, tile);
+ if (entity != null)
+ return entity;
+ }
+
+ // building machine
+ if (location is BuildableGameLocation buildableLocation)
+ {
+ foreach (Building building in buildableLocation.buildings)
+ {
+ Rectangle tileArea = new Rectangle(building.tileX.Value, building.tileY.Value, building.tilesWide.Value, building.tilesHigh.Value);
+ if (tileArea.Contains((int)tile.X, (int)tile.Y))
+ {
+ IAutomatable entity = factory.GetFor(building, buildableLocation, tile);
+ if (entity != null)
+ return entity;
+ }
+ }
+ }
+
+ // from tile position
+ {
+ IAutomatable entity = factory.GetForTile(location, tile);
+ if (entity != null)
+ return entity;
+ }
+ }
+
+ // none found
+ return null;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Buildings/JunimoHutMachine.cs b/Mods/Automate/Automate/Framework/Machines/Buildings/JunimoHutMachine.cs
new file mode 100644
index 000000000..9dbc845f2
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Buildings/JunimoHutMachine.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Objects;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Buildings
+{
+ /// A Junimo hut machine that accepts input and provides output.
+ internal class JunimoHutMachine : BaseMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Whether seeds should be ignored when selecting output.
+ private readonly bool IgnoreSeedOutput;
+
+ /// The Junimo hut's output chest.
+ private Chest Output => this.Machine.output.Value;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying Junimo hut.
+ /// The location which contains the machine.
+ /// Whether seeds should be ignored when selecting output.
+ public JunimoHutMachine(JunimoHut hut, GameLocation location, bool ignoreSeedOutput)
+ : base(hut, location, BaseMachine.GetTileAreaFor(hut))
+ {
+ this.IgnoreSeedOutput = ignoreSeedOutput;
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.Output.items.Any(item => item != null))
+ return MachineState.Done;
+ return MachineState.Processing;
+ }
+
+ /// Get the machine output.
+ public override ITrackedStack GetOutput()
+ {
+ IList- inventory = this.Output.items;
+ return new TrackedItem(inventory.FirstOrDefault(item => item != null), onEmpty: this.OnOutputTaken);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Remove an output item once it's been taken.
+ /// The removed item.
+ private void OnOutputTaken(Item item)
+ {
+ this.Output.clearNulls();
+ this.Output.items.Remove(item);
+ }
+
+ /// Get the next output item.
+ private Item GetNextOutput()
+ {
+ foreach (Item item in this.Output.items)
+ {
+ if (item == null)
+ continue;
+
+ if (this.IgnoreSeedOutput && (item as SObject)?.Category == SObject.SeedsCategory)
+ continue;
+
+ return item;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Buildings/MillMachine.cs b/Mods/Automate/Automate/Framework/Machines/Buildings/MillMachine.cs
new file mode 100644
index 000000000..599367867
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Buildings/MillMachine.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Objects;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Buildings
+{
+ /// A mill machine that accepts input and provides output.
+ internal class MillMachine : BaseMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mill's input chest.
+ private Chest Input => this.Machine.input.Value;
+
+ /// The mill's output chest.
+ private Chest Output => this.Machine.output.Value;
+
+ /// The maximum input stack size to allow per item ID, if different from .
+ private readonly IDictionary MaxInputStackSize;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying mill.
+ /// The location which contains the machine.
+ public MillMachine(Mill mill, GameLocation location)
+ : base(mill, location, BaseMachine.GetTileAreaFor(mill))
+ {
+ this.MaxInputStackSize = new Dictionary
+ {
+ [284] = new SObject(284, 1).maximumStackSize() / 3 // beet => 3 sugar (reduce stack to avoid overfilling output)
+ };
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.Output.items.Any(item => item != null))
+ return MachineState.Done;
+ return this.InputFull()
+ ? MachineState.Processing
+ : MachineState.Empty; // 'empty' insofar as it will accept more input, not necessarily empty
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ IList
- inventory = this.Output.items;
+ return new TrackedItem(inventory.FirstOrDefault(item => item != null), onEmpty: this.OnOutputTaken);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ if (this.InputFull())
+ return false;
+
+ // fill input with wheat (262) and beets (284)
+ bool anyPulled = false;
+ foreach (ITrackedStack stack in input.GetItems().Where(i => i.Sample.ParentSheetIndex == 262 || i.Sample.ParentSheetIndex == 284))
+ {
+ // add item
+ bool anyAdded = this.TryAddInput(stack);
+ if (!anyAdded)
+ continue;
+ anyPulled = true;
+
+ // stop if full
+ if (this.InputFull())
+ return true;
+ }
+
+ return anyPulled;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Try to add an item to the input queue, and adjust its stack size accordingly.
+ /// The item stack to add.
+ /// Returns whether any items were taken from the stack.
+ private bool TryAddInput(ITrackedStack item)
+ {
+ // nothing to add
+ if (item.Count <= 0)
+ return false;
+
+ // clean up input bin
+ this.Input.clearNulls();
+
+ // try adding to input
+ int originalSize = item.Count;
+ IList
- slots = this.Input.items;
+ int maxStackSize = this.GetMaxInputStackSize(item.Sample);
+ for (int i = 0; i < Chest.capacity; i++)
+ {
+ // done
+ if (item.Count <= 0)
+ break;
+
+ // add to existing slot
+ if (slots.Count > i)
+ {
+ Item slot = slots[i];
+ if (item.Sample.canStackWith(slot) && slot.Stack < maxStackSize)
+ {
+ int maxToAdd = Math.Min(item.Count, maxStackSize - slot.Stack); // the most items we can add to the stack (in theory)
+ int actualAdded = maxToAdd - slot.addToStack(maxToAdd); // how many items were actually added to the stack
+ item.Reduce(actualAdded);
+ }
+ continue;
+ }
+
+ // add to new slot
+ slots.Add(item.Take(Math.Min(item.Count, maxStackSize)));
+ }
+
+ return item.Count < originalSize;
+ }
+
+ /// Get whether the mill's input bin is full.
+ private bool InputFull()
+ {
+ var slots = this.Input.items;
+
+ // free slots
+ if (slots.Count < Chest.capacity)
+ return false;
+
+ // free space in stacks
+ foreach (Item slot in slots)
+ {
+ if (slot == null || slot.Stack < this.GetMaxInputStackSize(slot))
+ return false;
+ }
+ return true;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Remove an output item once it's been taken.
+ /// The removed item.
+ private void OnOutputTaken(Item item)
+ {
+ this.Output.clearNulls();
+ this.Output.items.Remove(item);
+ }
+
+ /// Get the maximum input stack size to allow for an item.
+ /// The input item to check.
+ private int GetMaxInputStackSize(Item item)
+ {
+ if (item == null)
+ return 0;
+
+ return this.MaxInputStackSize.TryGetValue(item.ParentSheetIndex, out int max)
+ ? max
+ : item.maximumStackSize();
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs b/Mods/Automate/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs
new file mode 100644
index 000000000..e31eccd90
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs
@@ -0,0 +1,70 @@
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Buildings;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Buildings
+{
+ /// A shipping bin that accepts input and provides output.
+ internal class ShippingBinMachine : BaseMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The farm to automate.
+ private readonly Farm Farm;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The farm containing the shipping bin.
+ /// The tile area covered by the machine.
+ public ShippingBinMachine(Farm farm, Rectangle tileArea)
+ : base(farm, tileArea)
+ {
+ this.Farm = farm;
+ }
+
+ /// Construct an instance.
+ /// The constructed shipping bin.
+ /// The location which contains the machine.
+ /// The farm which has the shipping bin data.
+ public ShippingBinMachine(ShippingBin bin, GameLocation location, Farm farm)
+ : base(location, BaseMachine.GetTileAreaFor(bin))
+ {
+ this.Farm = farm;
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return MachineState.Empty; // always accepts items
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ return null; // no output
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ ITrackedStack tracker = input.GetItems().Where(p => p.Sample is SObject obj && obj.canBeShipped()).Take(1).FirstOrDefault();
+ if (tracker != null)
+ {
+ SObject item = (SObject)tracker.Take(tracker.Count);
+ this.Farm.shippingBin.Add(item);
+ this.Farm.lastItemShipped = item;
+ this.Farm.showShipment(item, false);
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/AutoGrabberMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/AutoGrabberMachine.cs
new file mode 100644
index 000000000..623d11f62
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/AutoGrabberMachine.cs
@@ -0,0 +1,92 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Objects;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// An auto-grabber that provides output.
+ /// See the game's default logic in and .
+ internal class AutoGrabberMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Whether seeds should be ignored when selecting output.
+ private readonly bool IgnoreSeedOutput;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The in-game location.
+ /// The tile covered by the machine.
+ /// Whether seeds should be ignored when selecting output.
+ public AutoGrabberMachine(SObject machine, GameLocation location, Vector2 tile, bool ignoreSeedOutput)
+ : base(machine, location, tile)
+ {
+ this.IgnoreSeedOutput = ignoreSeedOutput;
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value is Chest output && this.GetNextOutput() != null
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ Item next = this.GetNextOutput();
+ return new TrackedItem(next, onEmpty: this.OnOutputTaken);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get the output chest.
+ private Chest GetOutputChest()
+ {
+ return (Chest)this.Machine.heldObject.Value;
+ }
+
+ /// Remove an output item once it's been taken.
+ /// The removed item.
+ private void OnOutputTaken(Item item)
+ {
+ Chest output = this.GetOutputChest();
+ output.clearNulls();
+ output.items.Remove(item);
+ }
+
+ /// Get the next output item.
+ private Item GetNextOutput()
+ {
+ foreach (Item item in this.GetOutputChest().items)
+ {
+ if (item == null)
+ continue;
+
+ if (this.IgnoreSeedOutput && (item as SObject)?.Category == SObject.SeedsCategory)
+ continue;
+
+ return item;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/BeeHouseMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/BeeHouseMachine.cs
new file mode 100644
index 000000000..0a187d0c2
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/BeeHouseMachine.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A bee house that accepts input and provides output.
+ /// See the game's machine logic in , , and .
+ internal class BeeHouseMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The honey types produced by this beehouse indexed by input ID.
+ private readonly IDictionary HoneyTypes = new Dictionary
+ {
+ [376] = SObject.HoneyType.Poppy,
+ [591] = SObject.HoneyType.Tulip,
+ [593] = SObject.HoneyType.SummerSpangle,
+ [595] = SObject.HoneyType.FairyRose,
+ [597] = SObject.HoneyType.BlueJazz
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public BeeHouseMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return Game1.currentSeason == "winter"
+ ? MachineState.Disabled
+ : base.GetState();
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ // get raw output
+ SObject output = this.Machine.heldObject.Value;
+ if (output == null)
+ return null;
+
+ // get flower data
+ SObject.HoneyType type = SObject.HoneyType.Wild;
+ string prefix = type.ToString();
+ int addedPrice = 0;
+ Crop flower = Utility.findCloseFlower(this.Location, this.Machine.TileLocation);
+ if (flower != null)
+ {
+ string[] flowerData = Game1.objectInformation[flower.indexOfHarvest.Value].Split('/');
+ prefix = flowerData[0];
+ addedPrice = Convert.ToInt32(flowerData[1]) * 2;
+ if (!this.HoneyTypes.TryGetValue(flower.indexOfHarvest.Value, out type))
+ type = SObject.HoneyType.Wild;
+ }
+
+ // build object
+ SObject result = new SObject(output.ParentSheetIndex, output.Stack)
+ {
+ name = $"{prefix} Honey",
+ Price = output.Price + addedPrice
+ };
+ result.honeyType.Value = type;
+
+ // yield
+ return new TrackedItem(result, onEmpty: this.Reset);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input needed
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Reset the machine so it's ready to accept a new input.
+ /// The output item that was taken.
+ private void Reset(Item item)
+ {
+ SObject machine = this.Machine;
+
+ machine.heldObject.Value = new SObject(Vector2.Zero, 340, null, false, true, false, false);
+ machine.MinutesUntilReady = 2400 - Game1.timeOfDay + 4320;
+ machine.readyForHarvest.Value = false;
+ machine.showNextIndex.Value = false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/CaskMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/CaskMachine.cs
new file mode 100644
index 000000000..654c0f043
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/CaskMachine.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Objects;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A cask that accepts input and provides output.
+ internal class CaskMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The items which can be aged in a cask with their aging rates.
+ private readonly IDictionary AgingRates = new Dictionary
+ {
+ [424] = 4, // cheese
+ [426] = 4, // goat cheese
+ [459] = 2, // mead
+ [303] = 1.66f, // pale ale
+ [346] = 2, // beer
+ [348] = 1 // wine
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public CaskMachine(Cask machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ SObject heldObject = this.Machine.heldObject.Value;
+ if (heldObject == null)
+ return MachineState.Empty;
+
+ return heldObject.Quality >= 4
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ Cask cask = this.Machine;
+ SObject heldObject = this.Machine.heldObject.Value;
+ return new TrackedItem(heldObject.getOne(), item =>
+ {
+ cask.heldObject.Value = null;
+ cask.MinutesUntilReady = 0;
+ cask.readyForHarvest.Value = false;
+ cask.agingRate.Value = 0;
+ cask.daysToMature.Value = 0;
+ });
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ Cask cask = this.Machine;
+
+ if (input.TryGetIngredient(match => (match.Sample as SObject)?.Quality < 4 && this.AgingRates.ContainsKey(match.Sample.ParentSheetIndex), 1, out IConsumable consumable))
+ {
+ SObject ingredient = (SObject)consumable.Take();
+
+ cask.heldObject.Value = ingredient;
+ cask.agingRate.Value = this.AgingRates[ingredient.ParentSheetIndex];
+ cask.daysToMature.Value = 56;
+ cask.MinutesUntilReady = 999999;
+ switch (ingredient.Quality)
+ {
+ case SObject.medQuality:
+ cask.daysToMature.Value = 42;
+ break;
+ case SObject.highQuality:
+ cask.daysToMature.Value = 28;
+ break;
+ case SObject.bestQuality:
+ cask.daysToMature.Value = 0;
+ cask.MinutesUntilReady = 1;
+ break;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/CharcoalKilnMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/CharcoalKilnMachine.cs
new file mode 100644
index 000000000..d091cd89f
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/CharcoalKilnMachine.cs
@@ -0,0 +1,49 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A charcoal kiln that accepts input and provides output.
+ internal class CharcoalKilnMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // wood => coal
+ new Recipe(
+ input: 388,
+ inputCount: 10,
+ output: input => new SObject(382, 1),
+ minutes: 30
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public CharcoalKilnMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ if (this.GenericPullRecipe(input, this.Recipes))
+ {
+ this.Machine.showNextIndex.Value = true;
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/CheesePressMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/CheesePressMachine.cs
new file mode 100644
index 000000000..1a31be930
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/CheesePressMachine.cs
@@ -0,0 +1,75 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A cheese press that accepts input and provides output.
+ internal class CheesePressMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes processed by this machine (input => output).
+ private readonly IRecipe[] Recipes =
+ {
+ // goat milk => goat cheese
+ new Recipe(
+ input: 436,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 426, null, false, true, false, false),
+ minutes: 200
+ ),
+
+ // large goat milk => gold-quality goat cheese
+ new Recipe(
+ input: 438,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 426, null, false, true, false, false) { Quality = SObject.highQuality },
+ minutes: 200
+ ),
+
+ // milk => cheese
+ new Recipe(
+ input: 184,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 424, null, false, true, false, false),
+ minutes: 200
+ ),
+
+ // large milk => gold-quality cheese
+ new Recipe(
+ input: 186,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 424, "Cheese (=)", false, true, false, false) { Quality = SObject.highQuality },
+ minutes: 200
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public CheesePressMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ if (input.TryGetIngredient(this.Recipes, out IConsumable consumable, out IRecipe recipe))
+ {
+ this.Machine.heldObject.Value = recipe.Output(consumable.Take());
+ this.Machine.MinutesUntilReady = recipe.Minutes;
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/CoopIncubatorMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/CoopIncubatorMachine.cs
new file mode 100644
index 000000000..43b3ed333
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/CoopIncubatorMachine.cs
@@ -0,0 +1,80 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A coop incubator that accepts eggs and spawns chickens.
+ internal class CoopIncubatorMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public CoopIncubatorMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile)
+ {
+ int minutesUntilReady = Game1.player.professions.Contains(2) ? 9000 : 18000;
+ this.Recipes = new IRecipe[]
+ {
+ // egg => chicken
+ new Recipe(
+ input: -5,
+ inputCount: 1,
+ output: item => new SObject(item.ParentSheetIndex, 1),
+ minutes: minutesUntilReady / 2
+ ),
+
+ // dinosaur egg => dinosaur
+ new Recipe(
+ input: 107,
+ inputCount: 1,
+ output: item => new SObject(107, 1),
+ minutes: minutesUntilReady
+ )
+ };
+ }
+
+ /// Get the machine's processing state.
+ /// The coop incubator never produces an object output so it is never done.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value != null
+ ? MachineState.Processing
+ : MachineState.Empty;
+ }
+
+ /// Get the output item.
+ /// The coop incubator never produces an object output.
+ public override ITrackedStack GetOutput()
+ {
+ return null;
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ bool started = this.GenericPullRecipe(input, this.Recipes);
+ if (started)
+ {
+ int eggID = this.Machine.heldObject.Value.ParentSheetIndex;
+ this.Machine.ParentSheetIndex = eggID == 180 || eggID == 182 || eggID == 305
+ ? this.Machine.ParentSheetIndex + 2
+ : this.Machine.ParentSheetIndex + 1;
+ }
+ return started;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/CrabPotMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/CrabPotMachine.cs
new file mode 100644
index 000000000..87286cc97
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/CrabPotMachine.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+using StardewValley;
+using StardewValley.Objects;
+using SFarmer = StardewValley.Farmer;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A crab pot that accepts input and provides output.
+ /// See the game's machine logic in and .
+ internal class CrabPotMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Simplifies access to private game code.
+ private readonly IReflectionHelper Reflection;
+
+ /// Encapsulates monitoring and logging.
+ private readonly IMonitor Monitor;
+
+ /// The fish IDs for which any crabpot has logged an 'invalid fish data' error.
+ private static readonly ISet LoggedInvalidDataErrors = new HashSet();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// Simplifies access to private game code.
+ /// Encapsulates monitoring and logging.
+ /// The tile covered by the machine.
+ public CrabPotMachine(CrabPot machine, GameLocation location, Vector2 tile, IMonitor monitor, IReflectionHelper reflection)
+ : base(machine, location, tile)
+ {
+ this.Monitor = monitor;
+ this.Reflection = reflection;
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.Machine.heldObject.Value == null)
+ {
+ bool hasBait = this.Machine.bait.Value != null || Game1.player.professions.Contains(11); // no bait needed if luremaster
+ return hasBait
+ ? MachineState.Processing
+ : MachineState.Empty;
+ }
+ return this.Machine.readyForHarvest.Value
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ return new TrackedItem(this.Machine.heldObject.Value, onEmpty: this.Reset);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ // get bait
+ if (input.TryGetIngredient(SObject.baitCategory, 1, out IConsumable bait))
+ {
+ this.Machine.bait.Value = (SObject)bait.Take();
+ this.Reflection.GetField(this.Machine, "lidFlapping").SetValue(true);
+ this.Reflection.GetField(this.Machine, "lidFlapTimer").SetValue(60);
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Reset the machine so it's ready to accept a new input.
+ /// The output item that was taken.
+ /// XP and achievement logic based on .
+ private void Reset(Item item)
+ {
+ CrabPot pot = this.Machine;
+
+ // add fishing XP
+ Game1.player.gainExperience(SFarmer.fishingSkill, 5);
+
+ // mark fish caught for achievements and stats
+ IDictionary fishData = Game1.content.Load>("Data\\Fish");
+ if (fishData.TryGetValue(item.ParentSheetIndex, out string fishRow))
+ {
+ int size = 0;
+ try
+ {
+ string[] fields = fishRow.Split('/');
+ int lowerSize = fields.Length > 5 ? Convert.ToInt32(fields[5]) : 1;
+ int upperSize = fields.Length > 5 ? Convert.ToInt32(fields[6]) : 10;
+ size = Game1.random.Next(lowerSize, upperSize + 1);
+ }
+ catch (Exception ex)
+ {
+ // The fish length stats don't affect anything, so it's not worth notifying the
+ // user; just log one trace message per affected fish for troubleshooting.
+ if (CrabPotMachine.LoggedInvalidDataErrors.Add(item.ParentSheetIndex))
+ this.Monitor.Log($"The game's fish data has an invalid entry (#{item.ParentSheetIndex}: {fishData[item.ParentSheetIndex]}). Automated crabpots won't track fish length stats for that fish.\n{ex}", LogLevel.Trace);
+ }
+
+ Game1.player.caughtFish(item.ParentSheetIndex, size);
+ }
+
+ // reset pot
+ pot.readyForHarvest.Value = false;
+ pot.heldObject.Value = null;
+ pot.tileIndexToShow = 710;
+ pot.bait.Value = null;
+ this.Reflection.GetField(pot, "lidFlapping").SetValue(true);
+ this.Reflection.GetField(pot, "lidFlapTimer").SetValue(60f);
+ this.Reflection.GetField(pot, "shake").SetValue(Vector2.Zero);
+ this.Reflection.GetField(pot, "shakeTimer").SetValue(0f);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/CrystalariumMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/CrystalariumMachine.cs
new file mode 100644
index 000000000..00e684326
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/CrystalariumMachine.cs
@@ -0,0 +1,63 @@
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A crystalarium that accepts input and provides output.
+ internal class CrystalariumMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// Simplifies access to private game code.
+ private readonly IReflectionHelper Reflection;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// Simplifies access to private game code.
+ /// The tile covered by the machine.
+ public CrystalariumMachine(SObject machine, GameLocation location, Vector2 tile, IReflectionHelper reflection)
+ : base(machine, location, tile)
+ {
+ this.Reflection = reflection;
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.Machine.heldObject.Value == null)
+ return MachineState.Disabled;
+
+ return this.Machine.readyForHarvest.Value
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ SObject machine = this.Machine;
+ SObject heldObject = machine.heldObject.Value;
+ return new TrackedItem(heldObject.getOne(), item =>
+ {
+ machine.MinutesUntilReady = this.Reflection.GetMethod(machine, "getMinutesForCrystalarium").Invoke(heldObject.ParentSheetIndex);
+ machine.readyForHarvest.Value = false;
+ });
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // started manually
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/FeedHopperMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/FeedHopperMachine.cs
new file mode 100644
index 000000000..5158f55e8
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/FeedHopperMachine.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Buildings;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A hay hopper that accepts input and provides output.
+ internal class FeedHopperMachine : BaseMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public FeedHopperMachine(GameLocation location, Vector2 tile)
+ : base(location, BaseMachine.GetTileAreaFor(tile)) { }
+
+ /// Construct an instance.
+ /// The silo to automate.
+ /// The location containing the machine.
+ public FeedHopperMachine(Building silo, GameLocation location)
+ : base(location, BaseMachine.GetTileAreaFor(silo)) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ Farm farm = Game1.getFarm();
+ return this.GetFreeSpace(farm) > 0
+ ? MachineState.Empty // 'empty' insofar as it will accept more input, not necessarily empty
+ : MachineState.Disabled;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ return null;
+ }
+
+ /// Reset the machine so it's ready to accept a new input.
+ /// Whether the current output was taken.
+ public void Reset(bool outputTaken)
+ {
+ // not applicable
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ Farm farm = Game1.getFarm();
+
+ // skip if full
+ if (this.GetFreeSpace(farm) <= 0)
+ return false;
+
+ // try to add hay (178) until full
+ bool anyPulled = false;
+ foreach (ITrackedStack stack in input.GetItems().Where(p => p.Sample.ParentSheetIndex == 178))
+ {
+ // get free space
+ int space = this.GetFreeSpace(farm);
+ if (space <= 0)
+ return anyPulled;
+
+ // pull hay
+ int maxToAdd = Math.Min(stack.Count, space);
+ int added = maxToAdd - farm.tryToAddHay(maxToAdd);
+ stack.Reduce(added);
+ if (added > 0)
+ anyPulled = true;
+ }
+
+ return anyPulled;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get the amount of hay the hopper can still accept before it's full.
+ /// The farm to check.
+ /// Derived from .
+ private int GetFreeSpace(Farm farm)
+ {
+ return Utility.numSilos() * 240 - farm.piecesOfHay.Value;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/FurnaceMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/FurnaceMachine.cs
new file mode 100644
index 000000000..8caf4d7dc
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/FurnaceMachine.cs
@@ -0,0 +1,92 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A furnace that accepts input and provides output.
+ internal class FurnaceMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ /// Derived from .
+ private readonly IRecipe[] Recipes =
+ {
+ // copper => copper bar
+ new Recipe(
+ input: SObject.copper,
+ inputCount: 5,
+ output: input => new SObject(Vector2.Zero, SObject.copperBar, 1),
+ minutes: 30
+ ),
+
+ // iron => iron bar
+ new Recipe(
+ input: SObject.iron,
+ inputCount: 5,
+ output: input => new SObject(Vector2.Zero, SObject.ironBar, 1),
+ minutes: 120
+ ),
+
+ // gold => gold bar
+ new Recipe(
+ input: SObject.gold,
+ inputCount: 5,
+ output: input => new SObject(Vector2.Zero, SObject.goldBar, 1),
+ minutes: 300
+ ),
+
+ // iridium => iridium bar
+ new Recipe(
+ input: SObject.iridium,
+ inputCount: 5,
+ output: input => new SObject(Vector2.Zero, SObject.iridiumBar, 1),
+ minutes: 480
+ ),
+
+ // quartz => refined quartz
+ new Recipe(
+ input: SObject.quartzIndex,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 338, 1),
+ minutes: 90
+ ),
+
+ // refined quartz => refined quartz
+ new Recipe(
+ input: 82,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 338, 3),
+ minutes: 90
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public FurnaceMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ if (input.TryGetIngredient(SObject.coal, 1, out IConsumable coal) && this.GenericPullRecipe(input, this.Recipes))
+ {
+ coal.Reduce();
+ this.Machine.initializeLightSource(this.Machine.TileLocation);
+ this.Machine.showNextIndex.Value = true;
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/KegMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/KegMachine.cs
new file mode 100644
index 000000000..efc8adfcc
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/KegMachine.cs
@@ -0,0 +1,103 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A keg that accepts input and provides output.
+ /// See the game's machine logic in and .
+ internal class KegMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // honey => mead
+ new Recipe(
+ input: 340,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 459, "Mead", false, true, false, false) { name = "Mead" },
+ minutes: 600
+ ),
+
+ // coffee bean => coffee
+ new Recipe(
+ input: 433,
+ inputCount: 5,
+ output: input => new SObject(Vector2.Zero, 395, "Coffee", false, true, false, false) { name = "Coffee" },
+ minutes: 120
+ ),
+
+ // wheat => beer
+ new Recipe(
+ input: 262,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 346, "Beer", false, true, false, false) { name = "Beer" },
+ minutes: 1750
+ ),
+
+ // hops => pale ale
+ new Recipe(
+ input: 304,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 303, "Pale Ale", false, true, false, false) { name = "Pale Ale" },
+ minutes: 2250
+ ),
+
+ // fruit => wine
+ new Recipe(
+ input: SObject.FruitsCategory,
+ inputCount: 1,
+ output: input =>
+ {
+ SObject wine = new SObject(Vector2.Zero, 348, input.Name + " Wine", false, true, false, false)
+ {
+ name = input.Name + " Wine",
+ Price = ((SObject)input).Price * 3
+ };
+ wine.preserve.Value = SObject.PreserveType.Wine;
+ wine.preservedParentSheetIndex.Value = input.ParentSheetIndex;
+ return wine;
+ },
+ minutes: 10000
+ ),
+ new Recipe(
+ input: SObject.VegetableCategory,
+ inputCount: 1,
+ output: input =>
+ {
+ SObject juice = new SObject(Vector2.Zero, 350, input.Name + " Juice", false, true, false, false)
+ {
+ name = input.Name + " Juice",
+ Price = (int)(((SObject)input).Price * 2.25)
+ };
+ juice.preserve.Value = SObject.PreserveType.Juice;
+ juice.preservedParentSheetIndex.Value = input.ParentSheetIndex;
+ return juice;
+ },
+ minutes: 6000
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public KegMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return this.GenericPullRecipe(input, this.Recipes);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/LightningRodMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/LightningRodMachine.cs
new file mode 100644
index 000000000..3f643ef47
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/LightningRodMachine.cs
@@ -0,0 +1,35 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A lightning rod that accepts input and provides output.
+ internal class LightningRodMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public LightningRodMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ SObject heldObject = this.Machine.heldObject.Value;
+ return new TrackedItem(heldObject.getOne(), onEmpty: this.GenericReset);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/LoomMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/LoomMachine.cs
new file mode 100644
index 000000000..4f6e6d09a
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/LoomMachine.cs
@@ -0,0 +1,56 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A loom that accepts input and provides output.
+ internal class LoomMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // wool => cloth
+ new Recipe(
+ input: 440,
+ inputCount: 1,
+ output: item => new SObject(Vector2.Zero, 428, null, false, true, false, false),
+ minutes: 240
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public LoomMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ SObject machine = this.Machine;
+ return new TrackedItem(machine.heldObject.Value, item =>
+ {
+ machine.heldObject.Value = null;
+ machine.readyForHarvest.Value = false;
+ machine.showNextIndex.Value = false;
+ });
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return this.GenericPullRecipe(input, this.Recipes);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/MayonnaiseMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/MayonnaiseMachine.cs
new file mode 100644
index 000000000..703784407
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/MayonnaiseMachine.cs
@@ -0,0 +1,86 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A mayonnaise that accepts input and provides output.
+ internal class MayonnaiseMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // void egg => void mayonnaise
+ new Recipe(
+ input: 305,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 308, null, false, true, false, false),
+ minutes: 180
+ ),
+
+ // duck egg => duck mayonnaise
+ new Recipe(
+ input: 442,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 307, null, false, true, false, false),
+ minutes: 180
+ ),
+
+ // white/brown egg => normal mayonnaise
+ new Recipe(
+ input: 176,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false),
+ minutes: 180
+ ),
+ new Recipe(
+ input: 180,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false),
+ minutes: 180
+ ),
+
+ // dinosaur or large white/brown egg => gold-quality mayonnaise
+ new Recipe(
+ input: 107,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false) { Quality = SObject.highQuality },
+ minutes: 180
+ ),
+ new Recipe(
+ input: 174,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false) { Quality = SObject.highQuality },
+ minutes: 180
+ ),
+ new Recipe(
+ input: 182,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false) { Quality = SObject.highQuality },
+ minutes: 180
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public MayonnaiseMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return this.GenericPullRecipe(input, this.Recipes);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/MushroomBoxMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/MushroomBoxMachine.cs
new file mode 100644
index 000000000..6f2adb2ff
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/MushroomBoxMachine.cs
@@ -0,0 +1,54 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A mushroom box that accepts input and provides output.
+ internal class MushroomBoxMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public MushroomBoxMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value != null && this.Machine.readyForHarvest.Value
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ return new TrackedItem(this.Machine.heldObject.Value, onEmpty: this.Reset);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input needed
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Reset the machine so it's ready to accept a new input.
+ /// The output item that was taken.
+ private void Reset(Item item)
+ {
+ this.GenericReset(item);
+ this.Machine.showNextIndex.Value = false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/OilMakerMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/OilMakerMachine.cs
new file mode 100644
index 000000000..3cd95f130
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/OilMakerMachine.cs
@@ -0,0 +1,68 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// An oil maker that accepts input and provides output.
+ internal class OilMakerMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // truffle => truffle oil
+ new Recipe(
+ input: 430,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 432, null, false, true, false, false),
+ minutes: 360
+ ),
+
+ // sunflower seed => oil
+ new Recipe(
+ input: 431,
+ inputCount: 1,
+ output: input => new SObject(247, 1),
+ minutes: 3200
+ ),
+
+ // corn => oil
+ new Recipe(
+ input: 270,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 247, null, false, true, false, false),
+ minutes: 1000
+ ),
+
+ // sunflower => oil
+ new Recipe(
+ input: 421,
+ inputCount: 1,
+ output: input => new SObject(Vector2.Zero, 247, null, false, true, false, false),
+ minutes: 60
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public OilMakerMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return this.GenericPullRecipe(input, this.Recipes);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/PreservesJarMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/PreservesJarMachine.cs
new file mode 100644
index 000000000..cc0b08b3c
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/PreservesJarMachine.cs
@@ -0,0 +1,74 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A preserves jar that accepts input and provides output.
+ /// See the game's machine logic in and .
+ internal class PreservesJarMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // fruit => jelly
+ new Recipe(
+ input: SObject.FruitsCategory,
+ inputCount: 1,
+ output: input =>
+ {
+ SObject jelly = new SObject(Vector2.Zero, 344, input.Name + " Jelly", false, true, false, false)
+ {
+ Price = 50 + ((SObject) input).Price * 2,
+ name = input.Name + " Jelly"
+ };
+ jelly.preserve.Value = SObject.PreserveType.Jelly;
+ jelly.preservedParentSheetIndex.Value = input.ParentSheetIndex;
+ return jelly;
+ },
+ minutes: 4000
+ ),
+
+ // vegetable => pickled vegetable
+ new Recipe(
+ input: SObject.VegetableCategory,
+ inputCount: 1,
+ output: input =>
+ {
+ SObject item = new SObject(Vector2.Zero, 342, "Pickled " + input.Name, false, true, false, false)
+ {
+ Price = 50 + ((SObject) input).Price * 2,
+ name = "Pickled " + input.Name
+ };
+ item.preserve.Value = SObject.PreserveType.Pickle;
+ item.preservedParentSheetIndex.Value = input.ParentSheetIndex;
+ return item;
+
+ },
+ minutes: 4000
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public PreservesJarMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return this.GenericPullRecipe(input, this.Recipes);
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/RecyclingMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/RecyclingMachine.cs
new file mode 100644
index 000000000..a436db130
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/RecyclingMachine.cs
@@ -0,0 +1,86 @@
+using System;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A recycling maching that accepts input and provides output.
+ /// This differs slightly from the game implementation in that it uses a more random RNG, due to a C# limitation which prevents us from accessing machine info from the cached recipe output functions for use in the RNG seed.
+ internal class RecyclingMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The RNG to use for randomising output.
+ private static readonly Random Random = new Random();
+
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes =
+ {
+ // trash => coal/iron ore/stone
+ new Recipe(
+ input: 168,
+ inputCount: 1,
+ output: input => new SObject(RecyclingMachine.Random.NextDouble() < 0.3 ? 382 : (RecyclingMachine.Random.NextDouble() < 0.3 ? 380 : 390), RecyclingMachine.Random.Next(1, 4)),
+ minutes: 60
+ ),
+
+ // driftwood => coal/wood
+ new Recipe(
+ input: 169,
+ inputCount: 1,
+ output: input => new SObject(RecyclingMachine.Random.NextDouble() < 0.25 ? 382 : 388, RecyclingMachine.Random.Next(1, 4)),
+ minutes: 60
+ ),
+
+ // broken glasses or broken CD => refined quartz
+ new Recipe(
+ input: 170,
+ inputCount: 1,
+ output: input => new SObject(338, 1),
+ minutes: 60
+ ),
+ new Recipe(
+ input: 171,
+ inputCount: 1,
+ output: input => new SObject(338, 1),
+ minutes: 60
+ ),
+
+ // soggy newspaper => cloth/torch
+ new Recipe(
+ input: 172,
+ inputCount: 1,
+ output: input => RecyclingMachine.Random.NextDouble() < 0.1 ? new SObject(428, 1) : new Torch(Vector2.Zero, 3),
+ minutes: 60
+ )
+ };
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public RecyclingMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ //Random random = new Random((int)Game1.uniqueIDForThisGame / 2 + (int)Game1.stats.DaysPlayed + Game1.timeOfDay + (int)machine.tileLocation.X * 200 + (int)machine.tileLocation.Y);
+
+ if (this.GenericPullRecipe(input, this.Recipes))
+ {
+ Game1.stats.PiecesOfTrashRecycled += 1;
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/SeedMakerMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/SeedMakerMachine.cs
new file mode 100644
index 000000000..093408e47
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/SeedMakerMachine.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A seed maker that accepts input and provides output.
+ internal class SeedMakerMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// A crop ID => seed ID lookup.
+ private static readonly IDictionary CropSeedIDs = SeedMakerMachine.GetCropSeedIDs();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public SeedMakerMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ SObject machine = this.Machine;
+
+ // crop => seeds
+ if (input.TryGetIngredient(this.IsValidCrop, 1, out IConsumable crop))
+ {
+ crop.Reduce();
+ int seedID = SeedMakerMachine.CropSeedIDs[crop.Sample.ParentSheetIndex];
+
+ Random random = new Random((int)Game1.stats.DaysPlayed + (int)Game1.uniqueIDForThisGame / 2 + (int)machine.TileLocation.X + (int)machine.TileLocation.Y * 77 + Game1.timeOfDay);
+ machine.heldObject.Value = new SObject(seedID, random.Next(1, 4));
+ if (random.NextDouble() < 0.005)
+ machine.heldObject.Value = new SObject(499, 1);
+ else if (random.NextDouble() < 0.02)
+ machine.heldObject.Value = new SObject(770, random.Next(1, 5));
+ machine.MinutesUntilReady = 20;
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Get whether a given item is a crop compatible with the seed marker.
+ /// The item to check.
+ public bool IsValidCrop(ITrackedStack item)
+ {
+ return
+ item.Sample.ParentSheetIndex != 433 // seed maker doesn't allow coffee beans
+ && SeedMakerMachine.CropSeedIDs.ContainsKey(item.Sample.ParentSheetIndex);
+ }
+
+ /// Get a crop ID => seed ID lookup.
+ public static IDictionary GetCropSeedIDs()
+ {
+ IDictionary lookup = new Dictionary();
+
+ IDictionary cropData = Game1.content.Load>("Data\\Crops");
+ foreach (KeyValuePair entry in cropData)
+ {
+ int dataCropID = Convert.ToInt32(entry.Value.Split('/')[3]);
+ int dataSeedID = entry.Key;
+ if (!lookup.ContainsKey(dataCropID)) // if multiple crops have the same seed, use the earliest one (which is more likely the vanilla seed)
+ lookup[dataCropID] = dataSeedID;
+ }
+
+ return lookup;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/SlimeEggPressMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/SlimeEggPressMachine.cs
new file mode 100644
index 000000000..2ce84cee3
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/SlimeEggPressMachine.cs
@@ -0,0 +1,50 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A slime egg-press that accepts input and provides output.
+ internal class SlimeEggPressMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public SlimeEggPressMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ SObject heldObject = this.Machine.heldObject.Value;
+ return new TrackedItem(heldObject.getOne(), this.GenericReset);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ // slime => slime egg
+ if (input.TryConsume(766, 100))
+ {
+ int parentSheetIndex = 680;
+ if (Game1.random.NextDouble() < 0.05)
+ parentSheetIndex = 439;
+ else if (Game1.random.NextDouble() < 0.1)
+ parentSheetIndex = 437;
+ else if (Game1.random.NextDouble() < 0.25)
+ parentSheetIndex = 413;
+ this.Machine.heldObject.Value = new SObject(parentSheetIndex, 1);
+ this.Machine.MinutesUntilReady = 1200;
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/SlimeIncubatorMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/SlimeIncubatorMachine.cs
new file mode 100644
index 000000000..6b814c436
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/SlimeIncubatorMachine.cs
@@ -0,0 +1,90 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A slime incubator that accepts slime eggs and spawns slime monsters.
+ internal class SlimeIncubatorMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The recipes to process.
+ private readonly IRecipe[] Recipes;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public SlimeIncubatorMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile)
+ {
+ int minutesUntilReady = Game1.player.professions.Contains(2) ? 2000 : 4000;
+ this.Recipes = new IRecipe[] {
+ // blue slime egg => object with parentSheetIndex of blue slime egg
+ new Recipe(
+ input: 413,
+ inputCount: 1,
+ output: input => new SObject(413,1),
+ minutes: minutesUntilReady
+ ),
+
+ // red slime egg => object with parentSheetIndex of red slime egg
+ new Recipe(
+ input: 437,
+ inputCount: 1,
+ output: input => new SObject(437,1),
+ minutes: minutesUntilReady
+ ),
+
+ // purple slime egg => object with parentSheetIndex of purple slime egg
+ new Recipe(
+ input: 439,
+ inputCount: 1,
+ output: input => new SObject(439,1),
+ minutes: minutesUntilReady
+ ),
+
+ // green slime egg => object with parentSheetIndex of green slime egg
+ new Recipe(
+ input: 680,
+ inputCount: 1,
+ output: input => new SObject(680,1),
+ minutes: minutesUntilReady
+ )
+ };
+ }
+
+ /// Get the machine's processing state.
+ /// The slime incubator does not produce an output object, so it is never done.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value != null
+ ? MachineState.Processing
+ : MachineState.Empty;
+ }
+
+ /// Get the output item.
+ /// The slime incubator does not produce an output object.
+ public override ITrackedStack GetOutput()
+ {
+ return null;
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ bool started = this.GenericPullRecipe(input, this.Recipes);
+ if (started)
+ this.Machine.ParentSheetIndex = 157;
+ return started;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/SodaMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/SodaMachine.cs
new file mode 100644
index 000000000..054c35169
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/SodaMachine.cs
@@ -0,0 +1,36 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A soda machine that accepts input and provides output.
+ internal class SodaMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public SodaMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value != null
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/StatueOfEndlessFortuneMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/StatueOfEndlessFortuneMachine.cs
new file mode 100644
index 000000000..0cbc6e774
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/StatueOfEndlessFortuneMachine.cs
@@ -0,0 +1,36 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A statue of endless fortune that accepts input and provides output.
+ internal class StatueOfEndlessFortuneMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public StatueOfEndlessFortuneMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value != null
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/StatueOfPerfectionMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/StatueOfPerfectionMachine.cs
new file mode 100644
index 000000000..8a48511c6
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/StatueOfPerfectionMachine.cs
@@ -0,0 +1,36 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A statue of perfection that accepts input and provides output.
+ internal class StatueOfPerfectionMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public StatueOfPerfectionMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ return this.Machine.heldObject.Value != null
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/TapperMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/TapperMachine.cs
new file mode 100644
index 000000000..dce6ed8de
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/TapperMachine.cs
@@ -0,0 +1,84 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A tapper that accepts input and provides output.
+ internal class TapperMachine : GenericObjectMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The tree type.
+ private readonly int TreeType;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location to search.
+ /// The tile covered by the machine.
+ /// The tree type being tapped.
+ public TapperMachine(SObject machine, GameLocation location, Vector2 tile, int treeType)
+ : base(machine, location, tile)
+ {
+ this.TreeType = treeType;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ SObject heldObject = this.Machine.heldObject.Value;
+ return new TrackedItem(heldObject.getOne(), onEmpty: this.Reset);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Reset the machine so it's ready to accept a new input.
+ /// The output item that was taken.
+ private void Reset(Item item)
+ {
+ SObject tapper = this.Machine;
+
+ switch (this.TreeType)
+ {
+ case 1:
+ tapper.heldObject.Value = new SObject(725, 1);
+ tapper.MinutesUntilReady = 13000 - Game1.timeOfDay;
+ break;
+ case 2:
+ tapper.heldObject.Value = new SObject(724, 1);
+ tapper.MinutesUntilReady = 16000 - Game1.timeOfDay;
+ break;
+ case 3:
+ tapper.heldObject.Value = new SObject(726, 1);
+ tapper.MinutesUntilReady = 10000 - Game1.timeOfDay;
+ break;
+ case 7:
+ tapper.heldObject.Value = new SObject(420, 1);
+ tapper.MinutesUntilReady = 3000 - Game1.timeOfDay;
+ if (!Game1.currentSeason.Equals("fall"))
+ {
+ tapper.heldObject.Value = new SObject(404, 1);
+ tapper.MinutesUntilReady = 6000 - Game1.timeOfDay;
+ }
+ break;
+ }
+ tapper.heldObject.Value = (SObject)tapper.heldObject.Value.getOne();
+ tapper.readyForHarvest.Value = false;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Objects/WormBinMachine.cs b/Mods/Automate/Automate/Framework/Machines/Objects/WormBinMachine.cs
new file mode 100644
index 000000000..172156ac6
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Objects/WormBinMachine.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
+{
+ /// A tapper that accepts input and provides output.
+ /// See the game's machine logic in and .
+ internal class WormBinMachine : GenericObjectMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying machine.
+ /// The location containing the machine.
+ /// The tile covered by the machine.
+ public WormBinMachine(SObject machine, GameLocation location, Vector2 tile)
+ : base(machine, location, tile) { }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ SObject bin = this.Machine;
+ return new TrackedItem(bin.heldObject.Value, item =>
+ {
+ bin.heldObject.Value = new SObject(685, Game1.random.Next(2, 6));
+ bin.MinutesUntilReady = 2600 - Game1.timeOfDay;
+ bin.readyForHarvest.Value = false;
+ bin.showNextIndex.Value = false;
+ });
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/TerrainFeatures/FruitTreeMachine.cs b/Mods/Automate/Automate/Framework/Machines/TerrainFeatures/FruitTreeMachine.cs
new file mode 100644
index 000000000..301773c9e
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/TerrainFeatures/FruitTreeMachine.cs
@@ -0,0 +1,72 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.TerrainFeatures;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.TerrainFeatures
+{
+ /// A fruit tree machine that accepts input and provides output.
+ /// Derived from .
+ internal class FruitTreeMachine : BaseMachine
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying fruit tree.
+ /// The machine's in-game location.
+ /// The tree's tile position.
+ public FruitTreeMachine(FruitTree tree, GameLocation location, Vector2 tile)
+ : base(tree, location, BaseMachine.GetTileAreaFor(tile)) { }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.Machine.growthStage.Value < FruitTree.treeStage)
+ return MachineState.Disabled;
+
+ return this.Machine.fruitsOnTree.Value > 0
+ ? MachineState.Done
+ : MachineState.Processing;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ FruitTree tree = this.Machine;
+
+ // if struck by lightning => coal
+ if (tree.struckByLightningCountdown.Value > 0)
+ return new TrackedItem(new SObject(382, tree.fruitsOnTree.Value), onReduced: this.OnOutputReduced);
+
+ // else => fruit
+ int quality = SObject.lowQuality;
+ if (tree.daysUntilMature.Value <= -112)
+ quality = SObject.medQuality;
+ if (tree.daysUntilMature.Value <= -224)
+ quality = SObject.highQuality;
+ if (tree.daysUntilMature.Value <= -336)
+ quality = SObject.bestQuality;
+ return new TrackedItem(new SObject(tree.indexOfFruit.Value, tree.fruitsOnTree.Value, quality: quality), onReduced: this.OnOutputReduced);
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Reset the machine so it's ready to accept a new input.
+ /// The output item that was taken.
+ private void OnOutputReduced(Item item)
+ {
+ this.Machine.fruitsOnTree.Value = item.Stack;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Machines/Tiles/TrashCanMachine.cs b/Mods/Automate/Automate/Framework/Machines/Tiles/TrashCanMachine.cs
new file mode 100644
index 000000000..0a8397dc3
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Machines/Tiles/TrashCanMachine.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI;
+using StardewValley;
+using StardewValley.Locations;
+
+namespace Pathoschild.Stardew.Automate.Framework.Machines.Tiles
+{
+ /// A trash can that accepts input and provides output.
+ internal class TrashCanMachine : BaseMachine
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The machine's position in its location.
+ private readonly Vector2 Tile;
+
+ /// The game's list of trash cans the player has already checked.
+ private readonly IList TrashCansChecked;
+
+ /// The trash can index (or -1 if not a valid trash can).
+ private readonly int TrashCanIndex = -1;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The town to search.
+ /// The machine's position in its location.
+ /// The trash can index.
+ /// Simplifies access to private game code.
+ public TrashCanMachine(Town town, Vector2 tile, int trashCanIndex, IReflectionHelper reflection)
+ : base(town, BaseMachine.GetTileAreaFor(tile))
+ {
+ this.Tile = tile;
+ this.TrashCansChecked = reflection.GetField>(town, "garbageChecked").GetValue();
+ if (trashCanIndex >= 0 && trashCanIndex < this.TrashCansChecked.Count)
+ this.TrashCanIndex = trashCanIndex;
+ }
+
+ /// Get the machine's processing state.
+ public override MachineState GetState()
+ {
+ if (this.TrashCanIndex == -1)
+ return MachineState.Disabled;
+ if (this.TrashCansChecked[this.TrashCanIndex])
+ return MachineState.Processing;
+ return MachineState.Done;
+ }
+
+ /// Get the output item.
+ public override ITrackedStack GetOutput()
+ {
+ // get trash
+ int? itemID = this.GetRandomTrash(this.TrashCanIndex);
+ if (itemID.HasValue)
+ return new TrackedItem(new StardewValley.Object(itemID.Value, 1), onEmpty: this.MarkChecked);
+
+ // if nothing is returned, mark trash can checked
+ this.MarkChecked(null);
+ return null;
+ }
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ public override bool SetInput(IStorage input)
+ {
+ return false; // no input
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Reset the machine so it starts processing the next item.
+ /// The output item that was taken.
+ private void MarkChecked(Item item)
+ {
+ this.TrashCansChecked[this.TrashCanIndex] = true;
+ }
+
+ /// Get a random trash item ID.
+ /// The trash can index.
+ /// Duplicated from .
+ private int? GetRandomTrash(int index)
+ {
+ Random random = new Random((int)Game1.uniqueIDForThisGame / 2 + (int)Game1.stats.DaysPlayed + 777 + index);
+ if (random.NextDouble() < 0.2 + Game1.dailyLuck)
+ {
+ int parentSheetIndex = 168;
+ switch (random.Next(10))
+ {
+ case 0:
+ parentSheetIndex = 168;
+ break;
+ case 1:
+ parentSheetIndex = 167;
+ break;
+ case 2:
+ parentSheetIndex = 170;
+ break;
+ case 3:
+ parentSheetIndex = 171;
+ break;
+ case 4:
+ parentSheetIndex = 172;
+ break;
+ case 5:
+ parentSheetIndex = 216;
+ break;
+ case 6:
+ parentSheetIndex = Utility.getRandomItemFromSeason(Game1.currentSeason, ((int)this.Tile.X) * 653 + ((int)this.Tile.Y) * 777, false);
+ break;
+ case 7:
+ parentSheetIndex = 403;
+ break;
+ case 8:
+ parentSheetIndex = 309 + random.Next(3);
+ break;
+ case 9:
+ parentSheetIndex = 153;
+ break;
+ }
+ if (index == 3 && random.NextDouble() < 0.2 + Game1.dailyLuck)
+ {
+ parentSheetIndex = 535;
+ if (random.NextDouble() < 0.05)
+ parentSheetIndex = 749;
+ }
+ if (index == 4 && random.NextDouble() < 0.2 + Game1.dailyLuck)
+ {
+ parentSheetIndex = 378 + random.Next(3) * 2;
+ random.Next(1, 5);
+ }
+ if (index == 5 && random.NextDouble() < 0.2 + Game1.dailyLuck && Game1.dishOfTheDay != null)
+ parentSheetIndex = Game1.dishOfTheDay.ParentSheetIndex != 217 ? Game1.dishOfTheDay.ParentSheetIndex : 216;
+ if (index == 6 && random.NextDouble() < 0.2 + Game1.dailyLuck)
+ parentSheetIndex = 223;
+
+ return parentSheetIndex;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Models/ModConfig.cs b/Mods/Automate/Automate/Framework/Models/ModConfig.cs
new file mode 100644
index 000000000..1327f11c3
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Models/ModConfig.cs
@@ -0,0 +1,37 @@
+using Newtonsoft.Json;
+using Pathoschild.Stardew.Common;
+using StardewModdingAPI;
+
+namespace Pathoschild.Stardew.Automate.Framework.Models
+{
+ /// The raw mod configuration.
+ internal class ModConfig
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// Whether to treat the shipping bin as a machine that can be automated.
+ public bool AutomateShippingBin { get; set; } = true;
+
+ /// The number of ticks between each automation process (60 = once per second).
+ public int AutomationInterval { get; set; } = 60;
+
+ /// The control bindings.
+ public ModConfigControls Controls { get; set; } = new ModConfigControls();
+
+ /// The in-game objects through which machines can connect.
+ public ModConfigObject[] Connectors { get; set; } = new ModConfigObject[0];
+
+
+ /*********
+ ** Nested models
+ *********/
+ /// A set of control bindings.
+ internal class ModConfigControls
+ {
+ /// The button which toggles the automation overlay.
+ [JsonConverter(typeof(StringEnumArrayConverter))]
+ public SButton[] ToggleOverlay { get; set; } = { SButton.U };
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Models/ModConfigObject.cs b/Mods/Automate/Automate/Framework/Models/ModConfigObject.cs
new file mode 100644
index 000000000..f61990a6e
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Models/ModConfigObject.cs
@@ -0,0 +1,16 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Pathoschild.Stardew.Automate.Framework.Models
+{
+ /// An object identifier.
+ internal class ModConfigObject
+ {
+ /// The object type.
+ [JsonConverter(typeof(StringEnumConverter))]
+ public ObjectType Type { get; set; }
+
+ /// The object ID.
+ public int ID { get; set; }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Models/ObjectType.cs b/Mods/Automate/Automate/Framework/Models/ObjectType.cs
new file mode 100644
index 000000000..746a2e76b
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Models/ObjectType.cs
@@ -0,0 +1,15 @@
+namespace Pathoschild.Stardew.Automate.Framework.Models
+{
+ /// The type of an in-game object for the mod's purposes.
+ internal enum ObjectType
+ {
+ /// A flooring object.
+ Floor,
+
+ /// A bigcraftable object.
+ BigCraftable,
+
+ /// A map object.
+ Object
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/OverlayMenu.cs b/Mods/Automate/Automate/Framework/OverlayMenu.cs
new file mode 100644
index 000000000..41b04858e
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/OverlayMenu.cs
@@ -0,0 +1,126 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Pathoschild.Stardew.Common;
+using Pathoschild.Stardew.Common.UI;
+using StardewModdingAPI;
+using StardewModdingAPI.Events;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// The overlay which highlights automatable machines.
+ internal class OverlayMenu : BaseOverlay
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The padding to apply to tile backgrounds to make the grid visible.
+ private readonly int TileGap = 1;
+
+ /// A machine group lookup by tile coordinate.
+ private readonly IDictionary GroupTiles;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The SMAPI events available for mods.
+ /// An API for checking and changing input state.
+ /// The machine groups to display.
+ public OverlayMenu(IModEvents events, IInputHelper inputHelper, IEnumerable machineGroups)
+ : base(events, inputHelper)
+ {
+ // init machine groups
+ machineGroups = machineGroups.ToArray();
+ this.GroupTiles =
+ (
+ from machineGroup in machineGroups
+ from tile in machineGroup.Tiles
+ select new { tile, machineGroup }
+ )
+ .ToDictionary(p => p.tile, p => p.machineGroup);
+ }
+
+
+ /*********
+ ** Protected
+ *********/
+ /// Draw the overlay to the screen.
+ /// The sprite batch being drawn.
+ [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "Deliberate discarded for conversion to tile coordinates.")]
+ protected override void Draw(SpriteBatch spriteBatch)
+ {
+ if (!Context.IsPlayerFree)
+ return;
+
+ // draw each tile
+ foreach (Vector2 tile in TileHelper.GetVisibleTiles())
+ {
+ // get tile's screen coordinates
+ float screenX = tile.X * Game1.tileSize - Game1.viewport.X;
+ float screenY = tile.Y * Game1.tileSize - Game1.viewport.Y;
+ int tileSize = Game1.tileSize;
+
+ // get machine group
+ this.GroupTiles.TryGetValue(tile, out MachineGroup group);
+ bool isGrouped = group != null;
+ bool isActive = isGrouped && group.HasInternalAutomation;
+
+ // draw background
+ {
+ Color color = Color.Black * 0.5f;
+ if (isActive)
+ color = Color.Green * 0.2f;
+ else if (isGrouped)
+ color = Color.Red * 0.2f;
+
+ spriteBatch.DrawLine(screenX + this.TileGap, screenY + this.TileGap, new Vector2(tileSize - this.TileGap * 2, tileSize - this.TileGap * 2), color);
+ }
+
+ // draw group edge borders
+ if (group != null)
+ this.DrawEdgeBorders(spriteBatch, group, tile, group.HasInternalAutomation ? Color.Green : Color.Red);
+ }
+
+ // draw cursor
+ this.DrawCursor();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Draw borders for each unconnected edge of a tile.
+ /// The sprite batch being drawn.
+ /// The machine group.
+ /// The group tile.
+ /// The border color.
+ private void DrawEdgeBorders(SpriteBatch spriteBatch, MachineGroup group, Vector2 tile, Color color)
+ {
+ int borderSize = 3;
+ float screenX = tile.X * Game1.tileSize - Game1.viewport.X;
+ float screenY = tile.Y * Game1.tileSize - Game1.viewport.Y;
+ float tileSize = Game1.tileSize;
+
+ // top
+ if (!group.Tiles.Contains(new Vector2(tile.X, tile.Y - 1)))
+ spriteBatch.DrawLine(screenX, screenY, new Vector2(tileSize, borderSize), color); // top
+
+ // bottom
+ if (!group.Tiles.Contains(new Vector2(tile.X, tile.Y + 1)))
+ spriteBatch.DrawLine(screenX, screenY + tileSize, new Vector2(tileSize, borderSize), color); // bottom
+
+ // left
+ if (!group.Tiles.Contains(new Vector2(tile.X - 1, tile.Y)))
+ spriteBatch.DrawLine(screenX, screenY, new Vector2(borderSize, tileSize), color); // left
+
+ // right
+ if (!group.Tiles.Contains(new Vector2(tile.X + 1, tile.Y)))
+ spriteBatch.DrawLine(screenX + tileSize, screenY, new Vector2(borderSize, tileSize), color); // right
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Recipe.cs b/Mods/Automate/Automate/Framework/Recipe.cs
new file mode 100644
index 000000000..9f2ac69f5
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Recipe.cs
@@ -0,0 +1,49 @@
+using System;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// Describes a generic recipe based on item input and output.
+ internal class Recipe : IRecipe
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The input item or category ID.
+ public int InputID { get; }
+
+ /// The number of inputs needed.
+ public int InputCount { get; }
+
+ /// The output to generate (given an input).
+ public Func
- Output { get; }
+
+ /// The time needed to prepare an output.
+ public int Minutes { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The input item or category ID.
+ /// The number of inputs needed.
+ /// The output to generate (given an input).
+ /// The time needed to prepare an output.
+ public Recipe(int input, int inputCount, Func
- output, int minutes)
+ {
+ this.InputID = input;
+ this.InputCount = inputCount;
+ this.Output = output;
+ this.Minutes = minutes;
+ }
+
+ /// Get whether the recipe can accept a given item as input (regardless of stack size).
+ /// The item to check.
+ public bool AcceptsInput(ITrackedStack stack)
+ {
+ return stack.Sample.ParentSheetIndex == this.InputID || stack.Sample.Category == this.InputID;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Storage/ChestContainer.cs b/Mods/Automate/Automate/Framework/Storage/ChestContainer.cs
new file mode 100644
index 000000000..aaa5eda7b
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Storage/ChestContainer.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Objects;
+
+namespace Pathoschild.Stardew.Automate.Framework.Storage
+{
+ /// A in-game chest which can provide or store items.
+ internal class ChestContainer : IContainer
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The underlying chest.
+ private readonly Chest Chest;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// The container name (if any).
+ public string Name => this.Chest.Name;
+
+ /// The location which contains the container.
+ public GameLocation Location { get; }
+
+ /// The tile area covered by the container.
+ public Rectangle TileArea { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying chest.
+ /// The location which contains the container.
+ /// The tile area covered by the container.
+ public ChestContainer(Chest chest, GameLocation location, Vector2 tile)
+ {
+ this.Chest = chest;
+ this.Location = location;
+ this.TileArea = new Rectangle((int)tile.X, (int)tile.Y, 1, 1);
+ }
+
+ /// Store an item stack.
+ /// The item stack to store.
+ /// If the storage can't hold the entire stack, it should reduce the tracked stack accordingly.
+ public void Store(ITrackedStack stack)
+ {
+ if (stack.Count <= 0)
+ return;
+
+ IList
- inventory = this.Chest.items;
+
+ // try stack into existing slot
+ foreach (Item slot in inventory)
+ {
+ if (slot != null && stack.Sample.canStackWith(slot))
+ {
+ int added = stack.Count - slot.addToStack(stack.Count);
+ stack.Reduce(added);
+ if (stack.Count <= 0)
+ return;
+ }
+ }
+
+ // try add to empty slot
+ for (int i = 0; i < Chest.capacity && i < inventory.Count; i++)
+ {
+ if (inventory[i] == null)
+ {
+ inventory[i] = stack.Take(stack.Count);
+ return;
+ }
+
+ }
+
+ // try add new slot
+ if (inventory.Count < Chest.capacity)
+ inventory.Add(stack.Take(stack.Count));
+ }
+
+ /// Find items in the pipe matching a predicate.
+ /// Matches items that should be returned.
+ /// The number of items to find.
+ /// If the pipe has no matching item, returns null. Otherwise returns a tracked item stack, which may have less items than requested if no more were found.
+ public ITrackedStack Get(Func
- predicate, int count)
+ {
+ ITrackedStack[] stacks = this.GetImpl(predicate, count).ToArray();
+ if (!stacks.Any())
+ return null;
+ return new TrackedItemCollection(stacks);
+ }
+
+ /// Returns an enumerator that iterates through the collection.
+ /// An enumerator that can be used to iterate through the collection.
+ public IEnumerator GetEnumerator()
+ {
+ foreach (Item item in this.Chest.items.ToArray())
+ {
+ if (item != null)
+ yield return this.GetTrackedItem(item);
+ }
+ }
+
+ /// Returns an enumerator that iterates through a collection.
+ /// An object that can be used to iterate through the collection.
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Find items in the pipe matching a predicate.
+ /// Matches items that should be returned.
+ /// The number of items to find.
+ /// If there aren't enough items in the pipe, it should return those it has.
+ private IEnumerable GetImpl(Func
- predicate, int count)
+ {
+ int countFound = 0;
+ foreach (Item item in this.Chest.items)
+ {
+ if (item != null && predicate(item))
+ {
+ countFound += item.Stack;
+ yield return this.GetTrackedItem(item);
+ if (countFound >= count)
+ yield break;
+ }
+ }
+ }
+
+ /// Get a tracked item sync'd with the chest inventory.
+ /// The item to track.
+ private ITrackedStack GetTrackedItem(Item item)
+ {
+ return new TrackedItem(item, onEmpty: i => this.Chest.items.Remove(i));
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/Storage/ContainerExtensions.cs b/Mods/Automate/Automate/Framework/Storage/ContainerExtensions.cs
new file mode 100644
index 000000000..ce9fe3b67
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/Storage/ContainerExtensions.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace Pathoschild.Stardew.Automate.Framework.Storage
+{
+ /// Provides extensions for instances.
+ internal static class ContainerExtensions
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Get whether the container name contains a given tag.
+ /// The container instance.
+ /// The tag to check, excluding the '|' delimiters.
+ public static bool HasTag(this IContainer container, string tag)
+ {
+ return container.Name?.IndexOf($"|{tag}|", StringComparison.InvariantCultureIgnoreCase) >= 0;
+ }
+
+ /// Get whether this container should be preferred for output when possible.
+ /// The container instance.
+ public static bool ShouldIgnore(this IContainer container)
+ {
+ return container.HasTag("automate:ignore");
+ }
+
+ /// Get whether input is enabled for this container.
+ /// The container instance.
+ public static bool AllowsInput(this IContainer container)
+ {
+ return !container.ShouldIgnore() && !container.HasTag("automate:noinput");
+ }
+
+ /// Get whether output is enabled for this container.
+ /// The container instance.
+ public static bool AllowsOutput(this IContainer container)
+ {
+ return !container.ShouldIgnore() && !container.HasTag("automate:nooutput");
+ }
+
+ /// Get whether this container should be preferred for output when possible.
+ /// The container instance.
+ public static bool PreferForOutput(this IContainer container)
+ {
+ return container.HasTag("automate:output");
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/Framework/StorageManager.cs b/Mods/Automate/Automate/Framework/StorageManager.cs
new file mode 100644
index 000000000..59fa3250d
--- /dev/null
+++ b/Mods/Automate/Automate/Framework/StorageManager.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Pathoschild.Stardew.Automate.Framework.Storage;
+using StardewValley;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate.Framework
+{
+ /// Manages access to items in the underlying containers.
+ internal class StorageManager : IStorage
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The storage containers.
+ private readonly IContainer[] Containers;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The storage containers.
+ public StorageManager(IEnumerable containers)
+ {
+ this.Containers = containers.ToArray();
+ }
+
+ /****
+ ** GetItems
+ ****/
+ /// Get all items from the given pipes.
+ public IEnumerable GetItems()
+ {
+ foreach (IContainer container in this.Containers)
+ {
+ if (!container.AllowsOutput())
+ continue;
+
+ foreach (ITrackedStack item in container)
+ yield return item;
+ }
+ }
+
+ /****
+ ** TryGetIngredient
+ ****/
+ /// Get an ingredient needed for a recipe.
+ /// Returns whether an item should be matched.
+ /// The number of items to find.
+ /// The matching consumables.
+ /// Returns whether the requirement is met.
+ public bool TryGetIngredient(Func predicate, int count, out IConsumable consumable)
+ {
+ int countMissing = count;
+ ITrackedStack[] consumables = this.GetItems().Where(predicate)
+ .TakeWhile(chestItem =>
+ {
+ if (countMissing <= 0)
+ return false;
+
+ countMissing -= chestItem.Count;
+ return true;
+ })
+ .ToArray();
+
+ consumable = new Consumable(new TrackedItemCollection(consumables), count);
+ return consumable.IsMet;
+ }
+
+ /// Get an ingredient needed for a recipe.
+ /// The item or category ID.
+ /// The number of items to find.
+ /// The matching consumables.
+ /// Returns whether the requirement is met.
+ public bool TryGetIngredient(int id, int count, out IConsumable consumable)
+ {
+ return this.TryGetIngredient(item => item.Sample.ParentSheetIndex == id || item.Sample.Category == id, count, out consumable);
+ }
+
+ /// Get an ingredient needed for a recipe.
+ /// The items to match.
+ /// The matching consumables.
+ /// The matched requisition.
+ /// Returns whether the requirement is met.
+ public bool TryGetIngredient(IRecipe[] recipes, out IConsumable consumable, out IRecipe recipe)
+ {
+ IDictionary> accumulator = recipes.ToDictionary(req => req, req => new List());
+
+ foreach (ITrackedStack stack in this.GetItems())
+ {
+ foreach (var entry in accumulator)
+ {
+ recipe = entry.Key;
+ List found = entry.Value;
+
+ if (recipe.AcceptsInput(stack))
+ {
+ found.Add(stack);
+ if (found.Sum(p => p.Count) >= recipe.InputCount)
+ {
+ consumable = new Consumable(new TrackedItemCollection(found), entry.Key.InputCount);
+ return true;
+ }
+ }
+ }
+ }
+
+ consumable = null;
+ recipe = null;
+ return false;
+ }
+
+ /****
+ ** TryConsume
+ ****/
+ /// Consume an ingredient needed for a recipe.
+ /// Returns whether an item should be matched.
+ /// The number of items to find.
+ /// Returns whether the item was consumed.
+ public bool TryConsume(Func predicate, int count)
+ {
+ if (this.TryGetIngredient(predicate, count, out IConsumable requirement))
+ {
+ requirement.Reduce();
+ return true;
+ }
+ return false;
+ }
+
+ /// Consume an ingredient needed for a recipe.
+ /// The item ID.
+ /// The number of items to find.
+ /// Returns whether the item was consumed.
+ public bool TryConsume(int itemID, int count)
+ {
+ return this.TryConsume(item => item.Sample.ParentSheetIndex == itemID, count);
+ }
+
+ /****
+ ** TryPush
+ ****/
+ /// Add the given item stack to the pipes if there's space.
+ /// The item stack to push.
+ public bool TryPush(ITrackedStack item)
+ {
+ if (item == null || item.Count <= 0)
+ return false;
+
+ int originalCount = item.Count;
+
+ var preferOutputContainers = this.Containers.Where(p => p.AllowsInput() && p.PreferForOutput());
+ var otherContainers = this.Containers.Where(p => p.AllowsInput() && !p.PreferForOutput());
+
+ // push into 'output' chests
+ foreach (IContainer container in preferOutputContainers)
+ {
+ container.Store(item);
+ if (item.Count <= 0)
+ return true;
+ }
+
+ // push into chests that already have this item
+ string itemKey = this.GetItemKey(item.Sample);
+ foreach (IContainer container in otherContainers)
+ {
+ if (container.All(p => this.GetItemKey(p.Sample) != itemKey))
+ continue;
+
+ container.Store(item);
+ if (item.Count <= 0)
+ return true;
+ }
+
+ // push into first available chest
+ if (item.Count >= 0)
+ {
+ foreach (IContainer container in otherContainers)
+ {
+ container.Store(item);
+ if (item.Count <= 0)
+ return true;
+ }
+ }
+
+ return item.Count < originalCount;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get a key which uniquely identifies an item type.
+ /// The item to identify.
+ private string GetItemKey(Item item)
+ {
+ string key = item.GetType().FullName;
+ if (item is SObject obj)
+ key += "_craftable:" + obj.bigCraftable.Value;
+ key += "_id:" + item.ParentSheetIndex;
+
+ return key;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/IAutomatable.cs b/Mods/Automate/Automate/IAutomatable.cs
new file mode 100644
index 000000000..a0ec02f17
--- /dev/null
+++ b/Mods/Automate/Automate/IAutomatable.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// An automatable entity, which can implement a more specific type like or . If it doesn't implement a more specific type, it's treated as a connector with no additional logic.
+ public interface IAutomatable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The location which contains the machine.
+ GameLocation Location { get; }
+
+ /// The tile area covered by the machine.
+ Rectangle TileArea { get; }
+ }
+}
diff --git a/Mods/Automate/Automate/IAutomateAPI.cs b/Mods/Automate/Automate/IAutomateAPI.cs
new file mode 100644
index 000000000..4a63532f2
--- /dev/null
+++ b/Mods/Automate/Automate/IAutomateAPI.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// The API which lets other mods interact with Automate.
+ public interface IAutomateAPI
+ {
+ /// Add an automation factory.
+ /// An automation factory which construct machines, containers, and connectors.
+ void AddFactory(IAutomationFactory factory);
+
+ /// 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/Automate/Automate/IAutomationFactory.cs b/Mods/Automate/Automate/IAutomationFactory.cs
new file mode 100644
index 000000000..c1adb0c03
--- /dev/null
+++ b/Mods/Automate/Automate/IAutomationFactory.cs
@@ -0,0 +1,43 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Locations;
+using StardewValley.TerrainFeatures;
+using SObject = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// Constructs machines, containers, or connectors which can be added to a machine group.
+ public interface IAutomationFactory
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// Get a machine, container, or connector instance for a given object.
+ /// The in-game object.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ IAutomatable GetFor(SObject obj, GameLocation location, in Vector2 tile);
+
+ /// Get a machine, container, or connector instance for a given terrain feature.
+ /// The terrain feature.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ IAutomatable GetFor(TerrainFeature feature, GameLocation location, in Vector2 tile);
+
+ /// Get a machine, container, or connector instance for a given building.
+ /// The building.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ IAutomatable GetFor(Building building, BuildableGameLocation location, in Vector2 tile);
+
+ /// Get a machine, container, or connector instance for a given tile position.
+ /// The location to check.
+ /// The tile position to check.
+ /// Returns an instance or null.
+ IAutomatable GetForTile(GameLocation location, in Vector2 tile);
+ }
+}
diff --git a/Mods/Automate/Automate/IConsumable.cs b/Mods/Automate/Automate/IConsumable.cs
new file mode 100644
index 000000000..260a1edaf
--- /dev/null
+++ b/Mods/Automate/Automate/IConsumable.cs
@@ -0,0 +1,34 @@
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// An ingredient stack (or stacks) which can be consumed by a machine.
+ public interface IConsumable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The items available to consumable.
+ ITrackedStack Consumables { get; }
+
+ /// A sample item for comparison.
+ /// This should not be a reference to the original stack.
+ Item Sample { get; }
+
+ /// The number of items needed for the recipe.
+ int CountNeeded { get; }
+
+ /// Whether the consumables needed for this requirement are ready.
+ bool IsMet { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Remove the needed number of this item from the stack.
+ void Reduce();
+
+ /// Remove the needed number of this item from the stack and return a new stack matching the count.
+ Item Take();
+ }
+}
diff --git a/Mods/Automate/Automate/IContainer.cs b/Mods/Automate/Automate/IContainer.cs
new file mode 100644
index 000000000..42197319e
--- /dev/null
+++ b/Mods/Automate/Automate/IContainer.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// Provides and stores items for machines.
+ public interface IContainer : IAutomatable, IEnumerable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The container name (if any).
+ string Name { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Find items in the pipe matching a predicate.
+ /// Matches items that should be returned.
+ /// The number of items to find.
+ /// If the pipe has no matching item, returns null. Otherwise returns a tracked item stack, which may have less items than requested if no more were found.
+ ITrackedStack Get(Func
- predicate, int count);
+
+ /// Store an item stack.
+ /// The item stack to store.
+ /// If the storage can't hold the entire stack, it should reduce the tracked stack accordingly.
+ void Store(ITrackedStack stack);
+ }
+}
diff --git a/Mods/Automate/Automate/IMachine.cs b/Mods/Automate/Automate/IMachine.cs
new file mode 100644
index 000000000..fcc20fbbe
--- /dev/null
+++ b/Mods/Automate/Automate/IMachine.cs
@@ -0,0 +1,28 @@
+namespace Pathoschild.Stardew.Automate
+{
+ /// A machine that accepts input and provides output.
+ public interface IMachine : IAutomatable
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// A unique ID for the machine type.
+ /// This value should be identical for two machines if they have the exact same behavior and input logic. For example, if one machine in a group can't process input due to missing items, Automate will skip any other empty machines of that type in the same group since it assumes they need the same inputs.
+ string MachineTypeID { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Get the machine's processing state.
+ MachineState GetState();
+
+ /// Get the output item.
+ ITrackedStack GetOutput();
+
+ /// Provide input to the machine.
+ /// The available items.
+ /// Returns whether the machine started processing an item.
+ bool SetInput(IStorage input);
+ }
+}
diff --git a/Mods/Automate/Automate/IRecipe.cs b/Mods/Automate/Automate/IRecipe.cs
new file mode 100644
index 000000000..806ecbdbf
--- /dev/null
+++ b/Mods/Automate/Automate/IRecipe.cs
@@ -0,0 +1,33 @@
+using System;
+using StardewValley;
+using Object = StardewValley.Object;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// Describes a generic recipe based on item input and output.
+ public interface IRecipe
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The input item or category ID.
+ int InputID { get; }
+
+ /// The number of inputs needed.
+ int InputCount { get; }
+
+ /// The output to generate (given an input).
+ Func
- Output { get; }
+
+ /// The time needed to prepare an output.
+ int Minutes { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// Get whether the recipe can accept a given item as input (regardless of stack size).
+ /// The item to check.
+ bool AcceptsInput(ITrackedStack stack);
+ }
+}
diff --git a/Mods/Automate/Automate/IStorage.cs b/Mods/Automate/Automate/IStorage.cs
new file mode 100644
index 000000000..f8df55eb6
--- /dev/null
+++ b/Mods/Automate/Automate/IStorage.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using Pathoschild.Stardew.Automate.Framework;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// Manages access to items in the underlying containers.
+ public interface IStorage
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Get all items from the given pipes.
+ IEnumerable GetItems();
+
+ /****
+ ** TryGetIngredient
+ ****/
+ /// Get an ingredient needed for a recipe.
+ /// Returns whether an item should be matched.
+ /// The number of items to find.
+ /// The matching consumables.
+ /// Returns whether the requirement is met.
+ bool TryGetIngredient(Func predicate, int count, out IConsumable consumable);
+
+ /// Get an ingredient needed for a recipe.
+ /// The item or category ID.
+ /// The number of items to find.
+ /// The matching consumables.
+ /// Returns whether the requirement is met.
+ bool TryGetIngredient(int id, int count, out IConsumable consumable);
+
+ /// Get an ingredient needed for a recipe.
+ /// The items to match.
+ /// The matching consumables.
+ /// The matched requisition.
+ /// Returns whether the requirement is met.
+ bool TryGetIngredient(IRecipe[] recipes, out IConsumable consumable, out IRecipe recipe);
+
+ /****
+ ** TryConsume
+ ****/
+ /// Consume an ingredient needed for a recipe.
+ /// Returns whether an item should be matched.
+ /// The number of items to find.
+ /// Returns whether the item was consumed.
+ bool TryConsume(Func predicate, int count);
+
+ /// Consume an ingredient needed for a recipe.
+ /// The item ID.
+ /// The number of items to find.
+ /// Returns whether the item was consumed.
+ bool TryConsume(int itemID, int count);
+
+ /****
+ ** TryPush
+ ****/
+ /// Add the given item stack to the pipes if there's space.
+ /// The item stack to push.
+ bool TryPush(ITrackedStack item);
+ }
+}
diff --git a/Mods/Automate/Automate/ITrackedStack.cs b/Mods/Automate/Automate/ITrackedStack.cs
new file mode 100644
index 000000000..aaa738417
--- /dev/null
+++ b/Mods/Automate/Automate/ITrackedStack.cs
@@ -0,0 +1,30 @@
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// An item stack in an input pipe which can be reduced or taken.
+ public interface ITrackedStack
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// A sample item for comparison.
+ /// This should be equivalent to the underlying item (except in stack size), but *not* a reference to it.
+ Item Sample { get; }
+
+ /// The number of items in the stack.
+ int Count { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Remove the specified number of this item from the stack.
+ /// The number to consume.
+ void Reduce(int count);
+
+ /// Remove the specified number of this item from the stack and return a new stack matching the count.
+ /// The number to get.
+ Item Take(int count);
+ }
+}
diff --git a/Mods/Automate/Automate/MachineState.cs b/Mods/Automate/Automate/MachineState.cs
new file mode 100644
index 000000000..b7275df56
--- /dev/null
+++ b/Mods/Automate/Automate/MachineState.cs
@@ -0,0 +1,18 @@
+namespace Pathoschild.Stardew.Automate
+{
+ /// A machine processing state.
+ public enum MachineState
+ {
+ /// The machine is not currently enabled (e.g. out of season or needs to be started manually).
+ Disabled,
+
+ /// The machine has no input.
+ Empty,
+
+ /// The machine is processing an input.
+ Processing,
+
+ /// The machine finished processing an input and has an output item ready.
+ Done
+ }
+}
diff --git a/Mods/Automate/Automate/ModEntry.cs b/Mods/Automate/Automate/ModEntry.cs
new file mode 100644
index 000000000..d394c482f
--- /dev/null
+++ b/Mods/Automate/Automate/ModEntry.cs
@@ -0,0 +1,285 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Pathoschild.Stardew.Automate.Framework;
+using Pathoschild.Stardew.Automate.Framework.Models;
+using Pathoschild.Stardew.Common;
+using Pathoschild.Stardew.Common.Utilities;
+using StardewModdingAPI;
+using StardewModdingAPI.Events;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// The mod entry point.
+ internal class ModEntry : Mod
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The mod configuration.
+ private ModConfig Config;
+
+ /// Constructs machine groups.
+ private MachineGroupFactory Factory;
+
+ /// Whether to enable automation for the current save.
+ private bool EnableAutomation => Context.IsMainPlayer;
+
+ /// The machines to process.
+ private readonly IDictionary ActiveMachineGroups = new Dictionary(new ObjectReferenceComparer());
+
+ /// The disabled machine groups (e.g. machines not connected to a chest).
+ private readonly IDictionary DisabledMachineGroups = new Dictionary(new ObjectReferenceComparer());
+
+ /// The locations that should be reloaded on the next update tick.
+ private readonly HashSet ReloadQueue = new HashSet(new ObjectReferenceComparer());
+
+ /// The number of ticks until the next automation cycle.
+ private int AutomateCountdown;
+
+ /// The current overlay being displayed, if any.
+ private OverlayMenu CurrentOverlay;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// The mod entry point, called after the mod is first loaded.
+ /// Provides methods for interacting with the mod directory, such as read/writing a config file or custom JSON files.
+ public override void Entry(IModHelper helper)
+ {
+ // toggle mod compatibility
+ bool hasBetterJunimos = helper.ModRegistry.IsLoaded("hawkfalcon.BetterJunimos");
+ bool hasDeluxeAutoGrabber = helper.ModRegistry.IsLoaded("stokastic.DeluxeGrabber");
+
+ // init
+ this.Config = helper.ReadConfig();
+ this.Factory = new MachineGroupFactory();
+ this.Factory.Add(new AutomationFactory(this.Config.Connectors, this.Config.AutomateShippingBin, this.Monitor, helper.Reflection, hasBetterJunimos, hasDeluxeAutoGrabber));
+
+ // hook events
+ helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded;
+ helper.Events.Player.Warped += this.OnWarped;
+ helper.Events.World.LocationListChanged += this.World_LocationListChanged;
+ helper.Events.World.ObjectListChanged += this.World_ObjectListChanged;
+ helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
+ helper.Events.Input.ButtonPressed += this.OnButtonPressed;
+
+ if (this.Config.Connectors.Any(p => p.Type == ObjectType.Floor))
+ helper.Events.World.TerrainFeatureListChanged += this.World_TerrainFeatureListChanged;
+
+ // log info
+ this.Monitor.VerboseLog($"Initialised with automation every {this.Config.AutomationInterval} ticks.");
+ }
+
+ /// Get an API that other mods can access. This is always called after .
+ public override object GetApi()
+ {
+ return new AutomateAPI(this.Monitor, this.Factory, this.ActiveMachineGroups, this.DisabledMachineGroups);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /****
+ ** Event handlers
+ ****/
+ /// The method invoked when the player loads a save.
+ /// The event sender.
+ /// The event arguments.
+ private void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
+ {
+ // disable if secondary player
+ if (!this.EnableAutomation)
+ this.Monitor.Log("Disabled automation (only the main player can automate machines in multiplayer mode).", LogLevel.Warn);
+
+ // reset
+ this.ActiveMachineGroups.Clear();
+ this.DisabledMachineGroups.Clear();
+ this.AutomateCountdown = this.Config.AutomationInterval;
+ this.DisableOverlay();
+ foreach (GameLocation location in CommonHelper.GetLocations())
+ this.ReloadQueue.Add(location);
+ }
+
+ /// The method invoked when the player warps to a new location.
+ /// The event sender.
+ /// The event arguments.
+ private void OnWarped(object sender, WarpedEventArgs e)
+ {
+ if (e.IsLocalPlayer)
+ this.ResetOverlayIfShown();
+ }
+
+ /// The method invoked when a location is added or removed.
+ /// The event sender.
+ /// The event arguments.
+ private void World_LocationListChanged(object sender, LocationListChangedEventArgs e)
+ {
+ if (!this.EnableAutomation)
+ return;
+
+ this.Monitor.VerboseLog("Location list changed, reloading all machines.");
+
+ try
+ {
+ this.ActiveMachineGroups.Clear();
+ this.DisabledMachineGroups.Clear();
+ foreach (GameLocation location in CommonHelper.GetLocations())
+ this.ReloadQueue.Add(location);
+ }
+ catch (Exception ex)
+ {
+ this.HandleError(ex, "updating locations");
+ }
+ }
+
+ /// The method invoked when an object is added or removed to a location.
+ /// The event sender.
+ /// The event arguments.
+ private void World_ObjectListChanged(object sender, ObjectListChangedEventArgs e)
+ {
+ if (!this.EnableAutomation)
+ return;
+
+ this.Monitor.VerboseLog($"Object list changed in {e.Location.Name}, reloading machines in current location.");
+ this.ReloadQueue.Add(e.Location);
+ }
+
+ /// The method invoked when a terrain feature is added or removed to a location.
+ /// The event sender.
+ /// The event arguments.
+ private void World_TerrainFeatureListChanged(object sender, TerrainFeatureListChangedEventArgs e)
+ {
+ if (!this.EnableAutomation)
+ return;
+
+ this.Monitor.VerboseLog($"Terrain feature list changed in {e.Location.Name}, reloading machines in current location.");
+ this.ReloadQueue.Add(e.Location);
+ }
+
+ /// The method invoked when the in-game clock time changes.
+ /// The event sender.
+ /// The event arguments.
+ private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
+ {
+ if (!Context.IsWorldReady || !this.EnableAutomation)
+ return;
+
+ try
+ {
+ // handle delay
+ this.AutomateCountdown--;
+ if (this.AutomateCountdown > 0)
+ return;
+ this.AutomateCountdown = this.Config.AutomationInterval;
+
+ // reload machines if needed
+ if (this.ReloadQueue.Any())
+ {
+ foreach (GameLocation location in this.ReloadQueue)
+ this.ReloadMachinesIn(location);
+ this.ReloadQueue.Clear();
+
+ this.ResetOverlayIfShown();
+ }
+
+ // process machines
+ foreach (MachineGroup group in this.GetActiveMachineGroups())
+ group.Automate();
+ }
+ catch (Exception ex)
+ {
+ this.HandleError(ex, "processing machines");
+ }
+ }
+
+ /// The method invoked when the player presses a button.
+ /// The event sender.
+ /// The event arguments.
+ private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
+ {
+ try
+ {
+ // toggle overlay
+ if (Context.IsPlayerFree && this.Config.Controls.ToggleOverlay.Contains(e.Button))
+ {
+ if (this.CurrentOverlay != null)
+ this.DisableOverlay();
+ else
+ this.EnableOverlay();
+ }
+ }
+ catch (Exception ex)
+ {
+ this.HandleError(ex, "handling key input");
+ }
+ }
+
+ /****
+ ** Methods
+ ****/
+ /// Get the active machine groups in every location.
+ private IEnumerable GetActiveMachineGroups()
+ {
+ foreach (KeyValuePair group in this.ActiveMachineGroups)
+ {
+ foreach (MachineGroup machineGroup in group.Value)
+ yield return machineGroup;
+ }
+ }
+
+ /// Reload the machines in a given location.
+ /// The location whose machines to reload.
+ private void ReloadMachinesIn(GameLocation location)
+ {
+ this.Monitor.VerboseLog($"Reloading machines in {location.Name}...");
+
+ // get machine groups
+ MachineGroup[] machineGroups = this.Factory.GetMachineGroups(location).ToArray();
+ this.ActiveMachineGroups[location] = machineGroups.Where(p => p.HasInternalAutomation).ToArray();
+ this.DisabledMachineGroups[location] = machineGroups.Where(p => !p.HasInternalAutomation).ToArray();
+
+ // remove unneeded entries
+ if (!this.ActiveMachineGroups[location].Any())
+ this.ActiveMachineGroups.Remove(location);
+ if (!this.DisabledMachineGroups[location].Any())
+ this.DisabledMachineGroups.Remove(location);
+ }
+
+ /// Log an error and warn the user.
+ /// The exception to handle.
+ /// The verb describing where the error occurred (e.g. "looking that up").
+ private void HandleError(Exception ex, string verb)
+ {
+ this.Monitor.Log($"Something went wrong {verb}:\n{ex}", LogLevel.Error);
+ CommonHelper.ShowErrorMessage($"Huh. Something went wrong {verb}. The error log has the technical details.");
+ }
+
+ /// Disable the overlay, if shown.
+ private void DisableOverlay()
+ {
+ this.CurrentOverlay?.Dispose();
+ this.CurrentOverlay = null;
+ }
+
+ /// Enable the overlay.
+ private void EnableOverlay()
+ {
+ if (this.CurrentOverlay == null)
+ this.CurrentOverlay = new OverlayMenu(this.Helper.Events, this.Helper.Input, this.Factory.GetMachineGroups(Game1.currentLocation));
+ }
+
+ /// Reset the overlay if it's being shown.
+ private void ResetOverlayIfShown()
+ {
+ if (this.CurrentOverlay != null)
+ {
+ this.DisableOverlay();
+ this.EnableOverlay();
+ }
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/TrackedItem.cs b/Mods/Automate/Automate/TrackedItem.cs
new file mode 100644
index 000000000..b0da39bc8
--- /dev/null
+++ b/Mods/Automate/Automate/TrackedItem.cs
@@ -0,0 +1,102 @@
+using System;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// An item stack which notifies callbacks when it's reduced.
+ public class TrackedItem : ITrackedStack
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The item stack.
+ private readonly Item Item;
+
+ /// The callback invoked when the stack size is reduced (including reduced to zero).
+ protected readonly Action
- OnReduced;
+
+ /// The callback invoked when the stack is empty.
+ protected readonly Action
- OnEmpty;
+
+ /// The last stack size handlers were notified of.
+ private int LastStackSize;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// A sample item for comparison.
+ /// This should be equivalent to the underlying item (except in stack size), but *not* a reference to it.
+ public Item Sample { get; }
+
+ /// The number of items in the stack.
+ public int Count => this.Item.Stack;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The item stack.
+ /// The callback invoked when the stack size is reduced (including reduced to zero).
+ /// The callback invoked when the stack is empty.
+ public TrackedItem(Item item, Action
- onReduced = null, Action
- onEmpty = null)
+ {
+ this.Item = item ?? throw new InvalidOperationException("Can't track a null item stack.");
+ this.Sample = this.GetNewStack(item);
+ this.LastStackSize = item.Stack;
+ this.OnReduced = onReduced;
+ this.OnEmpty = onEmpty;
+ }
+
+ /// Remove the specified number of this item from the stack.
+ /// The number to consume.
+ public void Reduce(int count)
+ {
+ this.Item.Stack -= Math.Max(0, count);
+ this.Delegate();
+ }
+
+ /// Remove the specified number of this item from the stack and return a new stack matching the count.
+ /// The number to get.
+ public Item Take(int count)
+ {
+ if (count <= 0)
+ return null;
+
+ this.Reduce(count);
+ return this.GetNewStack(this.Item, count);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Notify handlers.
+ private void Delegate()
+ {
+ // skip if not reduced
+ if (this.Item.Stack >= this.LastStackSize)
+ return;
+ this.LastStackSize = this.Item.Stack;
+
+ // notify handlers
+ this.OnReduced?.Invoke(this.Item);
+ if (this.Item.Stack <= 0)
+ this.OnEmpty?.Invoke(this.Item);
+ }
+
+ /// Create a new stack of the given item.
+ /// The item stack to clone.
+ /// The new stack size.
+ private Item GetNewStack(Item original, int stackSize = 1)
+ {
+ if (original == null)
+ return null;
+
+ Item stack = original.getOne();
+ stack.Stack = stackSize;
+ return stack;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/TrackedItemCollection.cs b/Mods/Automate/Automate/TrackedItemCollection.cs
new file mode 100644
index 000000000..969366c24
--- /dev/null
+++ b/Mods/Automate/Automate/TrackedItemCollection.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+
+namespace Pathoschild.Stardew.Automate
+{
+ /// An item stack which wraps an underlying collection of stacks.
+ public class TrackedItemCollection : ITrackedStack
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The underlying item stacks.
+ private readonly ITrackedStack[] Stacks;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// A sample item for comparison.
+ /// This should be equivalent to the underlying item (except in stack size), but *not* a reference to it.
+ public Item Sample { get; }
+
+ /// The number of items in the stack.
+ public int Count => this.Stacks.Sum(p => p.Count);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying item stacks.
+ public TrackedItemCollection(IEnumerable stacks)
+ {
+ this.Stacks = stacks.ToArray();
+ this.Sample = this.Stacks.FirstOrDefault()?.Sample;
+ }
+
+ /// Remove the specified number of this item from the stack.
+ /// The number to consume.
+ public void Reduce(int count)
+ {
+ if (count <= 0 || !this.Stacks.Any())
+ return;
+
+ // reduce
+ int left = count;
+ foreach (ITrackedStack stack in this.Stacks)
+ {
+ // skip, stack empty
+ if (stack.Count <= 0)
+ continue;
+
+ // take entire stack
+ if (stack.Count < left)
+ {
+ left -= stack.Count;
+ stack.Reduce(stack.Count);
+ continue;
+ }
+
+ // take remaining items
+ stack.Reduce(left);
+ break;
+ }
+ }
+
+ /// Remove the specified number of this item from the stack and return a new stack matching the count.
+ /// The number to get.
+ public Item Take(int count)
+ {
+ if (count <= 0 || !this.Stacks.Any())
+ return null;
+
+ // reduce
+ this.Reduce(count);
+
+ // create new stack
+ Item item = this.Sample.getOne();
+ item.Stack = count;
+ return item;
+ }
+ }
+}
diff --git a/Mods/Automate/Automate/screenshots/chests-anywhere-config.png b/Mods/Automate/Automate/screenshots/chests-anywhere-config.png
new file mode 100644
index 0000000000000000000000000000000000000000..1777e755a9fad2289a2b272eeb8c625776a4258c
GIT binary patch
literal 29371
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