From ec7f5e953145955c7852953058bfb8e911b19576 Mon Sep 17 00:00:00 2001 From: focustense Date: Sat, 27 Jul 2024 18:53:04 -0400 Subject: [PATCH] Add read-only API for querying automation groups. Extends `IAutomatable` with a few additional fields useful for callers, and implements them on the relevant machine/storage types. Adds an `IAutomationGroup` to hold the new result type; this is trivially implementable by the existing `MachineGroup` since all the other interfaces match. --- Automate/AutomationRole.cs | 28 ++++++++++++++++++ Automate/CustomAutomatableInfo.cs | 27 +++++++++++++++++ Automate/Framework/AutomateAPI.cs | 19 ++++++++++++ Automate/Framework/BaseMachine.cs | 10 +++++++ Automate/Framework/IMachineGroup.cs | 5 ++++ Automate/Framework/MachineGroup.cs | 17 ++++++++++- Automate/Framework/MachineWrapper.cs | 3 ++ .../Machines/Buildings/ShippingBinMachine.cs | 3 ++ .../Objects/MiniShippingBinMachine.cs | 7 +++++ .../Machines/Tiles/TrashCanMachine.cs | 17 +++++++---- Automate/Framework/Storage/ChestContainer.cs | 3 ++ Automate/IAutomatable.cs | 7 +++++ Automate/IAutomateAPI.cs | 11 +++++++ Automate/IAutomationGroup.cs | 29 +++++++++++++++++++ 14 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 Automate/AutomationRole.cs create mode 100644 Automate/CustomAutomatableInfo.cs create mode 100644 Automate/IAutomationGroup.cs diff --git a/Automate/AutomationRole.cs b/Automate/AutomationRole.cs new file mode 100644 index 000000000..f24fd0018 --- /dev/null +++ b/Automate/AutomationRole.cs @@ -0,0 +1,28 @@ +namespace Pathoschild.Stardew.Automate +{ + /// + /// Describes the role of an . + /// + public enum AutomationRole + { + /// + /// No role is specified. This may be the case for automation objects added by mods based on an older version of Automate that don't yet support this API. + /// + Unspecified = 0, + + /// + /// A container, such as a chest, from which items can be taken for processing. + /// + Container, + + /// + /// A connector that passively links other objects together. + /// + Connector, + + /// + /// A machine that processes items in a . + /// + Machine, + } +} diff --git a/Automate/CustomAutomatableInfo.cs b/Automate/CustomAutomatableInfo.cs new file mode 100644 index 000000000..8252b15a9 --- /dev/null +++ b/Automate/CustomAutomatableInfo.cs @@ -0,0 +1,27 @@ +namespace Pathoschild.Stardew.Automate +{ + /// + /// Provides information about an automatable type (typically an ad-hoc "machine", such as + /// ) that does not correspond to an addressable game object. + /// + /// + /// Can be used as the when no other type would be applicable. + /// + public interface ICustomAutomatableInfo + { + /// + /// A unique (per save) identifier for this instance. + /// + string Id { get; set; } + + /// + /// Describes the kind of automation performed. Can be the name of the or any other descriptive string. + /// + string Kind { get; set; } + } + + /// + /// Holds the data for an used in the API. + /// + internal record CustomAutomatableInfo(string Id, string Kind); +} diff --git a/Automate/Framework/AutomateAPI.cs b/Automate/Framework/AutomateAPI.cs index 24c82e051..3c8d292b0 100644 --- a/Automate/Framework/AutomateAPI.cs +++ b/Automate/Framework/AutomateAPI.cs @@ -40,6 +40,25 @@ public void AddFactory(IAutomationFactory factory) this.MachineManager.Factory.Add(factory); } + public IEnumerable GetAutomationGroups(GameLocation location, Rectangle? tileArea = null, bool includeDisabled = false) + { + var machineData = this.MachineManager.GetMachineDataFor(location); + if (machineData is null) + { + return []; + } + var foundGroups = machineData.ActiveMachineGroups.AsEnumerable(); + if (includeDisabled) + { + foundGroups = foundGroups.Concat(machineData.DisabledMachineGroups); + } + if (tileArea is not null) + { + foundGroups = foundGroups.Where(group => group.Intersects(group.LocationKey ?? "", tileArea.Value)); + } + return foundGroups.OfType(); + } + /// Get the status of machines in a tile area. This is a specialized API for Data Layers and similar mods. /// The location for which to display data. /// The tile area for which to display data. diff --git a/Automate/Framework/BaseMachine.cs b/Automate/Framework/BaseMachine.cs index d9518563b..60ded4d79 100644 --- a/Automate/Framework/BaseMachine.cs +++ b/Automate/Framework/BaseMachine.cs @@ -13,6 +13,9 @@ internal abstract class BaseMachine : IMachine /********* ** Accessors *********/ + /// + public virtual object? Instance => null; + /// 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; } @@ -133,6 +136,13 @@ internal abstract class BaseMachine : BaseMachine protected TMachine Machine { get; } + /********* + ** Accessors + *********/ + /// + public override object? Instance => this.Machine; + + /********* ** Protected methods *********/ diff --git a/Automate/Framework/IMachineGroup.cs b/Automate/Framework/IMachineGroup.cs index cdc371e58..0447d5405 100644 --- a/Automate/Framework/IMachineGroup.cs +++ b/Automate/Framework/IMachineGroup.cs @@ -36,5 +36,10 @@ internal interface IMachineGroup /// Get the tiles covered by this machine group. /// The location key for which to get tiles. IReadOnlySet GetTiles(string locationKey); + + /// Tests if this group intersects with any tiles in the specified area. + /// The location key in which to search for intersecting tiles. + /// The tile area in which to search. + bool Intersects(string locationKey, Rectangle tileArea); } } diff --git a/Automate/Framework/MachineGroup.cs b/Automate/Framework/MachineGroup.cs index a6ef919bb..578bafdb4 100644 --- a/Automate/Framework/MachineGroup.cs +++ b/Automate/Framework/MachineGroup.cs @@ -13,7 +13,7 @@ namespace Pathoschild.Stardew.Automate.Framework { /// A collection of machines and storage which work as one unit. - internal class MachineGroup : IMachineGroup + internal class MachineGroup : IMachineGroup, IAutomationGroup { /********* ** Fields @@ -72,6 +72,15 @@ internal class MachineGroup : IMachineGroup /// public virtual bool HasInternalAutomation => this.IsJunimoGroup || (this.Machines.Length > 0 && this.Containers.Any(p => !p.IsJunimoChest)); + /// + public string Id => this.GetHashCode().ToString(); + + /// + IReadOnlyList IAutomationGroup.Containers => this.Containers; + + /// + IReadOnlyList IAutomationGroup.Machines => this.Machines; + /********* ** Public methods @@ -103,6 +112,12 @@ public virtual IReadOnlySet GetTiles(string locationKey) : ImmutableHashSet.Empty; } + /// + public bool Intersects(string locationKey, Rectangle tileArea) + { + return locationKey == this.LocationKey && tileArea.GetTiles().Any(this.Tiles.Contains); + } + /// public void Automate() { diff --git a/Automate/Framework/MachineWrapper.cs b/Automate/Framework/MachineWrapper.cs index 35cf50faf..8117b67d2 100644 --- a/Automate/Framework/MachineWrapper.cs +++ b/Automate/Framework/MachineWrapper.cs @@ -10,6 +10,9 @@ internal class MachineWrapper : IMachine /********* ** Accessors *********/ + /// + public virtual object? Instance => this.Machine.Instance; + /// The wrapped machine instance. public IMachine Machine { get; } diff --git a/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs b/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs index 37b55a481..301510bfd 100644 --- a/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs +++ b/Automate/Framework/Machines/Buildings/ShippingBinMachine.cs @@ -22,6 +22,9 @@ internal class ShippingBinMachine : BaseMachine /********* ** Accessors *********/ + /// + public override object? Instance => this.Bin; + /// Get the unique ID for the shipping bin machine. internal static string ShippingBinId { get; } = BaseMachine.GetDefaultMachineId(typeof(ShippingBinMachine)); diff --git a/Automate/Framework/Machines/Objects/MiniShippingBinMachine.cs b/Automate/Framework/Machines/Objects/MiniShippingBinMachine.cs index d7159fd4f..84be711ef 100644 --- a/Automate/Framework/Machines/Objects/MiniShippingBinMachine.cs +++ b/Automate/Framework/Machines/Objects/MiniShippingBinMachine.cs @@ -17,6 +17,13 @@ internal class MiniShippingBinMachine : BaseMachine private readonly IContainer MiniBin; + /********* + ** Accessors + *********/ + /// + public override object? Instance => this.MiniBin.Instance; + + /********* ** Public methods *********/ diff --git a/Automate/Framework/Machines/Tiles/TrashCanMachine.cs b/Automate/Framework/Machines/Tiles/TrashCanMachine.cs index 123577c9d..52f8cd48f 100644 --- a/Automate/Framework/Machines/Tiles/TrashCanMachine.cs +++ b/Automate/Framework/Machines/Tiles/TrashCanMachine.cs @@ -12,7 +12,14 @@ internal class TrashCanMachine : BaseMachine ** Fields *********/ /// The trash can ID. - private readonly string TrashCanId; + private readonly CustomAutomatableInfo TrashCanInfo; + + + /********* + ** Accessors + *********/ + /// + public override object? Instance => this.TrashCanInfo; /********* @@ -25,13 +32,13 @@ internal class TrashCanMachine : BaseMachine public TrashCanMachine(GameLocation location, Vector2 tile, string trashCanId) : base(location, BaseMachine.GetTileAreaFor(tile)) { - this.TrashCanId = this.GetActualTrashCanId(trashCanId); + this.TrashCanInfo = new(this.GetActualTrashCanId(trashCanId), "TrashCan"); } /// Get the machine's processing state. public override MachineState GetState() { - if (Game1.netWorldState.Value.CheckedGarbage.Contains(this.TrashCanId)) + if (Game1.netWorldState.Value.CheckedGarbage.Contains(this.TrashCanInfo.Id)) return MachineState.Processing; return MachineState.Done; @@ -41,7 +48,7 @@ public override MachineState GetState() public override ITrackedStack? GetOutput() { // get item - this.Location.TryGetGarbageItem(this.TrashCanId, Game1.MasterPlayer.DailyLuck, out Item? item, out _, out _); + this.Location.TryGetGarbageItem(this.TrashCanInfo.Id, Game1.MasterPlayer.DailyLuck, out Item? item, out _, out _); if (item != null) return new TrackedItem(item, onEmpty: _ => this.MarkChecked()); @@ -65,7 +72,7 @@ public override bool SetInput(IStorage input) /// Reset the machine, so it starts processing the next item. private void MarkChecked() { - if (Game1.netWorldState.Value.CheckedGarbage.Add(this.TrashCanId)) + if (Game1.netWorldState.Value.CheckedGarbage.Add(this.TrashCanInfo.Id)) Game1.stats.Increment("trashCansChecked"); } diff --git a/Automate/Framework/Storage/ChestContainer.cs b/Automate/Framework/Storage/ChestContainer.cs index 1d301cf07..3cf798710 100644 --- a/Automate/Framework/Storage/ChestContainer.cs +++ b/Automate/Framework/Storage/ChestContainer.cs @@ -36,6 +36,9 @@ internal class ChestContainer : IContainer /// public ModDataDictionary ModData => this.Chest.modData; + /// + public object? Instance => this.Chest; + /// public bool IsJunimoChest => this.Chest.SpecialChestType == Chest.SpecialChestTypes.JunimoChest; diff --git a/Automate/IAutomatable.cs b/Automate/IAutomatable.cs index a0ec02f17..eb300ba6f 100644 --- a/Automate/IAutomatable.cs +++ b/Automate/IAutomatable.cs @@ -9,9 +9,16 @@ public interface IAutomatable /********* ** Accessors *********/ + /// The underlying game object such as or that performs the automation function. + /// A null value may indicate that there is no single/primary participant, or that the automation is implemented in a mod based on an older version of the Automate API. + object? Instance => null; + /// The location which contains the machine. GameLocation Location { get; } + /// Role performed by this instance, if known. + AutomationRole Role => AutomationRole.Unspecified; + /// The tile area covered by the machine. Rectangle TileArea { get; } } diff --git a/Automate/IAutomateAPI.cs b/Automate/IAutomateAPI.cs index 920fc7943..525e94734 100644 --- a/Automate/IAutomateAPI.cs +++ b/Automate/IAutomateAPI.cs @@ -11,6 +11,17 @@ public interface IAutomateAPI /// An automation factory which construct machines, containers, and connectors. void AddFactory(IAutomationFactory factory); + /// Finds all discrete automation groups (connected groups of containers and machines) in a location. + /// + /// Specifying a limits the initial search area, but does not limit the items within a group. + /// and other elements can be outside the requested area as long as at least one element is inside. + /// + /// The location in which to perform the search. + /// Optional tile area to restrict the search. If not specified, all tiles in the are included. + /// Whether or not to include disabled groups in the result. + /// All groups present within the specified and within the given + IEnumerable GetAutomationGroups(GameLocation location, Rectangle? tileArea = null, bool includeDisabled = false); + /// Get the status of machines in a tile area. This is a specialized API for Data Layers and similar mods. /// The location for which to display data. /// The tile area for which to display data. diff --git a/Automate/IAutomationGroup.cs b/Automate/IAutomationGroup.cs new file mode 100644 index 000000000..6059c5cc5 --- /dev/null +++ b/Automate/IAutomationGroup.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Pathoschild.Stardew.Automate +{ + /// + /// Provides details about a group of mutually-connected automation objects. + /// + public interface IAutomationGroup + { + /// + /// A non-persistent ID that identifies the group as a whole. + /// + /// + /// IDs can be used to detect identical groups within the context of a single game session, e.g. to dedupe the results of invoked on multiple tile areas in one location. + /// However, this ID should never be stored in mod data or game data as it is not guaranteed to be stable across game loads. + /// + string Id { get; } + + /// + /// The list of containers, which hold the items that can be consumed by for processing. + /// + IReadOnlyList Containers { get; } + + /// + /// The list of machines, which process the contents of . + /// + IReadOnlyList Machines { get; } + } +}