Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execution Diagram #49

Open
BrunoMine opened this issue Apr 30, 2018 · 29 comments
Open

Execution Diagram #49

BrunoMine opened this issue Apr 30, 2018 · 29 comments

Comments

@BrunoMine
Copy link
Contributor

BrunoMine commented Apr 30, 2018

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.

@hkzorman
Copy link
Owner

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.

@BrunoMine
Copy link
Contributor Author

How can you ensure that the old program will return exactly from where it was interrupted? Can you pause a moon function?

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 5, 2018

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)

@hkzorman
Copy link
Owner

hkzorman commented May 5, 2018

@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" ....

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 6, 2018

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.

@BrunoMine
Copy link
Contributor Author

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.

@hkzorman
Copy link
Owner

hkzorman commented May 8, 2018

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 with acknowledge_nearby_objs = false and wander_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.
  • 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 #47 (comment)) 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.

@BrunoMine
Copy link
Contributor Author

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.

@hkzorman
Copy link
Owner

hkzorman commented May 8, 2018

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(...)

@BrunoMine
Copy link
Contributor Author

But, the #5 argument of npc.exec.enqueue_program mean this is a state program.

@hkzorman
Copy link
Owner

hkzorman commented May 9, 2018

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.

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 9, 2018

But this really looks like a redundancy. If I already informed you that this is a program state, then the API should do so.

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 9, 2018

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?

@hkzorman
Copy link
Owner

hkzorman commented May 9, 2018

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`

@BrunoMine
Copy link
Contributor Author

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

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 9, 2018

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).

@hkzorman
Copy link
Owner

hkzorman commented May 9, 2018

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?

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 9, 2018

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.

@hkzorman
Copy link
Owner

hkzorman commented May 9, 2018

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

@BrunoMine
Copy link
Contributor Author

The argument makes it much simpler.

@BrunoMine
Copy link
Contributor Author

Could you improve it?

@hkzorman
Copy link
Owner

Latest push in master (a7d5900) is an attempt at doing this. Hopefully it should work without issues.

@BrunoMine
Copy link
Contributor Author

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: 

@BrunoMine
Copy link
Contributor Author

A /clearobjects command fix all.
The problem is fixed

@hkzorman
Copy link
Owner

Let me know if it works.

@BrunoMine
Copy link
Contributor Author

I've tested everything, the state program is working as expected.

@BrunoMine
Copy link
Contributor Author

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.

@BrunoMine
Copy link
Contributor Author

BrunoMine commented May 11, 2018

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)

@hkzorman
Copy link
Owner

Definition 1 seems more accurate to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants