you just make my rails and minecart go to hell
I’m eating a white chocolate and honeycomb cookie at the moment, crazy what they do these days.
Is there something I do not understand but should?
"Twas another fine day in the land of Stonehearth with the sun shining bright and the birds singing merrily from a tree. The squirrels and rabbits hopped gleefully through the grass, whilst workers picked the sweetest ripest berries you ever did see. Nothing could break the peaceful bliss that enveloped this bountiful wilderness.
But wait, what was that i hear in the distance …whooooooooooooo…whooooooooo…"
Also nice work, Its about time stonehearth entered the industrial revolution, goblins are no match for the combustion engine.
If a train did get put in the game, with conductor and ticketer classes added (LOL), so going to build a train station.
If not Someone Mod that in. And change the way villagers show up if you do, They come by train instead of just warping in and running to banner
“A new villager want to join your town” Accept or Decline
“Accept”
Whooooo…WHOOOooooooo
Here comes the Train
Yeah, imagine if there was a collaboration currently developing a total era conversion for SH that did exactly that!..
what would be even better, if stonehearth had through the ages upgrades… so like say for example, you have a tech tree thingy and to get out of the stone age you have to have certain buildings/techs, to enter the iron age etc… all the way to the modern age and/or beyond!
also, how on earth did you build that, or is it a mod?
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
anddestination
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! 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.
(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?
after this one i need to ask, are you alright?
@albert once said that he never really ran out of horizontal room in sublime till he started working on AI. Gee, I wonder why that is?
My $0.02: one thing that’s really useful to know about the AI is that in an action tree, the thinking actions are non-blocking. They go on in the background. Only if all the thinking actions complete successfully (ie, set think output) do the run actions happen. The run actions ARE blocking so never do any long-running anything in there or the hearthlings will stand around looking at nothing. For example, if you’re looking for something to pick up, you must find a suitable object and then find a path to it and then reserve it before you actually try to run to it.
Another really useful thing to know about the AI: if you override (write) start_thinking and DO NOT set_think_output, the action will hang around forever (unless some other route down the tree completes and the tree is terminated). Sometimes, you get re-entrant behavior, if you’re lucky and the action manager mulligans the AI stack, but if you’re trying to run an action and realize it’s never going to complete successfully, now but you’d like it to try again, then make sure you have a listener hanging out to catch some event that forces start-thinking to happen again. This has gotten me SO many times.
Lastly, dunno if this was clear from RP’s description, multiple actions can implement the same triggers. The trigger an action implements is defined in it’s “does” parameter. If multiple actions implement a trigger, then there’s a priority run off to determine which one runs. If they all have the same priority, then it depends on which one’s start_thinking returns first.
Edit: Earlier this year, I put together a higher-level look at the AI. You can find it here; relevant bits are in the comments section of each slide: Working on Stonehearth: AI - Google Slides
After this rather abstract jump into AI, which I’m sure we’ll get into later on again, let’s have some generic thinking instead. Something easier to digest.
For the next three posts in RepeatFeed, I will give you three pieces and to combine them, you will need your right brain. So, right brain training time. Also, Shiba Inus.
Don’t ever ask me where I got all these videos from. I don’t know either.
I still have no clue what is going on here…
[ @Geoffers747, did I somehow get lost on my way to the modding section and get stuck in the world of terror known as the map game that you warned me about?]
Two things:
- I think @RepeatPan is really, really hype because he have done something quite incredible (last time he did this day 1, day 2 day… thing we ended up with a flying unicorn)
- Ooooh interesting
Something that starts with A, eh?
A block of land?
A chunk of land?
A floating is-land?
A place for a helicopter to land?
Wait, why do I think it ends with land? And more importantly, what am I doing here? I still have not figured that one out.