Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add read-only API for querying automation groups. #1019

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Automate/AutomationRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Pathoschild.Stardew.Automate
{
/// <summary>
/// Describes the role of an <see cref="IAutomatable"/>.
/// </summary>
public enum AutomationRole
{
/// <summary>
/// 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.
/// </summary>
Unspecified = 0,

/// <summary>
/// A container, such as a chest, from which items can be taken for processing.
/// </summary>
Container,

/// <summary>
/// A connector that passively links other <see cref="IAutomatable"/> objects together.
/// </summary>
Connector,

/// <summary>
/// A machine that processes items in a <see cref="Container"/>.
/// </summary>
Machine,
}
}
27 changes: 27 additions & 0 deletions Automate/CustomAutomatableInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Pathoschild.Stardew.Automate
{
/// <summary>
/// Provides information about an automatable type (typically an ad-hoc "machine", such as
/// <see cref="Framework.Machines.Tiles.TrashCanMachine"/>) that does not correspond to an addressable game object.
/// </summary>
/// <remarks>
/// Can be used as the <see cref="IAutomatable.Instance"/> when no other type would be applicable.
/// </remarks>
public interface ICustomAutomatableInfo
{
/// <summary>
/// A unique (per save) identifier for this instance.
/// </summary>
string Id { get; set; }

/// <summary>
/// Describes the kind of automation performed. Can be the name of the <see cref="System.Type"/> or any other descriptive string.
/// </summary>
string Kind { get; set; }
}

/// <summary>
/// Holds the data for an <see cref="ICustomAutomatableInfo"/> used in the API.
/// </summary>
internal record CustomAutomatableInfo(string Id, string Kind);
}
19 changes: 19 additions & 0 deletions Automate/Framework/AutomateAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ public void AddFactory(IAutomationFactory factory)
this.MachineManager.Factory.Add(factory);
}

public IEnumerable<IAutomationGroup> 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<IAutomationGroup>();
}

/// <summary>Get the status of machines in a tile area. This is a specialized API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
Expand Down
10 changes: 10 additions & 0 deletions Automate/Framework/BaseMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ internal abstract class BaseMachine : IMachine
/*********
** Accessors
*********/
/// <inheritdoc/>
public virtual object? Instance => null;

/// <summary>A unique ID for the machine type.</summary>
/// <remarks>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.</remarks>
public string MachineTypeID { get; protected set; }
Expand Down Expand Up @@ -133,6 +136,13 @@ internal abstract class BaseMachine<TMachine> : BaseMachine
protected TMachine Machine { get; }


/*********
** Accessors
*********/
/// <inheritdoc/>
public override object? Instance => this.Machine;


/*********
** Protected methods
*********/
Expand Down
5 changes: 5 additions & 0 deletions Automate/Framework/IMachineGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,10 @@ internal interface IMachineGroup
/// <summary>Get the tiles covered by this machine group.</summary>
/// <param name="locationKey">The location key for which to get tiles.</param>
IReadOnlySet<Vector2> GetTiles(string locationKey);

