REPEATFEED RᴇᴘᴇᴀᴛFᴇᴇᴅ RepeatFeed

I might have accidentally pressed “Reply” when I wanted to get back to this window… Well, this is a story to be written, so I’ll update it ASAP.

AI can’t get no sleep

Let’s dig around in AI code for a bit. I want to know how restocking stockpiles actually works.

Spoilers before we start

  • SH has several regions (components); at least region_collision_shape and destination I’ve come across. They are implemented in the engine, but somewhat accessible from lua, where they are little more than fancy Cube3s.
  • Stockpiles utilise the destination region, which has two parts: The destination itself (shown as red when viewing), the reserved region (shown gray) and “adjacent blocks” that are a little hard to describe, but it boils down to that every destination block is either surrounded by other destination blocks or those adjacent ones. They’re not really destinations, but count as adjacent I suppose. Useful for the pathfinder maybe.
  • When an item is dropped down, that block is removed from the stockpile’s destination. When it’s picked up again, the block is added again.
  • When a worker is on its way to a stockpile with an item, that spot is reserved. This should avoid that two items are dropped in the same spot.
  • Whether an item is added or dropped is decided by a tracer on the entity container (a component that handles children) of the root entity. Whenever something is on the floor (as items are), it’s a child of the root entity. Upon being picked up, it’s a child of the worker that carries it. Upon being dropped again, it becomes a child of the root. The stockpile component monitors this to see when an item is added, or removed, from it.
  • The stockpile component is the interface to stockpiles, which are, for all intents and purposes, glorified destinations in my book.
  • AI actions/activities have names that “group” them. We’ll see that when we deal with such an action later on. Basically, the AI/somebody else will say “Execute activity X please”, then the AI gets all activities with that name and somehow weights/prioritises/does evil magic to decide which of these activities is chosen, if any. In the end, I suppose the AI of a single entity looks something like a really really big tree. With all possible actions being executed or stopped or whatever at any time.
  • Actions are injected into entities. This sounds more cruel than it is. But then again, getting a part inserted into you that suddenly takes over and tells you to do stuff does sound kinda evil, so “injected” is suddenly a jolly good term.
  • Compound actions are… group… of… actions. They’re a bit strange (as is the whole AI code, because it’s extremely abstract), but I think it’s a sequential group of actions. You can take outputs from an earlier action and wire it up as input for a new action to execute things in an order. The actions are created as soon as the compound action is, but only executed once it’s their turn. Therefore, many compound actions are “yielding” until an event/external input happens, at which point they pop up again, ready to be continued as soon as the AI scheduler deems that action worthy.
  • The game has another reservation system that is utilised for entities, called “leases”. The AI uses this extensively to avoid doing things with two items at once; for example when picking something up the carrier has a lease on the entity, thereby avoiding that any other entity tries to pick it up. Leases are organized in a [faction][name] = owner_entity kind of style (an entity can have as many leases on other entities as it wishes, but an entity can only be owned once per faction and cause) and the AI always uses the same name so technically, a goblin and a hearthling could have an AI lease on the same entity. This is mostly avoided because…
  • The game has also an idea of ownership (radiant.entities.get_player_id / radiant.entities.set_player_id). This is a string that uniquely identifies a player and (most) AI will only interact with entities that are owned by their master. However, there’s exceptions; harvestable resources can still be harvested, but the produce will be owned by the faction who issued the action. Of course,
  • with the latest alpha, we got the loot tool. The loot tool actually just sets the owner to you and suddenly, there’s interaction going. It’s more of a “This is now mine!” tool than a loot tool, but then again, what is looting if not just that?

Finding what we’re looking for

We open our trusty text editor and search for, dunno, ‘stockpile’. We’re interested in files within stonehearth/ai/actions.

stonehearth/ai/actions/choose_item_in_stockpile_action.lua:2: local ChooseItemInStockpileAction = class() stonehearth/ai/actions/choose_item_in_stockpile_action.lua:3: ChooseItemInStockpileAction.name = 'choose item in stockpile' stonehearth/ai/actions/choose_item_in_stockpile_action.lua:4: ChooseItemInStockpileAction.does = 'stonehearth:choose_item_in_stockpile' stonehearth/ai/actions/choose_item_in_stockpile_action.lua:5: ChooseItemInStockpileAction.args = { stonehearth/ai/actions/choose_item_in_stockpile_action.lua:6: stockpile = Entity, stonehearth/ai/actions/choose_item_in_stockpile_action.lua:14: ChooseItemInStockpileAction.think_output = { stonehearth/ai/actions/choose_item_in_stockpile_action.lua:18: ChooseItemInStockpileAction.version = 2 stonehearth/ai/actions/choose_item_in_stockpile_action.lua:19: ChooseItemInStockpileAction.priority = 1 stonehearth/ai/actions/choose_item_in_stockpile_action.lua:21: function ChooseItemInStockpileAction:start_thinking (ai, entity, args) stonehearth/ai/actions/choose_item_in_stockpile_action.lua:22: local items = args.stockpile:get_component 'stonehearth:stockpile':get_items() stonehearth/ai/actions/choose_item_in_stockpile_action.lua:46: return ChooseItemInStockpileAction stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:3: local DropCarryingInStockpile = class() stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:4: DropCarryingInStockpile.name = 'drop carrying in stockpile' stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:5: DropCarryingInStockpile.does = 'stonehearth:drop_carrying_in_stockpile' stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:6: DropCarryingInStockpile.args = { stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:7: stockpile = Entity, stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:10: DropCarryingInStockpile.version = 2 stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:11: DropCarryingInStockpile.priority = 1 stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:14: return ai:create_compound_action(DropCarryingInStockpile):execute('stonehearth:goto_entity', { stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:15: entity = ai.ARGS.stockpile, stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:18: entity = ai.ARGS.stockpile, stonehearth/ai/actions/drop_carrying_in_stockpile_action.lua:25: obj = ai.ARGS.stockpile:get_component 'stonehearth:stockpile', stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:3: local FindStockpileForBackpackItem = class() stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:4: FindStockpileForBackpackItem.name = 'find stockpile for backpack item' stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:5: FindStockpileForBackpackItem.does = 'stonehearth:find_stockpile_for_backpack_item' stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:6: FindStockpileForBackpackItem.args = {} stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:8: FindStockpileForBackpackItem.think_output = { stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:9: stockpile = Entity, stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:13: FindStockpileForBackpackItem.version = 2 stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:14: FindStockpileForBackpackItem.priority = 1 stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:15: local function get_all_stockpiles_for_entity (entity) stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:19: return player_inventory:get_all_stockpiles() stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:22: function FindStockpileForBackpackItem:start_thinking (ai, entity, args) stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:30: stockpile = path:get_destination(), stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:36: local stockpile_component = candidate:get_component 'stonehearth:stockpile' stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:38: if not stockpile_component or stockpile_component:is_full() then stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:45: local filter_fn = stockpile_component:get_filter() stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:61: function FindStockpileForBackpackItem:stop_thinking (ai, entity, args) stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:67: function FindStockpileForBackpackItem:_create_listeners () stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:68: if not self._stockpile_item_listener then stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:72: self._stockpile_item_listener = radiant.events.listen(inventory, 'stonehearth:inventory:stockpile_added', function () stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:83: function FindStockpileForBackpackItem:_destroy_listeners () stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:84: if self._stockpile_item_listener then stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:85: self._stockpile_item_listener:destroy() stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:86: self._stockpile_item_listener = nil stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:95: function FindStockpileForBackpackItem:_start_searching () stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:111: self._pathfinder = pf:find_path_to_entity_type(ai.CURRENT.location, self._filter_fn, 'find stockpile for backpack', self._solved_cb) stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:114: function FindStockpileForBackpackItem:_stop_searching () stonehearth/ai/actions/find_stockpile_for_backpack_item_action.lua:121: return FindStockpileForBackpackItem stonehearth/ai/actions/move_item_to_available_stockpile.lua:2: StockpileComponent = require 'components.stockpile.stockpile_component' stonehearth/ai/actions/move_item_to_available_stockpile.lua:3: local MoveItemToAvailableStockpile = class() stonehearth/ai/actions/move_item_to_available_stockpile.lua:4: MoveItemToAvailableStockpile.name = 'move item to available stockpile' stonehearth/ai/actions/move_item_to_available_stockpile.lua:5: MoveItemToAvailableStockpile.does = 'stonehearth:clear_workshop' stonehearth/ai/actions/move_item_to_available_stockpile.lua:6: MoveItemToAvailableStockpile.args = { stonehearth/ai/actions/move_item_to_available_stockpile.lua:10: MoveItemToAvailableStockpile.version = 2 stonehearth/ai/actions/move_item_to_available_stockpile.lua:11: MoveItemToAvailableStockpile.priority = 2 stonehearth/ai/actions/move_item_to_available_stockpile.lua:13: function MoveItemToAvailableStockpile:start (ai, entity, args) stonehearth/ai/actions/move_item_to_available_stockpile.lua:14: ai:set_status_text 'moving item to stockpile' stonehearth/ai/actions/move_item_to_available_stockpile.lua:19: return ai:create_compound_action(MoveItemToAvailableStockpile):execute('stonehearth:wait_for_closest_stockpile_space', { stonehearth/ai/actions/move_item_to_available_stockpile.lua:26: entity = ai.BACK(2).stockpile, stonehearth/ai/actions/move_item_to_available_stockpile.lua:29: entity = ai.BACK(3).stockpile, stonehearth/ai/actions/reserve_stockpile_space.lua:3: local ReserveStockpileSpace = class() stonehearth/ai/actions/reserve_stockpile_space.lua:4: ReserveStockpileSpace.name = 'wait for stockpile space' stonehearth/ai/actions/reserve_stockpile_space.lua:5: ReserveStockpileSpace.does = 'stonehearth:wait_for_stockpile_space' stonehearth/ai/actions/reserve_stockpile_space.lua:6: ReserveStockpileSpace.args = { stonehearth/ai/actions/reserve_stockpile_space.lua:7: stockpile = Entity, stonehearth/ai/actions/reserve_stockpile_space.lua:10: ReserveStockpileSpace.think_output = { stonehearth/ai/actions/reserve_stockpile_space.lua:14: ReserveStockpileSpace.version = 2 stonehearth/ai/actions/reserve_stockpile_space.lua:15: ReserveStockpileSpace.priority = 1 stonehearth/ai/actions/reserve_stockpile_space.lua:17: function ReserveStockpileSpace:start_thinking (ai, entity, args) stonehearth/ai/actions/reserve_stockpile_space.lua:20: self._stockpile = args.stockpile:get_component 'stonehearth:stockpile' stonehearth/ai/actions/reserve_stockpile_space.lua:22: self._space_listener = radiant.events.listen(args.stockpile, 'stonehearth:stockpile:space_available', self, self._on_space_available) stonehearth/ai/actions/reserve_stockpile_space.lua:23: self:_on_space_available(self._stockpile, not self._stockpile:is_full()) stonehearth/ai/actions/reserve_stockpile_space.lua:26: function ReserveStockpileSpace:_on_space_available (stockpile, space_available) stonehearth/ai/actions/reserve_stockpile_space.lua:27: if not stockpile or not stockpile:get_entity():is_valid() then stonehearth/ai/actions/reserve_stockpile_space.lua:28: self._log:warning 'stockpile destroyed' stonehearth/ai/actions/reserve_stockpile_space.lua:34: local filter_fn = self._stockpile:get_filter() stonehearth/ai/actions/reserve_stockpile_space.lua:45: function ReserveStockpileSpace:stop_thinking (ai, entity) stonehearth/ai/actions/reserve_stockpile_space.lua:50: return ReserveStockpileSpace stonehearth/ai/actions/restock_items_in_backpack_action.lua:11: return ai:create_compound_action(RestockItemsInBackpack):execute 'stonehearth:find_stockpile_for_backpack_item':execute('stonehearth:follow_path', { stonehearth/ai/actions/restock_items_in_backpack_action.lua:15: entity = ai.BACK(2).stockpile, stonehearth/ai/actions/restock_items_in_backpack_action.lua:19: filter_fn = ai.BACK(3).stockpile:add_component 'stonehearth:stockpile':get_filter(), stonehearth/ai/actions/restock_items_in_backpack_action.lua:25: obj = ai.BACK(5).stockpile:add_component 'stonehearth:stockpile', stonehearth/ai/actions/restock_stockpile_action.lua:2: local RestockStockpile = class() stonehearth/ai/actions/restock_stockpile_action.lua:3: RestockStockpile.name = 'restock stockpile' stonehearth/ai/actions/restock_stockpile_action.lua:4: RestockStockpile.does = 'stonehearth:restock_stockpile' stonehearth/ai/actions/restock_stockpile_action.lua:5: RestockStockpile.args = { stonehearth/ai/actions/restock_stockpile_action.lua:6: stockpile = Entity, stonehearth/ai/actions/restock_stockpile_action.lua:9: RestockStockpile.version = 2 stonehearth/ai/actions/restock_stockpile_action.lua:10: RestockStockpile.priority = 1 stonehearth/ai/actions/restock_stockpile_action.lua:12: function RestockStockpile:start (ai, entity, args) stonehearth/ai/actions/restock_stockpile_action.lua:13: ai:set_status_text('restocking ' .. radiant.entities.get_name(args.stockpile)) stonehearth/ai/actions/restock_stockpile_action.lua:18: return ai:create_compound_action(RestockStockpile):execute('stonehearth:wait_for_stockpile_space', { stonehearth/ai/actions/restock_stockpile_action.lua:19: stockpile = ai.ARGS.stockpile, stonehearth/ai/actions/restock_stockpile_action.lua:25: ):execute('stonehearth:drop_carrying_in_stockpile', { stonehearth/ai/actions/restock_stockpile_action.lua:26: stockpile = ai.ARGS.stockpile, stonehearth/ai/actions/stockpile_arson_action.lua:1: local StockpileArson = class() stonehearth/ai/actions/stockpile_arson_action.lua:2: local StockpileComponent = require 'components.stockpile.stockpile_component' stonehearth/ai/actions/stockpile_arson_action.lua:5: StockpileArson.name = 'stockpile arson' stonehearth/ai/actions/stockpile_arson_action.lua:6: StockpileArson.does = 'stonehearth:stockpile_arson' stonehearth/ai/actions/stockpile_arson_action.lua:7: StockpileArson.args = { stonehearth/ai/actions/stockpile_arson_action.lua:8: stockpile_comp = StockpileComponent, stonehearth/ai/actions/stockpile_arson_action.lua:12: StockpileArson.version = 2 stonehearth/ai/actions/stockpile_arson_action.lua:13: StockpileArson.priority = 1 stonehearth/ai/actions/stockpile_arson_action.lua:15: function stockpile_igniter (stockpile_comp) stonehearth/ai/actions/stockpile_arson_action.lua:16: local fire_effect = radiant.effects.run_effect(stockpile_comp:get_entity(), '/stonehearth/data/effects/firepit_effect') stonehearth/ai/actions/stockpile_arson_action.lua:20: for _, item in pairs(stockpile_comp:get_items()) do stonehearth/ai/actions/stockpile_arson_action.lua:35: return ai:create_compound_action(StockpileArson):execute('stonehearth:goto_location', { stonehearth/ai/actions/stockpile_arson_action.lua:36: reason = 'stockpile arson', stonehearth/ai/actions/stockpile_arson_action.lua:43: fn = stockpile_igniter, stonehearth/ai/actions/stockpile_arson_action.lua:45: ai.ARGS.stockpile_comp, stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:3: local WaitForClosestStockpileSpace = class() stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:4: WaitForClosestStockpileSpace.name = 'wait for closest stockpile space' stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:5: WaitForClosestStockpileSpace.does = 'stonehearth:wait_for_closest_stockpile_space' stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:6: WaitForClosestStockpileSpace.args = { stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:10: WaitForClosestStockpileSpace.think_output = { stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:11: stockpile = Entity, stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:14: WaitForClosestStockpileSpace.version = 2 stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:15: WaitForClosestStockpileSpace.priority = 1 stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:17: function WaitForClosestStockpileSpace:start_thinking (ai, entity, args) stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:20: local stockpiles = stonehearth.inventory:get_inventory(entity):get_all_stockpiles() stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:22: local closest_stockpile stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:24: for id, stockpile_entity in pairs(stockpiles) do stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:25: local stockpile_component = stockpile_entity:get_component 'stonehearth:stockpile' stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:27: if not stockpile_component:is_full() and stockpile_component:can_stock_entity(args.item) then stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:28: local distance_between = radiant.entities.distance_between(entity, stockpile_entity) stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:30: if not closest_stockpile or distance_between < shortest_distance then stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:31: closest_stockpile = stockpile_entity stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:37: if closest_stockpile then stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:39: stockpile = closest_stockpile, stonehearth/ai/actions/wait_for_closest_stockpile_space.lua:47: return WaitForClosestStockpileSpace

Alright, maybe that’s a bit much. There’s other files though which are more obvious.

stonehearth/components/stockpile/stockpile_component.lua:465: self._restock_task = town:create_task_for_group('stonehearth:task_group:restock', 'stonehearth:restock_stockpile', { stonehearth/components/stockpile/stockpile_component.lua:468: ):set_source(self._entity):set_name 'restock task':set_priority(stonehearth.constants.priorities.simple_labor.RESTOCK_STOCKPILE)

Much better. Looking up the definition of that function create_task_for_group, we end up with

stonehearth/services/server/town/town.lua:173: function Town:create_task_for_group (task_group_name, activity_name, args)

Alright, let’s seach for “stonehearth:restock_stockpile” (becuase "stonehearth:task_group:restock" is just the key for the scheduler or something and not found anywhere else in the lua anyway):

stonehearth/ai/actions/restock_stockpile_action.lua:4: RestockStockpile.does = 'stonehearth:restock_stockpile'

Beautiful. We’ve found our entry point.

Hold my llama, I’m going in!

Up to the annoying part. The good news: I think we’ve found our entry point! :clap: We can verify that by looking into mixins/base_human/base_human.json and see that this action is actually injected into all workers, so it’s a match.

(1) stonehearth:restock_stockpile (Entity stockpile) -> (function filter_fn)

The good news: It’s a compound action! The bad news: It’s a compound action. It, itself, does little more than setting the status text (which I suppose is used to indicate what the unit is currently doing - it’s probably polled by the GUI).

The compound itself consists of three actions: stonehearth:wait_for_stockpile_space (which gets passed the stockpile that we got passed), stonehearth:pickup_item_type, which is passed the filter_function from the previous result, and stonehearth:drop_carrying_in_stockpile which gets the stockpile passed again.

(1.1) stonehearth:wait_for_stockpile_space (Entity stockpile)

The first function in our chain is a little more complex - hooray! It reads the stockpile component from the stockpile entity and subscribes to its stonehearth:stockpile:space_available event. This event is called in two cases:

  • When an item was added to a stockpile and the stockpile became full and
  • When an item was removed from a stockpile and the stockpile was full before

Basically, it informs the AI “Hey, you can continue looking to fill this stockpile” and “Stop searching for items for this stockpile, we’re full”.

So this callback function, which is also “fake-called” upon creating the activity, is then doing two things, too:

  • When the stockpile is not full, it will output the stockpile’s filtering function.
  • When the stockpile is full, it will clear its output. That means that the compound action likely falls back to step zero.

(1.2) stonehearth:pickup_item_type (function filter_fn, string description) -> (Entity item)

This action is, as mentioned before, one that has two built-in mappings. filter_fn is a filter; filter_fn(entity) returns something that evaluates to true for all entities that match. In our case, the stockpile’s filter.

(1.2a) PickupItemTypeTrivial (priority 1)

This action is relatively simple: It checks if the unit is currently carrying something and if it is and the item matches, it returns that item immediately.

Otherwise, this action is already over with no result. I suppose? It has literally just the start_thinking definition and nothing else. Poor thing. This isn’t what we’re looking for, however. This action, likely, serves as an emergency bridge: If you remove a stockpile and create a new one (or just update a stockpile’s filter I believe), the workers shouldn’t drop all items and then pick them up again.

(1.2b) PickupItemType (priority 1)

Not trivial this time; it’s a bit more difficult. To start off, if the unit is not carrying anything, the output is set to nil (I suppose?) to start with. Then, surprise, it’s a compound action!

(1.2b.1) stonehearth:goto_entity_type (function filter_fn, string description, number range = 128) -> (Entity output)

You’ve guessed it! We need to go deeper.. At the end, this compound action returns the path that the pathfinder found.

(1.2b.1.1) stonehearth:find_path_to_entity_type (function filter_fn, string description, number range = 128) -> (Entity destination, Path path)

Finally something I can talk about again. We’re now in a compound action inside a compound action inside a compound action, give or take one compound action.

There’s not too much I can talk about however, because it’s more or less just a wrapper of the pathfinder. Upon creating the action, it’s creating the pathfinder and initializes them with the proper settings for this action. As soon as the pathfinder has a solution, the action sets it as output. There’s a few questions that I’m currently not really able to answer, namely: Entities can be reconsidered (so the PF should check them again, which seems a bit hazy at the moment). Does this mean that this action lingers around until all eternity? Is it never suspended? Does it work parallel? Or is the callback actually immediately (and not asynchronous)? But that’s details that would be nice-to-know, but not necessary for our cause.

After it finds a path, it outputs it to

(1.2b.1.2) stonehearth:follow_path (Path path, number stop_distance = 0)

The path from our PF is piped into this function, which just runs along the path. Probably interesting is that this action is utilising some of the asynchronous AI parts (suspend/resume; stop/abort).

(1.2b.2) stonehearth:reserve_entity (Entity entity)

Checks if a lease on this entity can be acquired; if that’s not possible then the AI’s halted.

Upon actively starting to work, the lease for the entity is acquired. When the action is stopped, the lease is released again to avoid items being wrongly reserved.

(1.2b.3) stonehearth:pickup_item_adjacent (Entity item)

One action, two mappings. This time around, they’re prioritised; meaning that in all likelihood, a) is checked first. Should a) not yield any result, the AI will skip to b).

(1.2b.3a) PickupPlacedItemAdjacent (priority 2)

The action for items that are yet to be retrieved (by hammering on them).

If the unit is already carrying something, this action will return immediately (because, well, we can’t disassemble something with both our hands carrying something, right?). The same happens if the entity has no stonehearth:entity_forms component and therefore is not an iconic version (of itself). And even if it has entity forms, but no iconic entity (for some reason), this action is skipped too.

If all that’s okay, we’re telling the AI that the unit’s now carrying something (which is somewhat important because it’s checked at various other places). The body of the action itself then turns to the entity, executes the “work” effect, drops the iconic entity on the floor, executes stonehearth:goto_entity (see 1.3.1) and then calls stonehearth:pickup_item_adjacent again on the entity (i.e. a recursive call). This time, however, this action (1.2b.3a) won’t be called again, because the check if our unit is carrying something will now evaluate to true, therefore aborting the action. It will proceed to…

(1.2b.3b) PickupItemAdjacent (priority 1)

The action for items that are already lying around in the world, which is also called by (1.2.b.3a) once the item has been disassembled.

Just like the trivial item pickup one, if the unit is already carrying one, that’s the item we’ll return (immediately).

Otherwise, there’s a few checks to make sure it’s a valid item (not being carried by someone else; the unit is standing next to it). If it is, the unit turns around to face it, picks it up and runs the “carry_pickup” effect. After that action has finished, we’re finished too.

(1.3) stonehearth:drop_carrying_in_stockpile (Entity stockpile)

Just a reminder, we’ve been in several layers of compound actions, now we’re just in one. But not for long, haha. Ha. :frowning:

(1.3.1) stonehearth:goto_entity (Entity entity, number stop_distance = 128) -> (Point3 point_of_interest)

Did you say compound action? Yes, yes you did.

In the end, this action returns the path from 1.3.1.1.

(1.3.1.1) stonehearth:find_path_to_entity (Entity destination) -> (Path path)

I won’t even pretend I understand half of the stuff here. This action searches a path from here to destination, I suppose, but it seems to involve time travelling and other things that I think it’s safe to assure is not that important right now.

But I’ll try anyway, this is absolutely just guessing: ai.CURRENT.future could depict if this is part of a compound action and therefore initialized too early. If that’s the case, the path finder keeps restarting itself every time the position of our unit changes by more than 2.

In the end, it returns a path that is piped to

(1.3.1.2) stonehearth:follow_path (Path path, number stop_distance = 128)

See 1.2b.1.2.

(1.3.2) stonehearth:reserve_entity_destination (Entity entity, Point3 location)

Interestingly enough, this action needs both the entity (in our case, this is a stockpile) and a location (which was the previous point-of-interest by the pathfinder). Speculation: The pathfinder actually returns as this point any point “inside” the destination region of the entity that was searched. So for the game, everything inside a destination region is considered “at” the entity.

After a few checks (is location really inside of entity's destination region, is that point not reserved yet), the point is reserved. If this action is stopped, the reservation is lifted again.

(1.3.3) stonehearth:drop_carrying_adjacent (Point3 location) -> (Entity item)

Not much to say here; if the unit is carrying anything, that’s set as output already. Once the action starts, a few checks are performed (is this item really carried by unit, are we standing next to location). If that’s all good, turn to location, run the effect “carry_putdown” and after that put the item on the ground (for everyone else).

(1.3.4) stonehearth:call_method (any obj, string method, table args)

Pretty simple: Calls the function named method on obj, passing args as arguments.


And we’re done. That has been an adventure, hasn’t it! The AI is still a riddle wrapped up in an enigma, but it seems to work, right? Kind of?

10 Likes