Skip to content

Latest commit

 

History

History
475 lines (381 loc) · 19 KB

technical.md

File metadata and controls

475 lines (381 loc) · 19 KB

This document provides technical info about the Automate mod. For more general info, see the README file instead.

Contents

FAQs

What's the order of processed machines?

See machine priority in the README.

What's the order of items taken from chests?

For each machine, the available chests are scanned until Automate finds enough items to fill a recipe for that machine. If multiple chests are connected, they're essentially combined into one inventory in discovery order (so items may be taken from multiple chests simultaneously).

For example, let's say you have one chest containing these item stacks:

1× coal
3× copper ore
3× iron ore
2× copper ore
2× iron ore

A furnace has two recipes with those ingredients: coal + 5× copper ore = copper bar, and coal + 5× iron ore = iron bar. Automate will scan the items from left to right and top to bottom, and collect items until it has a complete recipe. In this case, the furnace will start producing a copper bar:

  1. Add coal from first stack (two unfinished recipes):
    coal + 0 of 5× copper ore = copper bar
    coal + 0 of 5× iron ore = iron bar
  2. Add 3× copper ore from second stack (two unfinished recipes):
    coal + 3 of 5× copper ore = copper bar
    coal + 0 of 5× iron ore = iron bar
  3. Add 3× iron ore from third stack (two unfinished recipes):
    coal + 3 of 5× copper ore = copper bar
    coal + 3 of 5× iron ore = iron bar
  4. Add 2× copper ore from fourth stack (one recipe filled):
    coal + 5× copper ore = copper bar
    coal + 3 of 5× iron ore = iron bar

Which chest will machine output go into?

The connected chests are prioritised in this order:

  1. chests with the "Put items in this chest first" option (see in-game settings in the README);
  2. chests which already contain an item of the same type;
  3. any chest.

Chests with the same priority are sorted by discovery order.

Can I change in-game settings without Chests Anywhere?

Normally you'd change how chests are automated through Chests Anywhere's chest options UI:

If you don't want Chests Anywhere's functionality, you have a few options:

  • You can install Chests Anywhere, but set the "Range": "None" option in its config.json. That will let you edit an opened chest, but you won't have any option to navigate chests or open them remotely.
  • Or you can install Chests Anywhere temporarily, set the options you want and save, then remove it. The options will still work after it's uninstalled.
  • Or you can edit your save file manually and set the chest options directly. That's more complicated, but you can ask for help in #making-mods on Discord if you're interested.

Extensibility for modders

See core concepts before reading this section.

APIs

Automate has a mod-provided API you can use to add custom machines, containers, and connectors.

Access the API

To access the API:

  1. Add a reference to the Automate.dll file. Make sure it's not copied to your build output.
  2. Hook into SMAPI's GameLoop.GameLaunched event and get a copy of the API:
    IAutomateAPI automate = this.Helper.ModRegistry.GetApi<IAutomateAPI>("Pathoschild.Automate");
  3. Use the API to extend Automate (see below).

Add connectors, containers, and machines

You can add automatables by implementing an IAutomationFactory. Automate will handle the core logic (like finding entities, linking automatables into groups, etc); you just need to return the automatable for a given entity. You can't change the automation for an existing automatable though; if Automate already has an automatable for an entity, it won't call your factory.

First, let's create a basic machine that transmutes an iron bar into gold in two hours:

using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Automate.Framework;
using StardewValley;
using SObject = StardewValley.Object;

namespace YourModName
{
    /// <summary>A machine that turns iron bars into gold bars.</summary>
    public class TransmuterMachine : IMachine
    {
        /*********
        ** Fields
        *********/
        /// <summary>The underlying entity.</summary>
        private readonly SObject Entity;


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

        /// <summary>The tile area covered by the machine.</summary>
        public Rectangle TileArea { get; }

        /// <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>
        string MachineTypeID { get; } = "YourModId/Transmuter";


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="entity">The underlying entity.</param>
        /// <param name="location">The location which contains the machine.</param>
        /// <param name="tile">The tile covered by the machine.</param>
        public TransmuterMachine(SObject entity, GameLocation location, in Vector2 tile)
        {
            this.Entity = entity;
            this.Location = location;
            this.TileArea = new Rectangle((int)tile.X, (int)tile.Y, 1, 1);
        }

        /// <summary>Get the machine's processing state.</summary>
        public MachineState GetState()
        {
            if (this.Entity.heldObject.Value == null)
                return MachineState.Empty;

            return this.Entity.readyForHarvest.Value
                ? MachineState.Done
                : MachineState.Processing;
        }

        /// <summary>Get the output item.</summary>
        public ITrackedStack GetOutput()
        {
            return new TrackedItem(this.Entity.heldObject.Value, onEmpty: item =>
            {
                this.Entity.heldObject.Value = null;
                this.Entity.readyForHarvest.Value = false;
            });
        }

        /// <summary>Provide input to the machine.</summary>
        /// <param name="input">The available items.</param>
        /// <returns>Returns whether the machine started processing an item.</returns>
        public bool SetInput(IStorage input)
        {
            if (input.TryGetIngredient(SObject.ironBar, 1, out IConsumable ingredient))
            {
                ingredient.Take();
                this.Entity.heldObject.Value = ItemRegistry.Create<SObject>(SObject.goldBarQID);
                this.Entity.MinutesUntilReady = 120;
                return true;
            }

            return false;
        }
    }
}

Next, let's create a factory which returns the new machine. This example assumes you've added an in-game object with ID Example.ModId_Transmuter that you want to automate.

using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;

namespace YourModName
{
    public class MyAutomationFactory : IAutomationFactory
    {
        /// <summary>Get a machine, container, or connector instance for a given object.</summary>
        /// <param name="obj">The in-game object.</param>
        /// <param name="location">The location to check.</param>
        /// <param name="tile">The tile position to check.</param>
        /// <returns>Returns an instance or <c>null</c>.</returns>
        public IAutomatable GetFor(SObject obj, GameLocation location, in Vector2 tile)
        {
            if (obj.QualifiedItemId == "(BC)Example.ModId_Transmuter")
                return new TransmuterMachine(obj, location, tile);

            return null;
        }

        /// <summary>Get a machine, container, or connector instance for a given terrain feature.</summary>
        /// <param name="feature">The terrain feature.</param>
        /// <param name="location">The location to check.</param>
        /// <param name="tile">The tile position to check.</param>
        /// <returns>Returns an instance or <c>null</c>.</returns>
        public IAutomatable GetFor(TerrainFeature feature, GameLocation location, in Vector2 tile)
        {
            return null;
        }

        /// <summary>Get a machine, container, or connector instance for a given building.</summary>
        /// <param name="building">The building.</param>
        /// <param name="location">The location to check.</param>
        /// <param name="tile">The tile position to check.</param>
        /// <returns>Returns an instance or <c>null</c>.</returns>
        public IAutomatable GetFor(Building building, GameLocation location, in Vector2 tile)
        {
            return null;
        }

        /// <summary>Get a machine, container, or connector instance for a given tile position.</summary>
        /// <param name="location">The location to check.</param>
        /// <param name="tile">The tile position to check.</param>
        /// <returns>Returns an instance or <c>null</c>.</returns>
        /// <remarks>Shipping bin logic from <see cref="Farm.leftClick"/>, garbage can logic from <see cref="Town.checkAction"/>.</remarks>
        public IAutomatable GetForTile(GameLocation location, in Vector2 tile)
        {
            return null;
        }
    }
}

And finally, add your factory to the automate API (see access the API above):

IAutomateAPI automate = ...;
automate.AddFactory(new MyAutomationFactory());

That's it! When Automate scans a location for automatables, it'll call your GetFor method and add your custom machine to its normal automation.

Chest automation options

You can change how Automate uses a chest by editing its modData field. See mod integrations in the Chests Anywhere docs for more info.

Custom chest capacity

Automate uses the value returned by chest.GetActualCapacity(). You can override or patch that method, and Automate will update automatically.

Patch Automate

When all else fails, you can patch Automate's logic using Harmony.

To simplify patching, Automate also wraps all machines with a MachineWrapper instance, so you can hook one place to change any machine's input, output, or processing logic. For example, you can patch GetOutput to adjust machine output, SetInput to add custom recipes if none of the vanilla recipes matched, etc.

Before you patch Automate, please consider these best practices:

  1. This is strongly discouraged in most cases. Patching Automate makes both Automate and your mod more fragile and likely to break. Only do this if you can't do it any other way.

  2. Inside your patch methods, wrap the code in a try..catch and log your own exception. That way players won't report errors on the Automate page instead. You should also fallback to running the original method, so errors in your code don't break Automate.

  3. When your mod starts, log a clear message indicating that your mod patches Automate. This simplifies troubleshooting and avoids confusion. For example:

    if (helper.ModRegistry.IsLoaded("Pathoschild.Automate"))
       this.Monitor.Log("This mod patches Automate. If you notice issues with Automate, make sure it happens without this mod before reporting it to the Automate page.", LogLevel.Debug);

    It doesn't have to be player-visible, even a TRACE-level message is useful when helping a player troubleshoot.

Implementation details

This section provides a view of Automate's inner workings for the curious. You can safely skip it if you're not interested.

Core concepts

These are the core concepts in the Automate design used in the rest of the technical details:

Entity
A placed item, terrain feature, building, or other in-game thing.
Connector
Something which can be added to a machine group (thus extending its range), but otherwise has no logic of its own. It has no state, input, or output.
Container
Something which stores and retrieves items (usually a chest).
Machine
Something which has a state (e.g. empty or processing) and accepts input, provides output, or both. This doesn't need to be a 'machine' in the gameplay sense; Automate provides default machines for shipping bins and fruit trees, for example.
Machine group

A set of machines, containers, and connectors which are chained together. You can press U in-game (configurable) to see machine groups highlighted in green. For example, these are two machine groups:

Machine logic

The game has no concept of machines. For example, nothing prevents a pumpkin from producing output, the game is just hardcoded to detect when the player interacts with certain object types to set their output. Trash cans and the default shipping bin don't even exist in-game, except as tiles on the map.

Automate handles this by wrapping entities into machine instances. These reuse game logic when possible, but usually they need to reimplement it in an automateable way. For example, the logic for kegs is implemented by KegMachine. See Framework/Machines in Automate's code for a list of machines directly supported by Automate.

To map entities to machines/containers/connectors, Automate defines a default automation factory — a standardized way to get a container/machine/connector from a tile/object/building/terrain feature. An automation factory essentially has logic like this:

if (building is FishPond pond)
    return new FishPondMachine(pond, location);
if (building is Mill mill)
    return new MillMachine(mill, location);
...

Other mods can add their own automation factories, to enable automation for their own machines. See APIs above for an example automation factory and machine instance.

Machine scanning

Scanning is when Automate collects all the entities in a location and combines them into machine groups for automation. This has a few steps:

1. Index the location.

The game doesn't have an efficient way to know if a tile is covered by some entities, like buildings. You need to iterate through the building list, calculate each building's area from its position and size, and see if that area contains the tile. Each location can have tens of thousands of tiles, so doing that for each tile can cause noticeable lag for players.

So Automate first creates an optimized lookup for the location's entities by tile position, internally called the index. For example:

machine layout index

(31, 11) ────────┐
(32, 11) ────────┤
(33, 11) ────────┤
(34, 11) ────────┤
(35, 11) ────────┼────> Fish Pond
(36, 11) ────────┤
(31, 12) ────────┤
...              │
(36, 15)─────────┘
(36, 16)──────────────> Chest

Note that the index only cares about tile positions, it has no concept of connections between the fish pond and chest.

2. Find a connected tile.

Automate checks each tile on the map looking for an entity in the index. It starts from the top-left corner, works its way to the right, then wraps to the left edge of the next row. It does this until it encounters a valid chest, machine, or connector.

For example, Automate will find a connected entity (the fish pond) on tile (31, 11):

3. Flood fill machine group.

Next Automate collects all the entities connected (directly or indirectly) to that tile. It does this using an entity-aware flood fill algorithm. Here entity-aware means it knows that (31, 11) is part of a larger entity covering multiple tiles (so it doesn't need to scan those tiles separately), and flood fill is an approach where it recursively scans all the tiles around a matching tile.

For example, here's how Automate would scan the tiles:

At that point it can build a machine group, which represents an optimized view of the entities:

{
   Machines: [ FishPond ],
   Containers: [ Chest ],
   Tiles: [ (31, 11), (32, 11), (33, 11), ..., (36, 16) ],
   HasInternalAutomation: true
}

Notes:

  • Once a machine group is constructed, the tile positions of each machine and container are no longer relevant; it's just a list of machines/containers that are linked together.
  • Machine priority is applied here (it's just a sort on the machine list).
  • The HasInternalAutomation flag indicates whether the machine can be automated. For example, a group containing a chest but no machines can't be automated, so the group will just be discarded when Automate finishes scanning.

(This works for any number of connected entities, not just two like in this example.)

4. Resume from step 2.

Finally Automate resumes from step 2 until all tiles in the location have been scanned, skipping tiles that have already been visited:

Discovery order

Discovery order is the order that Automate finds entities when scanning a location. This affects the order of many other things. For example, the order that machines process recipes is directly linked to the discovery order of their connected chests.

See also