diff --git a/actions/places.lua b/actions/places.lua index 19af7e1..f93cc2d 100644 --- a/actions/places.lua +++ b/actions/places.lua @@ -56,7 +56,8 @@ npc.places.nodes = { }, WORKPLACE_TYPE = { -- TODO: Do we have an advanced_npc workplace? - "mg_villages:mob_workplace_marker" + "mg_villages:mob_workplace_marker", + "advanced_npc:workplace_marker" } } @@ -102,13 +103,13 @@ function npc.places.add_owned(self, place_name, place_type, pos, access_node) self.places_map[place_name] = {type=place_type, pos=pos, access_node=access_node or pos, status="owned"} end -function npc.places.add_owned_accessible_place(self, nodes, place_type) +function npc.places.add_owned_accessible_place(self, nodes, place_type, walkables) for i = 1, #nodes do -- Check if node has owner if nodes[i].owner == "" then -- If node has no owner, check if it is accessible - local empty_nodes = npc.places.find_node_orthogonally( - nodes[i].node_pos, {"air"}, 0) + local empty_nodes = npc.places.find_orthogonal_accessible_node( + nodes[i].node_pos, nil, 0, true, walkables) -- Check if node is accessible if #empty_nodes > 0 then -- Set owner to this NPC @@ -127,27 +128,30 @@ end -- Override flag allows to overwrite a place in the places_map. -- The only valid use right now is for schedules - don't use this -- anywhere else unless you have a position that changes over time. -function npc.places.add_shared_accessible_place(self, nodes, place_type, override) - if not override then +function npc.places.add_shared_accessible_place(self, nodes, place_type, override, walkables) + + if not override or (override and override == false) then for i = 1, #nodes do -- Check if not adding same owned place if nodes[i].owner ~= self.npc_id then -- Check if it is accessible - local empty_nodes = npc.places.find_node_orthogonally( - nodes[i].node_pos, {"air"}, 0) + local empty_nodes = npc.places.find_orthogonal_accessible_node( + nodes[i].node_pos, nil, 0, true, walkables) -- Check if node is accessible if #empty_nodes > 0 then -- Assign node to NPC npc.places.add_shared(self, place_type..dump(i), place_type, nodes[i].node_pos, empty_nodes[1].pos) + else + npc.log("WARNING", "Found non-accessible place at pos: "..minetest.pos_to_string(nodes[i].node_pos)) end end end - elseif override then + elseif override == true then -- Note: Nodes is only *one* node in case override = true -- Check if it is accessible - local empty_nodes = npc.places.find_node_orthogonally( - nodes.node_pos, {"air"}, 0) + local empty_nodes = npc.places.find_orthogonal_accessible_node( + nodes.node_pos, nil, 0, true, walkables) -- Check if node is accessible if #empty_nodes > 0 then -- Nodes is only one node @@ -157,10 +161,31 @@ function npc.places.add_shared_accessible_place(self, nodes, place_type, overrid end end -function npc.places.get_by_type(self, place_type) +function npc.places.get_by_type(self, place_type, exact_match) local result = {} for _, place_entry in pairs(self.places_map) do - if place_entry.type == place_type then + -- Check if place_type matches any stored place + -- local condition = false + -- if exact_match then + -- -- If no exact match, search if place_type is contained + -- if exact_match == false then + -- local s, _ = string.find(place_entry.type, place_type) + -- if s ~= nil then + -- condition = true + -- end + -- else + -- -- Exact match + -- if place_entry.type == place_type then + -- condition = true + -- end + -- end + -- end + -- if condition == true + local s, _ = string.find(place_entry.type, place_type) + --minetest.log("place_entry: "..dump(place_entry)) + --minetest.log("place_type: "..dump(place_type)) + --minetest.log("S: "..dump(s)) + if s ~= nil then table.insert(result, place_entry) end end @@ -187,6 +212,12 @@ end -- TODO: This function can be improved to support a radius greater than 1. function npc.places.find_node_orthogonally(pos, nodes, y_adjustment) + -- Call the more generic function with appropriate params + npc.places.find_orthogonal_accessible_node(pos, nodes, y_adjustment, nil, nil) +end + +-- TODO: This function can be improved to support a radius greater than 1. +function npc.places.find_orthogonal_accessible_node(pos, nodes, y_adjustment, include_walkables, extra_walkables) -- Calculate orthogonal points local points = {} table.insert(points, {x=pos.x+1,y=pos.y+y_adjustment,z=pos.z}) @@ -197,8 +228,20 @@ function npc.places.find_node_orthogonally(pos, nodes, y_adjustment) for _,point in pairs(points) do local node = minetest.get_node(point) --minetest.log("Found node: "..dump(node)..", at pos: "..dump(point)) - for _,node_name in pairs(nodes) do - if node.name == node_name then + -- Search for specific node names + if nodes then + for _,node_name in pairs(nodes) do + if node.name == node_name then + table.insert(result, {name=node.name, pos=point, param2=node.param2}) + end + end + else + -- Search for air, walkable nodes, or any node availble in the extra_walkables array + if node.name == "air" + or (include_walkables == true + and minetest.registered_nodes[node.name].walkable + and minetest.registered_nodes[node.name].groups.fence ~= 1) + or (extra_walkables and npc.utils.array_contains(extra_walkables, node.name)) then table.insert(result, {name=node.name, pos=point, param2=node.param2}) end end @@ -206,6 +249,7 @@ function npc.places.find_node_orthogonally(pos, nodes, y_adjustment) return result end + -- Wrapper around minetest.find_nodes_in_area() -- TODO: Verify if this wrapper is actually needed function npc.places.find_node_in_area(start_pos, end_pos, type) @@ -268,57 +312,57 @@ if minetest.get_modpath("mg_villages") ~= nil then result.building_type = result.building_data.typ end return result - end + end - -- Pre-requisite: only run this function on mg_villages:plotmarker that has been adapted - -- by using spawner.adapt_mg_villages_plotmarker - function npc.places.get_all_workplaces_from_plotmarker(pos) - local result = {} - local meta = minetest.get_meta(pos) - local pos_data = minetest.deserialize(meta:get_string("building_pos_data")) - if pos_data then - local workplaces = pos_data.workplaces - if workplaces then - -- Insert all workplaces in this plotmarker - for i = 1, #workplaces do - table.insert(result, - { - workplace=workplaces[i], - building_type=meta:get_string("building_type"), + -- Pre-requisite: only run this function on mg_villages:plotmarker that has been adapted + -- by using spawner.adapt_mg_villages_plotmarker + function npc.places.get_all_workplaces_from_plotmarker(pos) + local result = {} + local meta = minetest.get_meta(pos) + local pos_data = minetest.deserialize(meta:get_string("building_pos_data")) + if pos_data then + local workplaces = pos_data.workplaces + if workplaces then + -- Insert all workplaces in this plotmarker + for i = 1, #workplaces do + table.insert(result, + { + workplace=workplaces[i], + building_type=meta:get_string("building_type"), surrounding_workplace = false, - node_pos= { - x=workplaces[i].x, - y=workplaces[i].y, - z=workplaces[i].z - } - }) - end - end - end - -- Check the other plotmarkers as well - local nearby_plotmarkers = minetest.deserialize(meta:get_string("nearby_plotmarkers")) - if nearby_plotmarkers then - for i = 1, #nearby_plotmarkers do - if nearby_plotmarkers[i].workplaces then - -- Insert all workplaces in this plotmarker - for j = 1, #nearby_plotmarkers[i].workplaces do - --minetest.log("Nearby plotmarker workplace #"..dump(j)..": "..dump(nearby_plotmarkers[i].workplaces[j])) - table.insert(result, { - workplace=nearby_plotmarkers[i].workplaces[j], + node_pos= { + x=workplaces[i].x, + y=workplaces[i].y, + z=workplaces[i].z + } + }) + end + end + end + -- Check the other plotmarkers as well + local nearby_plotmarkers = minetest.deserialize(meta:get_string("nearby_plotmarkers")) + if nearby_plotmarkers then + for i = 1, #nearby_plotmarkers do + if nearby_plotmarkers[i].workplaces then + -- Insert all workplaces in this plotmarker + for j = 1, #nearby_plotmarkers[i].workplaces do + --minetest.log("Nearby plotmarker workplace #"..dump(j)..": "..dump(nearby_plotmarkers[i].workplaces[j])) + table.insert(result, { + workplace=nearby_plotmarkers[i].workplaces[j], building_type = nearby_plotmarkers[i].building_type, surrounding_workplace = true, node_pos = { - x=nearby_plotmarkers[i].workplaces[j].x, - y=nearby_plotmarkers[i].workplaces[j].y, - z=nearby_plotmarkers[i].workplaces[j].z - } + x=nearby_plotmarkers[i].workplaces[j].x, + y=nearby_plotmarkers[i].workplaces[j].y, + z=nearby_plotmarkers[i].workplaces[j].z + } }) - end - end - end - end - return result - end + end + end + end + end + return result + end end -- This function will search for nodes of type plotmarker and, @@ -353,11 +397,11 @@ function npc.places.find_plotmarkers(pos, radius, exclude_current_pos) def["building_type"] = data.building_type if data.building_pos_data then def["building_pos_data"] = data.building_pos_data - def["workplaces"] = data.building_pos_data.workplaces + def["workplaces"] = data.building_pos_data.workplaces end - end - -- Add building - --minetest.log("Adding building: "..dump(def)) + end + -- Add building + --minetest.log("Adding building: "..dump(def)) table.insert(result, def) end end @@ -374,7 +418,7 @@ function npc.places.scan_area_for_usable_nodes(pos1, pos2) furnace_type = {}, storage_type = {}, openable_type = {}, - workplace_type = {} + workplace_type = {} } local start_pos, end_pos = vector.sort(pos1, pos2) @@ -384,17 +428,24 @@ function npc.places.scan_area_for_usable_nodes(pos1, pos2) result.storage_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.STORAGE_TYPE) result.openable_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.OPENABLE_TYPE) - -- Find workplace nodes: if mg_villages:plotmarker is given a start pos, take it from there. - -- If not, search for them. - local node = minetest.get_node(pos1) - if node.name == "mg_villages:plotmarker" then - if npc.places.get_all_workplaces_from_plotmarker then - result.workplace_type = npc.places.get_all_workplaces_from_plotmarker(pos1) - end - else - -- Just search for workplace nodes - result.workplace_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.WORKPLACE_TYPE) - end + -- Find workplace nodes: if mg_villages:plotmarker is given a start pos, take it from there. + -- If not, search for them. + local node = minetest.get_node(pos1) + if node.name == "mg_villages:plotmarker" then + if npc.places.get_all_workplaces_from_plotmarker then + result.workplace_type = npc.places.get_all_workplaces_from_plotmarker(pos1) + end + else + -- Just search for workplace nodes + result.workplace_type = npc.places.get_nodes_by_type(start_pos, end_pos, npc.places.nodes.WORKPLACE_TYPE) + -- Find out building type and add it to the result + for i = 1, #result.workplace_type do + local meta = minetest.get_meta(result.workplace_type[i].node_pos) + local building_type = meta:get_string("building_type") or "none" + minetest.log("Building type: "..dump(building_type)) + result.workplace_type[i]["building_type"] = building_type + end + end return result end diff --git a/npc.lua b/npc.lua index c382dc3..ce13e17 100755 --- a/npc.lua +++ b/npc.lua @@ -48,7 +48,9 @@ npc.log_level = { INFO = true, WARNING = true, ERROR = true, - DEBUG = false + DEBUG = false, + DEBUG_ACTION = false, + DEBUG_SCHEDULE = false } npc.texture_check = { @@ -474,9 +476,9 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- If occupation name given, override properties with -- occupation values and initialize schedules if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then - -- Set occupation name - ent.occupation_name = occupation_name - -- Override relevant values + -- Set occupation name + ent.occupation_name = occupation_name + -- Override relevant values npc.occupations.initialize_occupation_values(ent, occupation_name) end @@ -707,9 +709,10 @@ end -- This function removes the first action in the action queue -- and then executes it function npc.execute_action(self) + npc.log("DEBUG_ACTION", "Current actions queue: "..dump(self.actions.queue)) -- Check if an action was interrupted if self.actions.current_action_state == npc.action_state.interrupted then - npc.log("DEBUG", "Re-inserting interrupted action for NPC: '"..dump(self.npc_name).."': "..dump(self.actions.state_before_lock.interrupted_action)) + npc.log("DEBUG_ACTION", "Re-inserting interrupted action for NPC: '"..dump(self.npc_name).."': "..dump(self.actions.state_before_lock.interrupted_action)) -- Insert into queue the interrupted action table.insert(self.actions.queue, 1, self.actions.state_before_lock.interrupted_action) -- Clear the action @@ -731,17 +734,17 @@ function npc.execute_action(self) end -- Check if action is an schedule check if action_obj.action == "schedule_check" then - -- Execute schedule check - npc.schedule_check(self) -- Remove table entry table.remove(self.actions.queue, 1) + -- Execute schedule check + npc.schedule_check(self) -- Return return false end -- If the entry is a task, then push all this new operations in -- stack fashion if action_obj.is_task == true then - npc.log("DEBUG", "Executing task for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) + npc.log("DEBUG_ACTION", "Executing task for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) -- Backup current queue local backup_queue = self.actions.queue -- Remove this "task" action from queue @@ -757,7 +760,7 @@ function npc.execute_action(self) table.insert(self.actions.queue, backup_queue[i]) end else - npc.log("DEBUG", "Executing action for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) + npc.log("DEBUG_ACTION", "Executing action for NPC '"..dump(self.npc_name).."': "..dump(action_obj)) -- Store the action that is being executed self.actions.state_before_lock.interrupted_action = action_obj -- Store current position @@ -811,7 +814,7 @@ function npc.lock_actions(self) -- Freeze mobs_redo API self.freeze = false - npc.log("DEBUG", "Locking NPC "..dump(self.npc_id).." actions") + npc.log("DEBUG_ACTION", "Locking NPC "..dump(self.npc_id).." actions") end function npc.unlock_actions(self) @@ -825,7 +828,7 @@ function npc.unlock_actions(self) self.freeze = true end - npc.log("DEBUG", "Unlocked NPC "..dump(self.npc_id).." actions") + npc.log("DEBUG_ACTION", "Unlocked NPC "..dump(self.npc_id).." actions") end --------------------------------------------------------------------------------------- @@ -1024,6 +1027,19 @@ function npc.add_schedule_check(self) table.insert(self.actions.queue, {action="schedule_check", args={}, is_task=false}) end +function npc.enqueue_schedule_action(self, entry) + if entry.task ~= nil then + -- Add task + npc.add_task(self, entry.task, entry.args) + elseif entry.action ~= nil then + -- Add action + npc.add_action(self, entry.action, entry.args) + elseif entry.property ~= nil then + -- Change NPC property + npc.schedule_change_property(self, entry.property, entry.args) + end +end + -- Range: integer, radius in which nodes will be searched. Recommended radius is -- between 1-3 -- Nodes: array of node names @@ -1035,7 +1051,9 @@ end -- None-action: array of entries {action=, args={}}. -- Will be executed when no node is found. function npc.schedule_check(self) + npc.log("DEBUG_SCHEDULE", "Prev Actions queue: "..dump(self.actions.queue)) local range = self.schedules.current_check_params.range + local walkable_nodes = self.schedules.current_check_params.walkable_nodes local nodes = self.schedules.current_check_params.nodes local actions = self.schedules.current_check_params.actions local none_actions = self.schedules.current_check_params.none_actions @@ -1044,15 +1062,17 @@ function npc.schedule_check(self) -- Search nodes local found_nodes = npc.places.find_node_nearby(start_pos, nodes, range) -- Check if any node was found - if found_nodes then + npc.log("DEBUG_SCHEDULE", "Found nodes using radius: "..dump(found_nodes)) + if found_nodes and #found_nodes > 0 then -- Pick a random node to act upon local node_pos = found_nodes[math.random(1, #found_nodes)] local node = minetest.get_node(node_pos) -- Set node as a place -- Note: Code below isn't *adding* a node, but overwriting the -- place with "schedule_target_pos" place type + npc.log("DEBUG_SCHEDULE", "Found "..dump(node.name).." at pos: "..minetest.pos_to_string(node_pos)) npc.places.add_shared_accessible_place( - self, node, npc.places.PLACE_TYPE.SCHEDULE.TARGET, true) + self, {owner="", node_pos=node_pos}, npc.places.PLACE_TYPE.SCHEDULE.TARGET, true, walkable_nodes) -- Get actions related to node and enqueue them for i = 1, #actions[node.name] do local args = {} @@ -1069,9 +1089,10 @@ function npc.schedule_check(self) -- otherwise args = { pos = node_pos, - add_to_inventory = action[node.name][i].args.add_to_inventory or true, - bypass_protection = action[node.name][i].args.bypass_protection or false + add_to_inventory = actions[node.name][i].args.add_to_inventory or true, + bypass_protection = actions[node.name][i].args.bypass_protection or false } + npc.add_action(self, actions[node.name][i].action, args) elseif actions[node.name][i].action == npc.actions.cmd.PLACE then -- Position: providing node_pos is because the currently planned -- behavior for placing nodes is replacing digged nodes. A NPC farmer, @@ -1081,16 +1102,29 @@ function npc.schedule_check(self) -- if not will be force-placed (item comes from thin air) -- Protection will be respected args = { - pos = action[node.name][i].args.pos or node_pos, - source = action[node.name][i].args.source or npc.actions.take_from_inventory_forced, - node = action[node.name][i].args.node, - bypass_protection = action[node.name][i].args.bypass_protection or false + pos = actions[node.name][i].args.pos or node_pos, + source = actions[node.name][i].args.source or npc.actions.take_from_inventory_forced, + node = actions[node.name][i].args.node, + bypass_protection = actions[node.name][i].args.bypass_protection or false } + --minetest.log("Enqueue dig action with args: "..dump(args)) + npc.add_action(self, actions[node.name][i].action, args) + elseif actions[node.name][i].action == npc.actions.cmd.ROTATE then + -- Set arguments + args = { + dir = actions[node.name][i].dir, + start_pos = actions[node.name][i].start_pos + or {x=start_pos.x, y=node_pos.y, z=start_pos.z}, + end_pos = actions[node.name][i].end_pos or node_pos + } + -- Enqueue action + npc.add_action(self, actions[node.name][i].action, args) elseif actions[node.name][i].action == npc.actions.cmd.WALK_STEP then -- Defaults: direction is calculated from start node to node_pos. -- Speed is default wandering speed. Target pos is node_pos -- Calculate dir if dir is random local dir = npc.actions.get_direction(start_pos, node_pos) + minetest.log("actions: "..dump(actions[node.name][i])) if actions[node.name][i].args.dir == "random" then dir = math.random(0,7) elseif type(actions[node.name][i].args.dir) == "number" then @@ -1101,57 +1135,76 @@ function npc.schedule_check(self) speed = actions[node.name][i].args.speed or npc.actions.one_nps_speed, target_pos = actions[node.name][i].args.target_pos or node_pos } - elseif actions[node.name][i].action == npc.actions.cmd.WALK_TO_POS then + npc.add_action(self, actions[node.name][i].action, args) + elseif actions[node.name][i].task == npc.actions.cmd.WALK_TO_POS then -- Optimize walking -- since distances can be really short, -- a simple walk_step() action can do most of the times. For -- this, however, we need to calculate direction -- First of all, check distance - if vector.distance(start_pos, node_pos) < 3 then + local distance = vector.distance(start_pos, node_pos) + if distance < 3 then -- Will do walk_step based instead - action = npc.actions.cmd.WALK_STEP - args = { - dir = npc.actions.get_direction(start_pos, node_pos), - speed = npc.actions.one_nps_speed, - target_pos = node_pos - } + if distance > 1 then + args = { + dir = npc.actions.get_direction(start_pos, node_pos), + speed = npc.actions.one_nps_speed + } + -- Enqueue walk step + npc.add_action(self, npc.actions.cmd.WALK_STEP, args) + end + -- Add standing action to look at node + npc.add_action(self, npc.actions.cmd.STAND, + {dir = npc.actions.get_direction(self.object:getpos(), node_pos)} + ) else -- Set end pos to be node_pos args = { end_pos = actions[node.name][i].args.end_pos or node_pos, - walkable = actions[node.name][i].args.walkable or {} + walkable = actions[node.name][i].args.walkable or walkable_nodes or {} } + -- Enqueue + npc.add_task(self, actions[node.name][i].task, args) end - elseif actions[node.name][i].action == npc.actions.cmd.USE_FURNACE then + elseif actions[node.name][i].task == npc.actions.cmd.USE_FURNACE then -- Defaults: pos is node_pos. Freeze is true args = { pos = actions[node.name][i].args.pos or node_pos, item = actions[node.name][i].args.item, freeze = actions[node.name][i].args.freeze or true } + npc.add_task(self, actions[node.name][i].task, args) + else + -- Action or task that is not supported for value calculation + npc.enqueue_schedule_action(self, actions[node.name][i]) end - -- Enqueue actions - npc.add_action(self, action or actions[node.name][i].action, args or actions[node.name][i].args) end + -- Increase execution count + self.schedules.current_check_params.execution_count = + self.schedules.current_check_params.execution_count + 1 -- Enqueue next schedule check if self.schedules.current_check_params.execution_count < self.schedules.current_check_params.execution_times then - npc.add_schedule_check() + npc.add_schedule_check(self) end - -- Nodes found - return true + npc.log("DEBUG_SCHEDULE", "Actions queue: "..dump(self.actions.queue)) else -- No nodes found, enqueue none_actions for i = 1, #none_actions do + -- Add start_pos to none_actions + none_actions[i].args["start_pos"] = start_pos -- Enqueue actions npc.add_action(self, none_actions[i].action, none_actions[i].args) end + -- Increase execution count + self.schedules.current_check_params.execution_count = + self.schedules.current_check_params.execution_count + 1 -- Enqueue next schedule check if self.schedules.current_check_params.execution_count < self.schedules.current_check_params.execution_times then - npc.add_schedule_check() + npc.add_schedule_check(self) end -- No nodes found - return false + npc.log("DEBUG_SCHEDULE", "Actions queue: "..dump(self.actions.queue)) end end @@ -1253,7 +1306,7 @@ mobs:register_mob("advanced_npc:npc", { local item = clicker:get_wielded_item() local name = clicker:get_player_name() - npc.log("INFO", "Right-clicked NPC: "..dump(self)) + npc.log("DEBUG", "Right-clicked NPC: "..dump(self)) -- Receive gift or start chat. If player has no item in hand -- then it is going to start chat directly @@ -1371,7 +1424,7 @@ mobs:register_mob("advanced_npc:npc", { if self.actions.walking.is_walking == true then -- Move NPC to expected position to ensure not getting lost local pos = self.actions.walking.target_pos - self.object:moveto({x=pos.x, y=pos.y-0.5, z=pos.z}) + self.object:moveto({x=pos.x, y=pos.y, z=pos.z}) end -- Execute action self.freeze = npc.execute_action(self) @@ -1395,18 +1448,18 @@ mobs:register_mob("advanced_npc:npc", { time = (time) - (time % 1) -- Check if there is a schedule entry for this time -- Note: Currently only one schedule is supported, for day 0 - minetest.log("Time: "..dump(time)) + npc.log("DEBUG_SCHEDULE", "Time: "..dump(time)) local schedule = self.schedules.generic[0] if schedule ~= nil then -- Check if schedule for this time exists if schedule[time] ~= nil then - npc.log("DEBUG", "Adding actions to action queue") + npc.log("DEBUG_SCHEDULE", "Adding actions to action queue") -- Add to action queue all actions on schedule for i = 1, #schedule[time] do -- Check if schedule has a check function if schedule[time][i].check then -- Add parameters for check function and run for first time - npc.log("DEBUG", "NPC "..dump(self.npc_name).." is starting check on "..minetest.pos_to_string(self.object:getpos())) + npc.log("DEBUG", "NPC "..dump(self.npc_id).." is starting check on "..minetest.pos_to_string(self.object:getpos())) local check_params = schedule[time][i] -- Calculates how many times check will be executed local execution_times = check_params.count @@ -1416,16 +1469,18 @@ mobs:register_mob("advanced_npc:npc", { -- Set current parameters self.schedules.current_check_params = { range = check_params.range, + walkable_nodes = check_params.walkable_nodes, nodes = check_params.nodes, actions = check_params.actions, none_actions = check_params.none_actions, execution_count = 0, execution_times = execution_times } - -- Execute check for the first time - npc.schedule_check(self) + -- Enqueue the schedule check + npc.add_schedule_check(self) else - npc.log("DEBUG", "Executing schedule entry: "..dump(schedule[time][i])) + npc.log("DEBUG_SCHEDULE", "Executing schedule entry for NPC "..dump(self.npc_id)..": " + ..dump(schedule[time][i])) -- Run usual schedule entry -- Check chance local execution_chance = math.random(1, 100) @@ -1466,12 +1521,12 @@ mobs:register_mob("advanced_npc:npc", { end -- Clear execution queue self.schedules.temp_executed_queue = {} - npc.log("DEBUG", "New action queue: "..dump(self.actions)) + npc.log("DEBUG", "New action queue: "..dump(self.actions.queue)) end end else -- Check if lock can be released - if (time % 1) > dtime then + if (time % 1) > dtime + 0.1 then -- Release lock self.schedules.lock = false end diff --git a/occupations/occupations.lua b/occupations/occupations.lua index a5d4279..e56b388 100644 --- a/occupations/occupations.lua +++ b/occupations/occupations.lua @@ -293,32 +293,42 @@ end -- This function scans all registered occupations and filter them by -- building type and surrounding building type, returning an array -- of occupation names (strings) +-- BEWARE! Below this lines lies ugly, incomprehensible code! function npc.occupations.get_for_building(building_type, surrounding_building_types) local result = {} for name,def in pairs(npc.occupations.registered_occupations) do -- Check for empty or nil building types, in that case, any building - if def.building_types == nil or def.building_types == {} then + if def.building_types == nil or def.building_types == {} + and def.surrounding_building_types == nil or def.surrounding_building_types == {} then + --minetest.log("Empty") -- Empty building types, add to result table.insert(result, name) - else + elseif def.building_types ~= nil and #def.building_types > 0 then -- Check if building type is contained in the def's building types if npc.utils.array_contains(def.building_types, building_type) then - -- Check for empty or nil surrounding building types - if def.surrounding_building_types == nil - or def.surrounding_building_types == {} then - -- Add this occupation - table.insert(result, name) - else - -- Check if surround type is included in the def - if npc.utils.array_is_subset_of_array( - def.surrounding_building_types, - surrounding_building_types) - or npc.utils.array_is_subset_of_array( - surrounding_building_types, - def.surrounding_building_types) - then - -- Add this occupation - table.insert(result, name) + table.insert(result, name) + end + end + -- Check for empty or nil surrounding building types + if def.surrounding_building_types ~= nil + and #def.surrounding_building_types > 0 then +-- -- Add this occupation +-- --table.insert(result, name) +-- else + -- Surrounding buildings is not empty, loop though them and compare + -- to the given ones + for i = 1, #surrounding_building_types do + for j = 1, #def.surrounding_building_types do + -- Check if the definition's surrounding building type is the same + -- as the given one + if def.surrounding_building_types[j].type + == surrounding_building_types[i].type then + -- Check if the origin buildings contain the expected type + if npc.utils.array_contains(def.surrounding_building_types[j].origin_building_types, + surrounding_building_types[i].origin_building_type) then + -- Add this occupation + table.insert(result, name) + end end end end @@ -342,7 +352,6 @@ function npc.occupations.initialize_occupation_values(self, occupation_name) npc.log("INFO", "Overriding NPC values using occupation '"..dump(occupation_name).."' values") -- Initialize textures, else it will leave the current textures - minetest.log("Texture entries: "..dump(table.getn(def.textures))) if def.textures and table.getn(def.textures) > 0 then self.selected_texture = npc.get_random_texture_from_array(self.sex, self.age, def.textures) diff --git a/spawner.lua b/spawner.lua index 994905e..e32bd72 100644 --- a/spawner.lua +++ b/spawner.lua @@ -92,11 +92,11 @@ spawner.spawn_eggs = {} function npc.spawner.scan_area_for_spawn(start_pos, end_pos, player_name) local result = { building_type = "", - building_plot_info = {}, - building_entrance = {}, - building_usable_nodes = {}, - building_npcs = {}, - building_npc_stats = {} + plot_info = {}, + entrance = {}, + node_data = {}, + npcs = {}, + npc_stats = {} } -- Set building_type @@ -104,7 +104,7 @@ function npc.spawner.scan_area_for_spawn(start_pos, end_pos, player_name) -- Get min pos and max pos local minp, maxp = vector.sort(start_pos, end_pos) -- Set plot info - result.building_plot_info = { + result.plot_info = { -- TODO: Check this and see if it is accurate! xsize = maxp.x - minp.x, ysize = maxp.y - minp.y, @@ -147,13 +147,13 @@ function npc.spawner.scan_area_for_spawn(start_pos, end_pos, player_name) if entrance then npc.log("INFO", "Found building entrance at: "..minetest.pos_to_string(entrance.node_pos)) -- Set building entrance - result.building_entrance = entrance + result.entrance = entrance else npc.log("ERROR", "Unable to find building entrance!") end -- Set node_data - result.building_usable_nodes = usable_nodes + result.node_data = usable_nodes -- Initialize NPC stats -- Initialize NPC stats @@ -171,7 +171,7 @@ function npc.spawner.scan_area_for_spawn(start_pos, end_pos, player_name) adult_total = 0, child_total = 0 } - result.building_npc_stats = npc_stats + result.npc_stats = npc_stats return result end @@ -179,38 +179,48 @@ end --------------------------------------------------------------------------------------- -- Spawning functions --------------------------------------------------------------------------------------- - +-- This function attempts to determine an occupation for an NPC given: +-- - The local building type (building NPC is spawning) +-- - The surrounding workplaces' building types +-- - The NPCs in the local building +-- Lo and behold! In this function lies a code monster, smelly, incomprehensible.. function npc.spawner.determine_npc_occupation(building_type, workplace_nodes, npcs) local surrounding_buildings_map = {} local current_building_map = {} local current_building_npc_occupations = {} + local result = {} + -- Get all occupation names in the current building for i = 1, #npcs do if not npc.utils.array_contains(current_building_npc_occupations, npcs[i].occupation) then table.insert(current_building_npc_occupations, npcs[i].occupations) end end - minetest.log("Occupation names in current building: "..dump(current_building_npc_occupations)) - -- Classify workplaces + -- Classify workplaces into local and surrounding for i = 1, #workplace_nodes do local workplace = workplace_nodes[i] if workplace.surrounding_workplace == true then - surrounding_buildings_map[workplace.building_type] = workplace + table.insert(surrounding_buildings_map, + {type=workplace.building_type, origin_building_type=building_type}) else current_building_map[workplace.building_type] = workplace end end - minetest.log("Surrounding workplaces map: "..dump(surrounding_buildings_map)) - minetest.log("Current building type: "..dump(building_type)) -- Get occupation names for the buildings local occupation_names = npc.occupations.get_for_building( building_type, - npc.utils.get_map_keys(surrounding_buildings_map) + surrounding_buildings_map ) - ----------------------- + npc.log("DEBUG, ""Found occupations: "..dump(occupation_names).."\nfor local building type: " + ..dump(building_type).."\nAnd surrounding building types: "..dump(surrounding_buildings_map)) + + -- Insert default occupation into result + table.insert(result, {name=npc.occupations.basic_name, node={node_pos={}}}) + + --------------------------------------------------------------------------------------- -- Determine occupation - ----------------------- + --------------------------------------------------------------------------------------- -- First of all, iterate through all names, discard the default basic occupation. -- Next, check if no-one in this builiding has this occupation name. -- Next, check if the workplace node has no data assigned to it. @@ -219,37 +229,91 @@ function npc.spawner.determine_npc_occupation(building_type, workplace_nodes, np -- Note: Much more can be done here. This is a simplistic implementation, -- given this is already complicated enough. For example, existing NPCs' occupation -- can play a much more important role, not only taken in consideration for discarding. + -- Beware: Incomprehensible code lies ahead for i = 1, #occupation_names do -- Check if this occupation name is the default occupation, and if it is, continue if occupation_names[i] ~= npc.occupations.basic_name then -- Check if someone already works on this - if not npc.utils.array_contains(current_building_npc_occupations, occupation_names[i]) then + if npc.utils.array_contains(current_building_npc_occupations, occupation_names[i]) == false then -- Check if someone else already has this occupation at the same workplace for j = 1, #workplace_nodes do -- Get building types from occupation local local_building_types = - npc.occupations.registered_occupations[occupation_names[i]].building_type or {} + npc.occupations.registered_occupations[occupation_names[i]].building_types or {} local surrounding_building_types = - npc.occupations.registered_occupations[occupation_names[i]].surrounding_building_types or {} - minetest.log("Occupation btype: "..dump(local_building_types)) - minetest.log("Surrounding btypes: "..dump(surrounding_building_types)) - -- Check the workplace_node is of any of those building_types - if npc.utils.array_contains(local_building_types, workplace_nodes[j].building_type) or - npc.utils.array_contains(surrounding_building_types, workplace_nodes[j].building_type) then - minetest.log("Found corresponding node: "..dump(workplace_nodes[j])) + npc.occupations.registered_occupations[occupation_names[i]].surrounding_building_types or {} + + -- Attempt to match the occupation definition's local and surrounding types + -- to the workplace node's building type. + local local_building_match = false + local surrounding_building_match = false + + -- Check if there is building_type match between the def's local + -- building_types and the current workplace node's building_type + if #local_building_types > 0 then + local_building_match = + npc.utils.array_contains(local_building_types, workplace_nodes[j].building_type) + end + + -- Check if there is building_type match between the def's surrounding + -- building_types and the current workplace node's building_type + if #surrounding_building_types > 0 then + for k = 1, #surrounding_building_types do + if surrounding_building_types[k].type == workplace_nodes[j].building_type then + surrounding_building_match = true + break + end + end + end + -- Check if there was a match + if local_building_match == true or surrounding_building_match == true then + -- Match found, attempt to map this workplace node to the + -- current occupation. How? Well, if the workplace isn't being + -- used by another NPC, then, use it local meta = minetest.get_meta(workplace_nodes[j].node_pos) local worker_data = minetest.deserialize(meta:get_string("work_data") or "") + npc.log("DEBUG", "Found worker data: "..dump(worker_data)) -- If no worker data is found, then create it if not worker_data then - return {name=occupation_names[i], node=workplace_nodes[j]} + npc.log("INFO", "Found suitable occupation and workplace: "..dump(result)) + table.insert(result, {name=occupation_names[i], node=workplace_nodes[j]}) end end end - end end end - return {name=npc.occupations.basic_name, node={}} + + -- Determine result. Choose profession, how to do it? + -- First, check previous NPCs' occupation. + -- - If there is a NPC working at something, check the NPC count. + -- - If count is less than three (only two NPCs), default_basic occupation. + -- - If count is greater than two, assign any eligible occupation with 50% chance + -- - If not NPC is working, choose an occupation that is not default_basic + if #current_building_npc_occupations > 0 then + for i = 1, #current_building_npc_occupations do + if current_building_npc_occupations[i] ~= npc.occupations.basic_name then + if #current_building_npc_occupations < 3 then + -- Choose basic default occupation + return result[1] + elseif #current_building_npc_occupations > 2 then + -- Choose any occupation + return result[math.random(1, #result)] + end + end + end + else + -- Check how many occupation names we have + if #result == 1 then + -- Choose basic default occupation + return result[1] + else + -- Choose an occupation with equal chance each + return result[math.random(2, #result)] + end + end + -- By default, if nothing else works, return basic default occupation + return result[1] end -- This function is called when the node timer for spawning NPC @@ -364,8 +428,9 @@ function npc.spawner.spawn_npc(pos, area_info, occupation_name, occupation_workp else npc.initialize(ent, pos, nil, nil, occupation) end - -- If entrance and node_data are present, assign nodes - if entrance and node_data then + -- If node_data is present, assign nodes + minetest.log("Node data: "..dump(node_data)) + if node_data then npc.spawner.assign_places(ent:get_luaentity(), entrance, node_data, pos) end -- Store spawned NPC data and stats into node @@ -499,15 +564,20 @@ function npc.spawner.assign_places(self, entrance, node_data, pos) end -- Assign workplace nodes + -- Beware: More incomprehensibe code lies ahead! if #node_data.workplace_type > 0 then -- First, find the workplace_node that was marked for i = 1, #node_data.workplace_type do minetest.log("In assign places: workplace nodes: "..dump(node_data.workplace_type)) + minetest.log("Condition? "..dump(node_data.workplace_type[i].occupation + and node_data.workplace_type[i].occupation == self.occupation_name)) if node_data.workplace_type[i].occupation and node_data.workplace_type[i].occupation == self.occupation_name then + -- Walkable nodes from occupation + local walkables = npc.occupations.registered_occupations[self.occupation_name].walkable_nodes -- Found the node. Assign only this node to the NPC. npc.places.add_shared_accessible_place(self, {node_data.workplace_type[i]}, - npc.places.PLACE_TYPE.WORKPLACE.PRIMARY) + npc.places.PLACE_TYPE.WORKPLACE.PRIMARY, false, walkables) -- Edit metadata of this workplace node to not allow it for other NPCs local meta = minetest.get_meta(node_data.workplace_type[i].node_pos) local work_data = { @@ -517,9 +587,6 @@ function npc.spawner.assign_places(self, entrance, node_data, pos) npc.occupations.registered_occupations[self.occupation_name].allow_multiple_npcs_at_workplace } meta:set_string("work_data", minetest.serialize(work_data)) - -- - -- meta = minetest.get_meta(node_data.workplace_type[i].node_pos) - -- minetest.log("Work data: "..dump(minetest.deserialize(meta:get_string("work_data")))) end end end @@ -591,6 +658,139 @@ end -- and the plot, entrance and workplace of the NPC. All of these are optional -- and default values will be chosen whenever no input is provided. +-- This map holds the spawning position chosen by a player at a given time. +spawner.spawn_pos = {} + +-- Spawn egg (WIP) +-- Use for manually spawning NPCs. Up to now, supports local occupations only. +minetest.register_craftitem("advanced_npc:spawn_egg", { + description = "NPC Spawner", + inventory_image = "mobs_chicken_egg.png^(default_brick.png^[mask:mobs_chicken_egg_overlay.png)", + on_place = function(itemstack, user, pointed_thing) + -- Store spawn pos + spawner.spawn_pos[user:get_player_name()] = pointed_thing.above + + + + local occupation_names = npc.utils.get_map_keys(npc.occupations.registered_occupations) + + local building_dropdown_string = "dropdown[0.5,0.75;6;building_type;" + for i = 1, #npc.spawner.mg_villages_supported_building_types do + building_dropdown_string = building_dropdown_string + ..npc.spawner.mg_villages_supported_building_types[i].."," + end + building_dropdown_string = building_dropdown_string..";1]" + + -- Generate occupation dropdown + local occupation_dropdown_string = "dropdown[0.5,1.95;6;occupation_name;" + for i = 1, #occupation_names do + occupation_dropdown_string = occupation_dropdown_string..occupation_names[i].."," + end + occupation_dropdown_string = occupation_dropdown_string..";1]" + + local formspec = "size[7,7]".. + "label[0.1,0.25;Building type]".. + building_dropdown_string.. + "label[0.1,1.45;Occupation]".. + occupation_dropdown_string.. + "button_exit[2.25,6.25;2.5,0.75;exit;Spawn]" + + minetest.show_formspec(user:get_player_name(), "advanced_npc:spawn_egg_main", formspec) + end +}) + +-- This map holds the name of the player and the position of the workplace that +-- he/she placed +spawner.workplace_pos = {} + +-- Manual workplace marker (WIP) +-- Put this where a workplace for a NPC is. For example: in a cotton field, +-- inside a church, etc. +minetest.register_node("advanced_npc:workplace_marker", { + description = "NPC Workplace Marker", + tiles = {"default_stone.png"}, + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + {-0.5+2/16, -0.5, -0.5+2/16, 0.5-2/16, -0.5+3/16, 0.5-2/16}, + }, + }, + groups = {cracky=1}, + after_place_node = function(pos, placer, itemstack, pointed_thing) + local meta = minetest.get_meta(pos) + meta:set_string("infotext", "Unconfigured workplace marker") + end, + on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) + -- Read current value + local meta = minetest.get_meta(pos) + local building_type = meta:get_string("building_type") or "" + -- Consider changing the field for a dropdown + local formspec = "size[7,3]".. + "label[0.1,0.25;Building type]".. + "field[0.5,1;6.5,2;text;(farm_tiny, farm_full, house, church, etc.);"..building_type.."]".. + "button_exit[2.25,2.25;2.5,0.75;exit;Proceed]" + + workplace_pos[clicker:get_player_name()] = pos + + minetest.show_formspec(clicker:get_player_name(), "advanced_npc:workplace_marker_formspec", formspec) + end, +}) + +-- Handle formspecs +minetest.register_on_player_receive_fields(function(player, formname, fields) + + if formname then + -- Handle spawn egg formspec + if formname == "advanced_npc:spawn_egg_main" then + if fields then + -- Handle exit (spawn) button + if fields.exit then + local pos = spawner.spawn_pos[player:get_player_name()] + local start_pos = {x=pos.x-20, y=pos.y-2, z=pos.z-20 } + local end_pos = {x=pos.x+20, y=pos.y+2, z=pos.z+20 } + + -- Scan for usable nodes + local area_info = npc.spawner.scan_area_for_spawn(start_pos, end_pos, player:get_player_name()) + + -- Assign occupation + local occupation_data = npc.spawner.determine_npc_occupation( + fields.building_type or area_info.building_type, + area_info.node_data.workplace_type, + area_info.npcs) + + -- Assign workplace node + if occupation_data then + for i = 1, #area_info.node_data.workplace_type do + if area_info.node_data.workplace_type[i].node_pos == occupation_data.node.node_pos then + -- Found node, mark it as being used by NPC + area_info.node_data.workplace_type[i]["occupation"] = occupation_data.name + end + end + end + + -- Spawn NPC + local metadata = npc.spawner.spawn_npc(pos, area_info, fields.occupation_name) + end + end + end + -- Handle workplace marker formspec + if formname == "advanced_npc:workplace_marker_formspec" then + if fields then + local pos = workplace_pos[player:get_player_name()] + if pos and fields.text then + local meta = minetest.get_meta(pos) + meta:set_string("building_type", fields.text) + meta:set_string("infotext", fields.text.." (workplace)") + end + end + end + end + +end) + --------------------------------------------------------------------------------------- -- Support code for mg_villages mods @@ -602,9 +802,6 @@ if minetest.get_modpath("mg_villages") ~= nil then -- point and the building_data to get the x, y and z-coordinate size -- of the building schematic function spawner.scan_mg_villages_building(pos, building_data) - --minetest.log("--------------------------------------------") - --minetest.log("Building data: "..dump(building_data)) - --minetest.log("--------------------------------------------") -- Get area of the building local x_size = building_data.bsizex local y_size = building_data.ysize @@ -759,79 +956,23 @@ if minetest.get_modpath("mg_villages") ~= nil then -- Node registration -- This node is currently a slightly modified mg_villages:plotmarker - -- TODO: Change formspec to a more detailed one. minetest.override_item("mg_villages:plotmarker", { - -- description = "Automatic NPC Spawner", - -- drawtype = "nodebox", - -- tiles = {"default_stone.png"}, - -- paramtype = "light", - -- paramtype2 = "facedir", - -- node_box = { - -- type = "fixed", - -- fixed = { - -- {-0.5+2/16, -0.5, -0.5+2/16, 0.5-2/16, -0.5+2/16, 0.5-2/16}, - -- --{-0.5+0/16, -0.5, -0.5+0/16, 0.5-0/16, -0.5+0/16, 0.5-0/16}, - -- } - -- }, walkable = false, groups = {cracky=3,stone=2}, + -- TODO: Change formspec to a more detailed one. on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) - -- NOTE: This is temporary code for testing... - local nodedata = minetest.deserialize(minetest.get_meta(pos):get_string("node_data")) - --minetest.log("Node data: "..dump(nodedata)) - --minetest.log("Entrance: "..dump(minetest.deserialize(minetest.get_meta(pos):get_string("entrance")))) - --minetest.log("First-floor beds: "..dump(spawner.filter_first_floor_nodes(nodedata.bed_type, pos))) - --local entrance = npc.places.find_entrance_from_openable_nodes(nodedata.openable_type, pos) - --minetest.log("Found entrance: "..dump(entrance)) - minetest.log("Replaced: "..dump(minetest.get_meta(pos):get_string("replaced"))) - -- for i = 1, #nodedata.bed_type do - -- nodedata.bed_type[i].owner = "" - -- end - -- minetest.get_meta(pos):set_string("node_data", minetest.serialize(nodedata)) - -- minetest.log("Cleared bed owners") - --minetest.log("NPC stats: "..dump(minetest.deserialize(minetest.get_meta(pos):get_string("npc_stats")))) - return mg_villages.plotmarker_formspec( pos, nil, {}, clicker ) end, - -- on_receive_fields = function(pos, formname, fields, sender) - -- return mg_villages.plotmarker_formspec( pos, formname, fields, sender ); - -- end, - on_timer = function(pos, elapsed) + -- Adds timer support npc.spawner.spawn_npc_on_plotmarker(pos) end, - - -- protect against digging - -- can_dig = function(pos, player) - -- local meta = minetest.get_meta(pos); - -- if (meta and meta:get_string("village_id") ~= "" and meta:get_int("plot_nr") and meta:get_int("plot_nr") > 0 ) then - -- return false; - -- end - -- return true; - -- end }) - -- LBM Registration - -- Used to modify plotmarkers and replace them with advanced_npc:plotmarker_auto_spawner - -- minetest.register_lbm({ - -- label = "Replace mg_villages:plotmarker with Advanced NPC auto spawners", - -- name = "advanced_npc:mg_villages_plotmarker_replacer", - -- nodenames = {"mg_villages:plotmarker"}, - -- run_at_every_load = false, - -- action = function(pos, node) - -- -- Check if replacement is activated - -- if npc.spawner.replace_activated then - -- -- Replace mg_villages:plotmarker - -- spawner.replace_mg_villages_plotmarker(pos) - -- -- Set NPCs to spawn - -- spawner.calculate_npc_spawning(pos) - -- end - -- end - -- }) - -- ABM Registration + -- Consider changing this to be on nodetimer minetest.register_abm({ label = "Replace mg_villages:plotmarker with Advanced NPC auto spawners", nodenames = {"mg_villages:plotmarker"}, @@ -841,13 +982,6 @@ if minetest.get_modpath("mg_villages") ~= nil then action = function(pos, node, active_object_count, active_object_count_wider) -- Check if replacement is needed local meta = minetest.get_meta(pos) - if meta then - -- minetest.log("------ Plotmarker metadata -------") - -- local plot_nr = meta:get_int("plot_nr") - -- local village_id = meta:get_string("village_id") - -- minetest.log("Plot nr: "..dump(plot_nr)..", village ID: "..dump(village_id)) - -- minetest.log(dump(mg_villages.get_plot_and_building_data( village_id, plot_nr ))) - end if minetest.get_meta(pos):get_string("replaced") == "true" then return end @@ -861,8 +995,6 @@ if minetest.get_modpath("mg_villages") ~= nil then end ---minetest.register_alias_force("mg_villages:plotmarker", ) - -- Chat commands to manage spawners minetest.register_chatcommand("restore_plotmarkers", { description = "Replaces all advanced_npc:plotmarker_auto_spawner with mg_villages:plotmarker in the specified radius.",