local MODPATH = minetest.get_modpath(minetest.get_current_modname()) local BUILDER_REQ_MATERIALS = minetest.setting_getbool("creative_mode") == false local MAX_SPEED = 5 local MAX_POS = 1000 local DEFAULT_NODE = {name="air"} local SCHEMS = {"basic_hut.we"} local INSTABUILD_PATH = minetest.get_modpath("instabuild") local SCHEMLIB_PATH = minetest.get_modpath("schemlib") if INSTABUILD_PATH then for _,v in ipairs({"factory.we", "large_warehouse.we", "small_farm.we", "tall_tower.we", "large_farm.we", "mansion.we", "small_house.we", "large_house.we", "modern_house.we", "small_hut.we", "large_hut.we", "short_tower.we", "small_warehouse.we"}) do table.insert(SCHEMS, v) end end local function reset_build(self) self.var.nodedata = {} self.var.nodelist = {} self.metadata.index = nil self.metadata.schematic = nil self.metadata.build_pos = nil self.metadata.building = false self.schemlib_plan = nil self.schemlib_npc_ai = nil end local function get_registered_itemname(name) if string.find(name, "^doors") then name = name:gsub("_[tb]_[12]", "") elseif string.find(name, "^stairs") then name = name:gsub("upside_down", "") elseif string.find(name, "^farming") then name = name:gsub("_%d", "") end return name end local function get_registered_nodename(name) if string.find(name, "^doors.*_[ab]_[12]$") then name = name:gsub("_[12]", "") elseif string.find(name, "^doors.*_t_[12]$") then name = "doors:hidden" end return name end local function load_schematic(self, filename) local input = nil local fullpath if INSTABUILD_PATH then fullpath = INSTABUILD_PATH.."/models/"..filename else fullpath = MODPATH.."/schems/"..filename end if SCHEMLIB_PATH then self.schemlib_plan = schemlib.plan.new() self.schemlib_plan:read_from_schem_file(fullpath) if self.schemlib_plan.data.nodecount == 0 then print("file could not be read") reset_build(self) return end self.schemlib_plan.data.anchor_pos = self.metadata.build_pos self.schemlib_plan:apply_flood_with_air(3, 0, 3) for name, nodeinfo in pairs(self.schemlib_plan.data.nodeinfos) do local cost_item = schemlib.mapping.get_cost_item(name, self.schemlib_plan) if cost_item and cost_item ~= schemlib.mapping.c_free_item then self.var.nodelist[cost_item] = nodeinfo.count self.metadata.inventory[cost_item] = self.metadata.inventory[cost_item] or 0 end end else input = io.open(fullpath, "r") if input then local data = minetest.deserialize(input:read('*all')) io.close(input) table.sort(data, function(a,b) if a.y == b.y then if a.z == b.z then return a.x > b.x end return a.z > b.z end return a.y > b.y end) local sorted = {} local pos = {x=0, y=0, z=0} while #data > 0 do local index = 1 local min_pos = {x=MAX_POS, y=MAX_POS, z=MAX_POS} for i,v in ipairs(data) do if v.y < min_pos.y or vector.distance(pos, v) < vector.distance(pos, min_pos) then min_pos = v index = i end end local node = data[index] table.insert(sorted, node) table.remove(data, index) pos = {x=node.x, y=node.y, z=node.z} end self.var.nodedata = {} self.var.nodelist = {} for i,v in ipairs(sorted) do if v.name and v.param1 and v.param2 and v.x and v.y and v.z then local item_name = get_registered_itemname(v.name) local node_name = get_registered_nodename(v.name) local node = {name=node_name, param1=v.param1, param2=v.param2} local pos = vector.add(self.metadata.build_pos, {x=v.x, y=v.y, z=v.z}) if minetest.registered_items[item_name] then self.metadata.inventory[item_name] = self.metadata.inventory[item_name] or 0 self.var.nodelist[item_name] = self.var.nodelist[item_name] or 0 self.var.nodelist[item_name] = self.var.nodelist[item_name] + 1 else node = DEFAULT_NODE end self.var.nodedata[i] = {pos=pos, node=node, item_name=item_name} end end end end end local function show_build_form(self, player_name) local nodelist = {} for k,v in pairs(self.var.nodelist) do if not SCHEMLIB_PATH then if string.find(k, "^doors") then v = v * 0.5 end end if self.metadata.inventory[k] then v = v - self.metadata.inventory[k] end if v < 0 then v = 0 end if minetest.registered_items[k].description ~= "" then k = minetest.registered_items[k].description end table.insert(nodelist, k.." ("..v..")") end local materials = table.concat(nodelist, ",") or "" local title = self.metadata.schematic:gsub("%.we","") local button_build = "button_exit[5.0,1.0;3.0,0.5;build_start;Begin Build]" if self.metadata.index then button_build = "button_exit[5.0,1.0;3.0,0.5;build_resume;Resume Build]" end local formspec = "size[8,9]" .."label[3.0,0.0;Project: "..title.."]" .."textlist[0.0,1.0;4.0,3.5;inv_sel;"..materials..";"..self.var.selected..";]" .."list[current_player;main;0.0,5.0;8.0,4.0;]" ..button_build if BUILDER_REQ_MATERIALS == true then formspec = formspec.."list[detached:npcf_"..self.npc_id..";input;6.0,3.5;1,1;]" end if self.owner == player_name then formspec = formspec.."button_exit[5.0,2.0;3.0,0.5;build_cancel;Cancel Build]" end npcf:show_formspec(player_name, self.npc_id, formspec) end local function get_speed(distance) local speed = distance * 0.5 if speed > MAX_SPEED then speed = MAX_SPEED end return speed end npcf:register_npc("npcf_builder:npc" ,{ description = "Builder NPC", textures = {"npcf_builder_skin.png"}, metadata = { schematic = nil, inventory = {}, index = nil, build_pos = nil, building = false, }, var = { selected = "", nodelist = {}, nodedata = {}, last_pos = {}, }, stepheight = 1.1, inventory_image = "npcf_builder_inv.png", on_activate = function(self) self.metadata.building = false if self.metadata.schematic and self.metadata.build_pos then load_schematic(self, self.metadata.schematic) end local inv = minetest.create_detached_inventory("npcf_"..self.npc_id, { on_put = function(inv, listname, index, stack, player) local player_name = player:get_player_name() local item = stack:get_name() if player_name and self.metadata.inventory[item] then self.metadata.inventory[item] = self.metadata.inventory[item] + stack:get_count() inv:remove_item("input", stack) show_build_form(self, player_name) end end, }) inv:set_size("input", 1) end, on_rightclick = function(self, clicker) local player_name = clicker:get_player_name() if self.owner == player_name then if not self.metadata.schematic then local schemlist = table.concat(SCHEMS, ",") or "" local formspec = "size[6,5]" .."textlist[0.0,0.0;5.0,4.0;schemlist;"..schemlist..";;]" .."button_exit[5.0,4.5;1.0,0.5;;Ok]" npcf:show_formspec(player_name, self.npc_id, formspec) return elseif self.metadata.building == true then self.metadata.building = false return end end if self.metadata.schematic and self.metadata.building == false then show_build_form(self, player_name) end end, on_step = function(self, dtime) local mv_obj = npcf.movement.getControl(self) local pos = mv_obj.pos if self.timer > 1 then self.timer = 0 if not self.owner then return end mv_obj:mine_stop() if self.metadata.building == true then local nodedata local schemlib_node local distance if not SCHEMLIB_PATH then nodedata = self.var.nodedata[self.metadata.index] distance = vector.distance(pos, nodedata.pos) mv_obj:walk(nodedata.pos, get_speed(distance), {teleport_on_stuck = true}) else self.schemlib_npc_ai = self.schemlib_npc_ai or schemlib.npc_ai.new(self.schemlib_plan, 4) schemlib_node = self.schemlib_npc_ai:plan_target_get(pos) if not schemlib_node then --stuck in plan mv_obj:stop() if self.schemlib_plan.data.nodecount == 0 then reset_build(self) end return end local node_pos = schemlib_node:get_world_pos() distance = vector.distance(pos, node_pos) mv_obj:walk(node_pos, get_speed(distance), {teleport_on_stuck = true}) end if distance < 4 then mv_obj:mine() mv_obj.speed = 1 mv_obj:set_walk_parameter({teleport_on_stuck = false}) if SCHEMLIB_PATH then self.schemlib_npc_ai:place_node(schemlib_node) if BUILDER_REQ_MATERIALS == true and schemlib_node.cost_item ~= schemlib.mapping.c_free_item then if self.metadata.inventory[schemlib_node.cost_item] > 0 then self.metadata.inventory[schemlib_node.cost_item] = self.metadata.inventory[schemlib_node.cost_item] - 1 self.var.selected = "" else self.metadata.building = false mv_obj:mine_stop() mv_obj:stop() end end if self.schemlib_plan.data.nodecount == 0 then reset_build(self) end else if minetest.registered_nodes[nodedata.node.name].sounds then local soundspec = minetest.registered_nodes[nodedata.node.name].sounds.place if soundspec then soundspec.pos = pos minetest.sound_play(soundspec.name, soundspec) end end minetest.add_node(nodedata.pos, nodedata.node) if BUILDER_REQ_MATERIALS == true and nodedata.node.name ~= "doors:hidden" then if self.metadata.inventory[nodedata.item_name] > 0 then self.metadata.inventory[nodedata.item_name] = self.metadata.inventory[nodedata.item_name] - 1 self.var.selected = "" else self.metadata.building = false mv_obj:stop() mv_obj:mine_stop() local i = 0 for k,v in pairs(self.var.nodelist) do i = i + 1 if k == nodedata.item_name then self.var.selected = i break end end end end self.metadata.index = self.metadata.index + 1 if self.metadata.index > #self.var.nodedata then reset_build(self) end end end elseif vector.equals(pos, self.origin.pos) == false then local distance = vector.distance(pos, self.origin.pos) if distance > 1 then mv_obj:walk(self.origin.pos, get_speed(distance), {teleport_on_stuck = true}) else mv_obj.yaw = self.origin.yaw end end end end, on_receive_fields = function(self, fields, sender) local player_name = sender:get_player_name() if self.owner == player_name then if fields.schemlist then local id = tonumber(string.match(fields.schemlist, "%d+")) if id then if SCHEMS[id] then local pos = { x=math.ceil(self.origin.pos.x) + 1, y=math.floor(self.origin.pos.y), z=math.ceil(self.origin.pos.z) + 1 } self.metadata.schematic = SCHEMS[id] self.metadata.build_pos = pos load_schematic(self, self.metadata.schematic) end end elseif fields.build_cancel then reset_build(self) end end if fields.build_start then for i,v in ipairs(self.var.nodedata) do minetest.remove_node(v.pos) end self.metadata.index = 1 self.metadata.building = true elseif fields.build_resume then self.metadata.building = true end end, })