/// <summary>Tests if this group intersects with any tiles in the specified area.</summary>
/// <param name="locationKey">The location key in which to search for intersecting tiles.</param>
/// <param name="tileArea">The tile area in which to search.</param>
bool Intersects(string locationKey, Rectangle tileArea);
}
}
17 changes: 16 additions & 1 deletion Automate/Framework/MachineGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>A collection of machines and storage which work as one unit.</summary>
internal class MachineGroup : IMachineGroup
internal class MachineGroup : IMachineGroup, IAutomationGroup
{
/*********
** Fields
Expand Down Expand Up @@ -72,6 +72,15 @@ internal class MachineGroup : IMachineGroup
/// <inheritdoc />
public virtual bool HasInternalAutomation => this.IsJunimoGroup || (this.Machines.Length > 0 && this.Containers.Any(p => !p.IsJunimoChest));

/// <inheritdoc />
public string Id => this.GetHashCode().ToString();

/// <inheritdoc />
IReadOnlyList<IAutomatable> IAutomationGroup.Containers => this.Containers;

/// <inheritdoc />
IReadOnlyList<IAutomatable> IAutomationGroup.Machines => this.Machines;


/*********
** Public methods
Expand Down Expand Up @@ -103,6 +112,12 @@ public virtual IReadOnlySet<Vector2> GetTiles(string locationKey)
: ImmutableHashSet<Vector2>.Empty;
}

/// <inheritdoc />
public bool Intersects(string locationKey, Rectangle tileArea)
{
return locationKey == this.LocationKey && tileArea.GetTiles().Any(this.Tiles.Contains);
}

/// <inheritdoc />
public void Automate()
{
Expand Down
3 changes: 3 additions & 0 deletions Automate/Framework/MachineWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ internal class MachineWrapper : IMachine
/*********
** Accessors
*********/
/// <inheritdoc/>
public virtual object? Instance => this.Machine.Instance;

/// <summary>The wrapped machine instance.</summary>
public IMachine Machine { get; }

Expand Down
3 changes: 3 additions & 0 deletions Automate/Framework/Machines/Buildings/ShippingBinMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ internal class ShippingBinMachine : BaseMachine
/*********
** Accessors
*********/
/// <inheritdoc/>
public override object? Instance => this.Bin;

/// <summary>Get the unique ID for the shipping bin machine.</summary>
internal static string ShippingBinId { get; } = BaseMachine.GetDefaultMachineId(typeof(ShippingBinMachine));

Expand Down
7 changes: 7 additions & 0 deletions Automate/Framework/Machines/Objects/MiniShippingBinMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ internal class MiniShippingBinMachine : BaseMachine
private readonly IContainer MiniBin;


/*********
** Accessors
*********/
/// <inheritdoc/>
public override object? Instance => this.MiniBin.Instance;


/*********
** Public methods
*********/
Expand Down
17 changes: 12 additions & 5 deletions Automate/Framework/Machines/Tiles/TrashCanMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ internal class TrashCanMachine : BaseMachine
** Fields
*********/
/// <summary>The trash can ID.</summary>
private readonly string TrashCanId;
private readonly CustomAutomatableInfo TrashCanInfo;


/*********
** Accessors
*********/
/// <inheritdoc/>
public override object? Instance => this.TrashCanInfo;


/*********
Expand All @@ -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");
}

/// <summary>Get the machine's processing state.</summary>
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;
Expand All @@ -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());

Expand All @@ -65,7 +72,7 @@ public override bool SetInput(IStorage input)
/// <summary>Reset the machine, so it starts processing the next item.</summary>
private void MarkChecked()
{
if (Game1.netWorldState.Value.CheckedGarbage.Add(this.TrashCanId))
if (Game1.netWorldState.Value.CheckedGarbage.Add(this.TrashCanInfo.Id))
Game1.stats.Increment("trashCansChecked");
}

Expand Down
3 changes: 3 additions & 0 deletions Automate/Framework/Storage/ChestContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ internal class ChestContainer : IContainer
/// <inheritdoc />
public ModDataDictionary ModData => this.Chest.modData;

/// <inheritdoc />
public object? Instance => this.Chest;

/// <inheritdoc />
public bool IsJunimoChest => this.Chest.SpecialChestType == Chest.SpecialChestTypes.JunimoChest;

Expand Down
7 changes: 7 additions & 0 deletions Automate/IAutomatable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ public interface IAutomatable
/*********
** Accessors
*********/
/// <summary>The underlying game object such as <see cref="StardewValley.Object"/> or <see cref="StardewValley.TerrainFeatures.TerrainFeature"/> that performs the automation function.</summary>
/// <remarks>A <c>null</c> 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.</remarks>
object? Instance => null;

/// <summary>The location which contains the machine.</summary>
GameLocation Location { get; }

/// <summary>Role performed by this instance, if known.</summary>
AutomationRole Role => AutomationRole.Unspecified;

/// <summary>The tile area covered by the machine.</summary>
Rectangle TileArea { get; }
}
Expand Down
11 changes: 11 additions & 0 deletions Automate/IAutomateAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ public interface IAutomateAPI
/// <param name="factory">An automation factory which construct machines, containers, and connectors.</param>
void AddFactory(IAutomationFactory factory);

/// <summary>Finds all discrete automation groups (connected groups of containers and machines) in a location.</summary>
/// <remarks>
/// Specifying a <paramref name="tileArea"/> limits the initial search area, but does not limit the items within a group.
/// <see cref="IAutomationGroup.Machines"/> and other elements can be outside the requested area as long as at least one element is inside.
/// </remarks>
/// <param name="location">The location in which to perform the search.</param>
/// <param name="tileArea">Optional tile area to restrict the search. If not specified, all tiles in the <paramref name="location"/> are included.</param>
/// <param name="includeDisabled">Whether or not to include disabled groups in the result.</param>
/// <returns>All groups present within the specified <paramref name="location"/> and within the given <paramref name="tileArea"/></returns>
IEnumerable<IAutomationGroup> GetAutomationGroups(GameLocation location, Rectangle? tileArea = null, bool includeDisabled = false);

/// <summary>Get the status of machines in a tile area. This is a specialized API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
Expand Down
29 changes: 29 additions & 0 deletions Automate/IAutomationGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;

namespace Pathoschild.Stardew.Automate
{
/// <summary>
/// Provides details about a group of mutually-connected automation objects.
/// </summary>
public interface IAutomationGroup
{
/// <summary>
/// A <b>non-persistent</b> ID that identifies the group as a whole.
/// </summary>
/// <remarks>
/// IDs can be used to detect identical groups within the context of a single game session, e.g. to dedupe the results of <see cref="IAutomateAPI.GetAutomationGroups"/> 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.
/// </remarks>
string Id { get; }

/// <summary>
/// The list of containers, which hold the items that can be consumed by <see cref="Machines"/> for processing.
/// </summary>
IReadOnlyList<IAutomatable> Containers { get; }

/// <summary>
/// The list of machines, which process the contents of <see cref="Containers"/>.
/// </summary>
IReadOnlyList<IAutomatable> Machines { get; }
}
}