Detecting reason (success/fail) for cancelling tasks and my further scripting problems

So I wasted two evenings trying to code autoharvest properly but I found no way to complete the mechanism. The problem is the same command is used to cancel a task when it is complete and when it was cancelled without completion.

Autoharvest Loop

The red part is what breaks the loop. For now I use modified task tracker component which allows looping a task until its done so harvest fired via autoharvest module simply cannot be cancelled, but it is not a proper way to solve the problem.

3 Likes

Task has notify_completed() and notify_interrupted() callbacks. Are those not sufficient for your needs?

Although now that I think of it, you are talking about a specific instance of harvesting, not the general harvesting task that lives forever. You can probably mixin an AI action to replace HarvestRenewableResource that does the same thing but also triggers an event afterwards. This wouldn’t be future proof, but should work for the current alpha.

3 Likes

Ugh, searching for notify_completed() in stonehearth.smod and radiant.smod returned no matches. How can I learn how these work?

1 Like

stonehearth/components/trapping/trapping_grounds_component.lua has an example of notify_completed(), but as per my edit above, I don’t think it’s gonna work for your case since the harvest task is global rather than per-instance. Modifying the task tracker as you are doing or the action to trigger an event when it finishes is probably the way to go.

2 Likes

My bad, I forgot to disable RegEx so the brackets made the search fail.

I haven’t worked with AI actions yet. I don’t understand how firing an event when AI action is finished works: does it fire whenever an action ends or when it reaches the end, that is: when it’s completed successfully?

EDIT: Another way to do that: all successful harvests end in the node made unharvestable. An option to consider is to make TaskTracker fire an event whenever a task is cancelled (that would be a lot of events, wouldn’t that hit the performance?) with args = {entity, task_name, category, player_id} and listen to that event. If we’re still harvestable when it fired we should stop autoharvesting. Forcing RenewableResourceNodeComponent:spawn_resource() which cancels the task first and then marks the node unharvestable (why is that? in case task cancellation fails?) to do so in reverse order would do the trick.

1 Like

You decide when it does. A simple way is to add something like this at the very end of harvest_renewable_resource.lua:

         :execute('stonehearth:trigger_event', {
            source = ai.ENTITY,
            event_name = 'mymod:harvesting:done'
         })

Then once all the harvesting logic is done (successfully), it’ll trigger that event.

The AI is probably the most complicated system we have, and the documentation isn’t great, so sorry about that, but perhaps we can release some more detailed docs with the AI refactor that’s going into A23.

2 Likes

I still don’t understand one very basic thing about compound actions: are all the commands merged into one action (so a trigger put at the end would fire when an AI starts the whole action) or fired in a cascade, one after another when the previous one ends (so a trigger put at the end would fire when an AI ends the last action before the trigger)?

I’m not sure how any of these would help me because there are easier ways to know when a harvest was requested (*cough* modifying RenewableResourceNodeComponent:request_harvest() *cough*) or ended successfully (stonehearth:gather_renewable_resource event is fired in harvest_renewable_resource_adjacent.lua) and I need to know when it was cancelled. Even if I know when attempting to harvest the node started I won’t be able to find out when it is cancelled because there is no event to listen to.

The TaskTracker way is not optimal because there are some restock/task tracker shenanigans - try tasking undeploy, move and harvest on the same object few times in a short time (so none of the tasks is finished) and random order. Most of the time Hearthlings will undeploy the object, no matter which task was ordered last. I’ve already modified renewable resource node harvest to cancel all tasks (resource node does that while the renewable one doesn’t) via stonehearth.town:get_town(player_id):remove_previous_task_on_item(self._entity) so it should not be a problem. Changing tasks while the game is paused seems to intensify the problem.

2 Likes

I still don’t understand one very basic thing about compound actions: are all the commands merged into one action (so a trigger put at the end would fire when an AI starts the whole action) or fired in a cascade, one after another when the previous one ends (so a trigger put at the end would fire when an AI ends the last action before the trigger)?

AI actions have a planning phase and a running phase. In a compound action, each step is planned (“thinked”) in order, then when the actions is chosen to run, each step is executed in order. The trigger only has a running phase, so it won’t happen until the previous step (harvest_renewable_resource_adjacent) successfully finishes.

1 Like

The last action already fires stonehearth:gather_renewable_resource event.

EDIT: some Discourse bug replaced my post with my next reply.

1 Like

In that case you can just listen to that event?

1 Like

So I do. The problem is I need to know when harvest task is cancelled without completion. As I wrote:

But what about performance here (tasks are cancelled very often and triggering it only if requested action is a harvest, while effective, is a very inelegant workaround as the function has to be altered every time a cancellation of a different task is required) and is there a reason for such order in RenewableResourceNodeComponent:spawn_resource()?

1 Like

