advanced_npc
advanced_npc copied to clipboard
Execution Diagram
I am trying to draw a diagram that exemplifies the whole operation of the NPC. sketchboard Can you explain how to insert a program into the instruction flow of another program? Show an example.
This is really cool! I never even envisioned a diagram for the docs!
Regarding your question, to execute a program from another program, the proper way to do it is to execute the advanced_npc:interrupt
instruction. It accepts arguments:
-
new_program
: name of the new program to execute -
new_args
: arguments of the new program to execute -
interrupt_options
: interrupt options for new program, optional
Example:
npc.programs.register("advanced_npc:farmer:dig_and_replant", function(self, args)
--minetest.log("Got as argument: "..dump(args.pos))
local pos = npc.programs.helper.get_pos_argument(self, args.pos, false)
--minetest.log("Got from helper: "..dump(pos))
if pos then
-- Get node
local node = minetest.get_node_or_nil(pos)
if node then
-- Calculate node name to plant
local plant_name = string.split(node.name, "_")
minetest.log("PLant name: "..dump(plant_name))
--minetest.log("Plant name: "..dump(plant_name))
local new_plant_name = node.name
if plant_name[1] and plant_name[2] then
if plant_name[2] == "8" then
new_plant_name = plant_name[1].."_1"
else
new_plant_name = plant_name[1].."_"..(plant_name[2] + 1)
end
end
npc.log("INFO", "New plant_name: "..dump(new_plant_name))
-- Decide whether to walk to the position or just rotate
-- towards the plant if it's close enough
local npc_pos = vector.round(self.object:getpos())
if vector.distance(npc_pos, pos) > 2 then
-- Walk to position
npc.programs.instr.execute(self, "advanced_npc:interrupt", {
new_program = "advanced_npc:walk_to_pos",
new_args = {
end_pos = {
place_type=npc.locations.data.calculated.target,
use_access_node=true
},
walkable = {}
},
interrupt_options = {}
})
else
-- Rotate towards the plant
npc.programs.instr.execute(self, "advanced_npc:rotate", {
yaw = minetest.dir_to_yaw(vector.direction(npc_pos, pos))
})
end
-- Dig
npc.exec.proc.enqueue(self, "advanced_npc:dig", {
pos = pos,
add_to_inventory = true,
bypass_protection = true
})
-- Rest of program...
When this instruction is executed, the current program will be interrupted and new program will start immediately. When this program is done, the scheduler will restore the old program and continue its execution.
How can you ensure that the old program will return exactly from where it was interrupted? Can you pause a moon function?
I tried to do this but it is not working
npc.programs.register("sunos:interact_furniture", function(self, args)
local places = npc.locations.get_by_type(self, "furniture")
p = places[math.random(1, #places)]
npc.locations.add_shared(self, "sunos_furniture_target", "sunos_target", p.pos, p.access_node)
npc.programs.instr.execute(self, "advanced_npc:interrupt", {
new_program = "advanced_npc:walk_to_pos",
new_args = {
end_pos = {
place_type="sunos_furniture_target",
use_access_node=true
},
walkable = sunos.estruturas.casa.walkable_nodes
},
interrupt_options = {}
})
npc.programs.instr.execute(self, "advanced_npc:interrupt", {
new_program = "sunos:interact",
new_args = {
end_pos = {
place_type="sunos_furniture_target",
use_access_node=true
},
walkable = sunos.estruturas.casa.walkable_nodes
},
interrupt_options = {}
})
end)
@BrunoMine Instruction state is also stored in the process table. And when an interrupt occurs, the instruction state is stored as well. See here and here
Notice the parameter set_instruction_as_interrupted
which is false when the interrupt is called.
If youe execute the interrupt instruction advanced_npc:interrupt
, you need to enqueue every other instruction after it. So do:
npc.exec.proc.enqueue(self, "advanced_npc:interrupt"...
instead of
npc.programs.instr.execute(self, "advanced_npc:interrupt" ....
This program is apparently working I develop more in Portuguese (my native language)
-- Interagir aleatoriamente com a mobilia da casa
npc.programs.register("sunos:interagir_mobilia", function(self, args)
-- Verificar distancia de casa
if verif_dist_pos(self.object:getpos(), self.sunos_fundamento) > 16 then
return
end
local places = npc.locations.get_by_type(self, "mobilia")
p = places[math.random(1, #places)]
npc.locations.add_shared(self, "sunos_alvo_mobilia", "sunos_alvo_mobilia", p.pos, p.access_node)
npc.exec.proc.enqueue(self, "advanced_npc:interrupt", {
new_program = "advanced_npc:walk_to_pos",
new_args = {
end_pos = {
place_type="sunos_alvo_mobilia",
use_access_node=true
},
walkable = sunos.estruturas.casa.walkable_nodes
},
interrupt_options = {}
})
-- Vira para "pos"
npc.exec.proc.enqueue(self, "advanced_npc:rotate", {
start_pos = self.object:getpos(),
end_pos = p.pos,
})
-- Fica parado por um tempo
npc.exec.proc.enqueue(self, "advanced_npc:wait", {
time = 5,
})
end)
Code used for send the npc to bed.
npc.exec.enqueue_program(self, "advanced_npc:walk_to_pos", {
end_pos = {
place_type="bed_primary",
use_access_node=true
}
})
npc.exec.enqueue_program(self, "advanced_npc:use_bed", {
pos = "bed_primary",
action = npc.programs.const.node_ops.beds.LAY
})
npc.exec.enqueue_program(self, "advanced_npc:idle",
{
acknowledge_nearby_objs = false,
wander_chance = 0
},
{},
true
)
NPC occupation
npc.occupations.register_occupation("sunos_npc_caseiro", {
dialogues = {},
textures = {},
building_types = {},
surrounding_building_types = {},
walkable_nodes = sunos.estruturas.casa.walkable_nodes,
initial_inventory = {},
schedules_entries = sunos.copy_tb({
-- Durmir/Sleep
[0] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[1] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[2] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[3] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[4] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[5] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[6] = sunos.estruturas.casa.acordar, -- breakfast
-- Mecher em casa/Work (sunos:interagir_mobilia)
[7] = interagir_casa, -- work at house
[8] = interagir_casa,-- work at house
[9] = interagir_casa,-- work at house
[10] = interagir_casa,-- work at house
[11] = interagir_casa,-- work at house
[12] = interagir_casa,-- work at house
[13] = interagir_casa,-- work at house
[14] = interagir_casa,-- work at house
[15] = interagir_casa,-- work at house
[16] = interagir_casa,-- work at house
[17] = interagir_casa,-- work at house
[18] = interagir_casa,-- work at house
[19] = interagir_casa,-- work at house
[20] = interagir_casa,-- work at house
[21] = interagir_casa,-- work at house
-- Durmir
[22] = sunos.estruturas.casa.durmir, -- send to bed if not yet
[23] = sunos.estruturas.casa.durmir -- send to bed if not yet
})
})
The problem is that they keep working even after going to sleep. I realized that this problem does not occur if the NPC spawns during the night, it will sleep and continue sleep.
He goes to bed, but then returns to work, then goes to bed (due to an external algorithm), then back to work. This is repeated all night until breakfast.
Sorry for the delay in answering. By the way, I kind of understand your programs, as my native language is Spanish, so I do understand some words.
First of all, I assume you want: NPC to sleep from 22 - 5:59 NPC to wake up and eat 6-6:59 NPC to walk around house and simulate interaction with furniture/work nodes 7 - 21:59
Based on that assumption, I suggest you model your programs like this:
- At 22, send the NPC to bed and then set state program as
advanced_npc:idle
withacknowledge_nearby_objs = false
andwander_chance = 0
.- This ensures your NPC doesn't moves from the bed. Also, once you do this, since
advanced_npc:idle
is a state program, you don't need to set it for every next hour (23, 0, 1, 2, ...). The NPC will execute this all the time until a new schedule entry comes.
- This ensures your NPC doesn't moves from the bed. Also, once you do this, since
- For 5, add a schedule entry to wake up the NPC. Here you can set
advanced_npc:idle
as state process again, but the arguments for acknowledging objects and wandering are not needed... NPC can move now - For 6, add a schedule entry to go and get food. I believe this works fine
- For 7, you should set the state program to be
sunos:interagir_mobilia
. This way, it will keep executing and you don't have to add any other schedule.
Regarding interagir_casa
program:
npc.locations.add_shared(self, "sunos_alvo_mobilia", "sunos_alvo_mobilia", p.pos, p.access_node)
npc.exec.proc.enqueue(self, "advanced_npc:interrupt", { ...
You should always execute the first instruction of a program instead of enqueuing it. Why? It makes the process faster and more responsive... apart from that, no other reason. You can enqueue, but I recommend to execute first, enqueue rest.
Another point:
-- Vira para "pos"
npc.exec.proc.enqueue(self, "advanced_npc:rotate", {
start_pos = self.object:getpos(),
end_pos = p.pos,
})
This instruction, unfortunately, will not work as you think. The reason is (and this is my fault, actually you made me aware in https://github.com/hkzorman/advanced_npc/issues/47#issuecomment-385075391) that when you give it start_pos
, Lua will evaluate self.object:getpos()
at the very moment of enqueuing this instruction. The position of the NPC will have changed by the moment the actual instruction gets executed, and then the NPC may not rotate correctly (actually it may, but it is pure coincidence). To do this, I have been thinking of adding the ability to pass instruction and process arguments as a table, something like:
{future=self.object:getpos}
or
{future="var_name"}
so that future values are properly evaluated.
A final point: I'm actually very happy that you are using the programs functionality for your mod. I've noticed that I need to expose myself to use advanced_npc more as an API, so I have started work on a mg_villages NPC mod, basically NPCs specifically tailored for mg_villages
mod. During my early tests I've noticed bad performance when lots of NPCs were around, how is the performance for you? Have you tried many NPCs at a time?
I've been able to improve performance by basically not acknowledging objects in both idle
and wander
programs, but still I know there are deficiencies in these programs... I will work to improve them.
Repeat scheduling is required in case the NPC is spawned overnight or day, this ensures that at any time of spawn it is spawned, it receives a task.
As you can see, I'm using the program state, but the work at home program continues even after another program state.
This is essentially because the following:
npc.exec.enqueue_program(self, "advanced_npc:idle",
{
acknowledge_nearby_objs = false,
wander_chance = 0
},
{},
true
)
Doesn't sets the state program, it just enqueues idle
which will run and not run anymore. You will have to use npc.exec.set_state_program(...)
But, the #5 argument of npc.exec.enqueue_program
mean this is a state program.
True, but the function itself doesn't sets it as the state program, it creates an entry in the process queue that is that of a state program.
To explain better what I'm saying: the state program is stored in the variable self.execution.state_process
. The functions in the schedule API will actually set state program to your sunos:interagir_mobilia
. This will reflect in self.execution.state_process
. When you enqueue idle
using npc.exec.enqueue_program
, the self.execution.state_process
variable is still pointing to sunos:interagir_mobilia
, not advanced_npc:idle
. When all processes finish execution, the scheduler will search for the state program in self.execution.state_process
, which is sunos:interagir_mobilia
.
But this really looks like a redundancy. If I already informed you that this is a program state, then the API should do so.
When the schedule processes the advanced_npc:idle
program in the queue, it should change the self.execution.state_process
as this is the new program state.
In addition, the program state does not stop after a new program gets in the queue, as I thought it was. Is that correct? how is it possible to interrupt a program state?
Ok, so basically we could add an argument to npc.exec.enqueue_program
that tells the function that the program given will be set as the state program.
The state program will indeed stop once a process is in the queue. This is the way the scheduler algorithm works. That's why NPC goes to bed in the first place. The algorithm is:
- If current process is state process and queue is larger than 2:
- Interrupt state process, execute next proces
- When next process finishes, it check if the
interrupted_process
variable contains a value. If it does, it will re-enqueue this process, except when:- The interrupted process is a state process, and the interrupted state process is not the same as the current state process (the one in self.execution.state_process`
But the npc.exec.enqueue_program
already has a argument (5-bool) for this. that how you said: it creates an entry in the process queue that is that of a state program, but for some reason I still do not understand, doesn't sets it as the state program
I understood the sequence, but I did not interrupt the state process, I just added new programs (npc.exec.enqueue_program
) in the queue, so I think this should be done after a state program loop and shut it down (because new programs are in the queue).
Basically it is a conceptual issue.
The way I conceptualized state programs are that they are not manually enqueued, they are managed only by the process scheduler. As an outsider of the API, the only control you have is to tell (by using npc.exec.set_state_program
) the scheduler, this is the state process you need to run whenever it should run.
Seems like the boolean argument in npc.exec.enqueue_program
is misleading...
Would you agree? Does this makes sense?
About the program state, okay, they are managed only by the process scheduler, but I think this can be manually enqueued too, because your operation is very simple (re-runs while there is nothing in the queue). The schedule queue should work separately from the fixed scheduler. The scheduler with fixed times should only insert programs in the schedule queue in each time.
Yes, the boolean argument in npc.exec.enqueue_program
is misleading, because he no cause practical effects in the queued program. This is the main problem.
Would it be okay if I remove that argument?
Or would you prefer that the argument sets the state process instead?
I guess either way is sensible
The argument makes it much simpler.
Could you improve it?
Latest push in master (a7d5900) is an attempt at doing this. Hopefully it should work without issues.
Erro
2018-05-10 13:40:27: ERROR[Main]: ServerError: AsyncErr: ServerThread::run Lua: Runtime error from mod 'sunos' in callback luaentity_Step(): ...Mods/minetest 0-5-0/bin/../mods/advanced_npc/npc.lua:1246: attempt to compare nil with number
2018-05-10 13:40:27: ERROR[Main]: stack traceback:
2018-05-10 13:40:27: ERROR[Main]: ...Mods/minetest 0-5-0/bin/../mods/advanced_npc/npc.lua:1246: in function 'process_scheduler'
2018-05-10 13:40:27: ERROR[Main]: ...Mods/minetest 0-5-0/bin/../mods/advanced_npc/npc.lua:1376: in function 'execution_routine'
2018-05-10 13:40:27: ERROR[Main]: ...Mods/minetest 0-5-0/bin/../mods/advanced_npc/npc.lua:2128: in function <...Mods/minetest 0-5-0/bin/../mods/advanced_npc/npc.lua:2060>
2018-05-10 13:40:27: ERROR[Main]: (tail call): ?
2018-05-10 13:40:27: ERROR[Main]: ...de Mods/minetest 0-5-0/bin/../mods/mobs_redo/api.lua:2579: in function <...de Mods/minetest 0-5-0/bin/../mods/mobs_redo/api.lua:2522>
2018-05-10 13:40:27: ACTION[Server]: 888 leaves game. List of players:
A /clearobjects
command fix all.
The problem is fixed
Let me know if it works.
I've tested everything, the state program is working as expected.
Here is how I soved the problem with rotation
npc.programs.instr.register("sunos:rotate_to_pos", function(self, args)
npc.programs.instr.execute(self, "advanced_npc:rotate", {
start_pos = self.object:getpos(),
end_pos = args.pos,
})
end)
In the future we can discuss another solution, but the API is very flexible to get around any situation.
About program state
Definition 1 - This is a configured program which run if the programs queue is empty. It means this programs stays how a program state even if another common program is added in the queue. (reexecuted when the queue is empty again)
Definition 2 - This is a program reexecuted ever that it is the last program and contains state_program=true
parameter, but is deleted when a new program is added in the queue.
What of this two definitions is true? (approximately)
Definition 1 seems more accurate to me.