Firing an event (or a couple dozen) in response to a player action is insignificant performance-wise. Internal systems can fire hundreds of events every frame. What can be slow is listening to these events with expensive handlers in many places at once.

2 Likes

So I’ll add self._sv.task_successful to TaskTracker and self:mark_current_task_successful() function setting it to true. Cancelling a task would fire an event with it listed in args.

I thought the challenge here would be to track all the functions cancelling tasks on completion and inject it into them. When I searched for cancel_current_task() I found only 6 files. How often is the task tracker used as of now? Are there any other tasks than harvests, placing an item and looting?

2 Likes

How often is the task tracker used as of now?

It’s mainly used for displaying the toasts above things that have some task issued on them. The name is confusing; it is not technically related to the task system.

Are there any other tasks than harvests, placing an item and looting?

One off the top of my head is rescuing. There might be more.

2 Likes

One side question: does listen_once completely destroy itself after a matching event is detected? In such case I wouldn’t need to specifically destroy listeners, what would simplify the code a bit.

After examining the code: renew detection loop doesn’t require a listener because as of now the only way to fire both events is via renew/spawn command and I have to mixin a bit of code into RenewableResourceNodeComponent anyway (as of now tasks are not cancelled by request_harvest() and depleted model/description is not applied if the node starts unharvestable, consider it a bug report). I wanted to use stonehearth:gathered_renewable_resource event but it is fired just after spawn_resource() which triggers stonehearth:renewable_resource_spawned. This means both renewal and depleting the node trigger an event inside RenewableResourceNodeComponent so it is not necessary to listen to these if I add one more saved variable to the node.

My last question is: is there any case of purposefully ordering the AI to perform/cancel an action which has a matching entry (harvest/placement/rescue) in TaskTracker without creating/deleting that entry? To put it another way: is it safe to assume that all harvest/placement/rescue actions and their cancellations are noticed by the TaskTracker? Monitoring the TaskTracker makes sense only if the tracker itself monitors all the corresponding actions, otherwise there would be information leaks leading to bugs.

1 Like

does listen_once completely destroy itself after a matching event is detected?

Yes, but you still need to clean up the listener in case the event never happened.

is it safe to assume that all harvest/placement/rescue actions and their cancellations are noticed by the TaskTracker?

TaskTracker itself is pretty much inert. Except for cancel_task_if_entity_moves(), it doesn’t change state unless someone calls methods on it. In the case of unmodded RenewableResourceNode, it doesn’t look like harvesting is ever enabled/disabled without notifying the task tracker.

2 Likes

So I have two major problems now.

  1. Listeners are not created at all (Lua console returns nil when asked about them). I get no error log. In RenewableResourceNodeComponent I do it like this:
function PawelRenewableResourceNodeComponent:_create_task_cancelled_listener()
   local task_tracker_component = self._entity:get_component('stonehearth:task_tracker')
   if task_tracker_component == nil then
      return
   elseif task_tracker_component:is_activity_requested(HARVEST_ACTION) then
      self._task_cancelled_listener = radiant.events.listen(self._entity, 'pawel_API:task_tracker:task_cancelled', self, self._on_task_cancelled)
   end
end

function PawelRenewableResourceNodeComponent:_on_task_cancelled(args)
   if args.activity_name == HARVEST_ACTION and args.cancel_reason ~= 'completed' then
      self:set_autoharvested(false)
   end
end
  1. Saved variables I added to initialize function of RRN component are gone after loading saved game.

That code looks reasonable at a glance. Perhaps it isn’t being called? You can add some logging statements there to make sure it is. For the saved variables, it might have to do with how the mixing is happening. Are you overriding the component via JSON or injecting methods at runtime?

Thanks to @max99x’s advice and explanations the autoharvest is working now.

However, my further ideas would involve a lot of listening to events. As all of them require components I could do it in a more memory-wise way by implementing a function which fires a specific command on all components of an entity instead of listening to an event inside each component:

function pawel_API:component_trigger(entity, function, args)
   if entity and entity:is_valid() and type(function) == 'string' then
      local component_list = --[[how do I get a list of all components an entity has?]]
      for i, component in ipairs(component_list) do
         if type(component[function]) == 'function' then
            component[function](self, args)
         end
      end
   end
end

For example, for detecting an entity going iconic I could do:

args = {}
pawel_API:component_trigger(self._entity, _on_removed_from_world_trigger, args)

instead of creating a listener on each component interested in the event:

self._on_removed_from_world_listener = radiant.events.listen(self._entity, 'stonehearth:on_removed_from_world', self, self._on_removed_from_world_trigger)

But, as I asked: how do I get a list of all components an entity has?

We don’t expose a function to get a component list, mainly because components are supposed to have unrelated APIs. You can hack around it using a trace, if you want:

local trace = entity:trace_components('a reason string that will appear in debug logs')
                   :on_added(function(name, component)
                         do_something_with_component(component)
                      end)
                   :push_object_state()
trace:destroy()