Alexander Weber 3124c5ae8f changes (22.09.2016):
- preparation for node overrides
- adjustment on route logic (processing in chunks) for really big buildings (>100.000 nodes)
2016-10-09 11:24:58 +02:00

660 lines
22 KiB
Lua

--[[
Towntest for Minetest
Copyright (c) 2012 cornernote, Brett O'Donnell <cornernote@gmail.com>
Source Code: https://github.com/cornernote/minetest-towntest
License: BSD-3-Clause https://raw.github.com/cornernote/minetest-towntest/master/LICENSE
CHEST
]]--
--if the value is to big, it can happen the builder stucks and just stay (beter hardware required in RL)
--if to low, it can happen the searching next near node is poor and the builder acts overwhelmed, fail to see some nearly gaps. The order seems to be randomized
--the right value is depend on building size. If the building (or the not builded rest) can full imaginated (less blocks in building then c_npc_imagination) there is the full search potencial active
local c_npc_imagination = 500
-- expose api
towntest_chest = {}
-- table of non playing characters
towntest_chest.npc = {}
-- debug. Used for debug messages. In production the function should be empty
local function dprint(...)
-- debug print. Comment out the next line if you don't need debug out
-- print(unpack(arg))
end
local modpath = minetest.get_modpath(minetest.get_current_modname())
-- get worldedit parser load_schematic from worldedit mod
dofile(modpath.."/".."worldedit-serialization.lua")
-- get_files
-- returns a table containing buildings
towntest_chest.get_files = function()
local lfs = require("lfs")
local i, t = 0, {}
for filename in lfs.dir(modpath .. "/buildings") do
if filename ~= "." and filename ~= ".." then
i = i + 1
t[i] = filename
end
end
table.sort(t,function(a,b) return a<b end)
return t
end
-- load
-- filename - the building file to load
-- return - WE-Shema, containing the pos and nodes to build
towntest_chest.load = function(filename)
local filepath = modpath.."/buildings/"..filename
local file, err = io.open(filepath, "rb")
if err ~= nil then
minetest.chat_send_player(placer:get_player_name(), "[towntest_chest] error: could not open file \"" .. filepath .. "\"")
return
end
-- load the building starting from the lowest y
local building_plan = towntest_chest.we_load_schematic(file:read("*a"))
return building_plan
end
-- towntest_chest.mapnodes Take filters and actions on nodes before building. Currently the payment item determination and check for registred node only
-- node - Node (from file) to check if buildable and payable
-- return - node with enhanced informations
towntest_chest.mapnodes = function(node)
-- no name given - something wrong
if not node or node.name == "" then
return nil
end
local node_chk = minetest.registered_items[node.name]
if not node_chk then
dprint("unknown node in building: "..node.name)
return nil
else
-- known node. Check for price or if it is free
if (node_chk.groups.not_in_creative_inventory and not (node_chk.groups.not_in_creative_inventory == 0)) or
(not node_chk.description or node_chk.description == "") then
if node_chk.drop then
-- use possible drop as payment
if type(node_chk.drop) == "table" then -- drop table
node.matname = node_chk.drop[1] -- use the first one
else
node.matname = node_chk.drop
end
else --something not supported, but known
node.matname = "free" -- will be build for free. they are something like doors:hidden or second part of coffee lrfurn:coffeetable_back
end
else -- build for payment the 1:1
node.matname = node.name
end
end
return node
end
-- is_equal_meta - compare meta information of 2 nodes
-- name - Node name to check and map
-- return - item name used as payment
local function is_equal_meta(a,b)
local typa = type(a)
local typb = type(b)
if typa ~= typb then
return false
end
if typa == "table" then
if #a ~= #b then
return false
else
for i,v in ipairs(a) do
if not is_equal_meta(a[i],b[i]) then
return false
end
end
return true
end
else
if a == b then
return true
end
end
end
-- skip_already_placed - check if the nodes are already placed
-- building_plan - filtered/enriched WE-Chema to process
-- chestpos - building chest position for alignment
-- return - filtered/enriched WE-Chema to process, without already placed nodes
local function skip_already_placed(building_plan, chestpos)
-- skip already right placed nodes. remove themfrom build plan. Usefull to resume the build
local building_out = {}
for idx, def in ipairs(building_plan) do
local pos = {x=def.x+chestpos.x,y=def.y+chestpos.y,z=def.z+chestpos.z}
local node_placed = minetest.get_node(pos)
if node_placed.name == def.name or node_placed.name == minetest.registered_nodes[def.name].name then -- right node is at the place. there are no costs to touch them
if not def.meta then
-- --same item without metadata. nothing to do
elseif is_equal_meta(minetest.get_meta(pos):to_table(), def.meta) then
-- --same metadata. Nothing to do
else
def.matname = "free" --metadata correction for free
table.insert(building_out, def)
dprint("rebuild to correct metadata",def.name)
end
elseif towntest_chest.mapnodes(node_placed).matname == towntest_chest.mapnodes(def).matname then
def.matname = "free" --same price. Check/set for free
table.insert(building_out, def)
dprint("rebuild for free because of the same drop",def.name)
else
table.insert(building_out, def) --rebuild for payment as usual
end
end
return building_out
end
-- do_prepare_building preprocessing of WE shema to be usable as building_plan
-- building_in: WE shema
-- return - filtered/enriched WE-Chema to process
towntest_chest.do_prepare_building = function(building_in)
local building_out = {}
for idx,def in pairs(building_in) do
if (def.x and def.y and def.z) and -- more robust. Values should be existing
(tonumber(def.x)~=0 or tonumber(def.y)~=0 or tonumber(def.z)~=0) then
local mapped_def = towntest_chest.mapnodes(def)
if mapped_def and mapped_def.matname then -- found
-- the node will be built
table.insert(building_out, mapped_def)
end
end
end
return building_out
end
-- update_needed - updates the needed inventory in the chest
-- inv - inventory object of the chest
-- building - table containing pos and nodes to build
towntest_chest.update_needed = function(inv,building)
dprint("update_needed - started")
for i=1,inv:get_size("needed") do
inv:set_stack("needed", i, nil)
end
if building == nil then
return
end
local materials = {}
-- sort by y to prefer lower nodes in building order. At the same level prefer nodes nearly the chest
table.sort(building,function(a,b)
if a and b then
return ((a.y<b.y) or (a.y==b.y and a.x+a.z<b.x+b.z)) end
end
)
dprint("update_needed - sort")
for i,v in ipairs(building) do
if v.matname ~= "free" then --free materials will be built for free
if not materials[v.matname] then
materials[v.matname] = {matname = v.matname, count = 1, order = i}
else
materials[v.matname].count = materials[v.matname].count+1
end
end
if i > (c_npc_imagination * 20) then --don't calculate all needs if it is really big value
break
end
end
dprint("update_needed - index")
-- order the needed by building plan order
local keys = {}
for key in pairs(materials) do
table.insert(keys, key)
end
table.sort(keys, function(a, b) return materials[a].order < materials[b].order end)
for _, key in ipairs(keys) do
inv:add_item("needed",materials[key].matname.." "..materials[key].count)
end
dprint("update_needed - finished")
end
-- set_status - activate or deactivate building
-- meta - meta object of the chest
-- status - integer (will toggle if status not given)
towntest_chest.set_status = function(meta,status)
if status==nil then
status=meta:get_int("building_status")
if status==1 then status=0 else status=1 end
end
if status==0 then
meta:set_string("infotext", "Building Chest (inactive)")
meta:set_int("building_status",0)
else
meta:set_string("infotext", "Building Chest (active)")
meta:set_int("building_status",1)
end
dprint("status changed", meta:get_int("building_status"))
end
-- build - build a node of the structure
-- chestpos - the position of the chest containing the instructions
towntest_chest.build = function(chestpos)
-- load the building_plan
local meta = minetest.env:get_meta(chestpos)
if meta:get_int("building_status")~=1 then return end
dprint("build step started")
local building_plan = minetest.deserialize((meta:get_string("building_plan")))
-- create the npc if needed
local inv = meta:get_inventory()
local k = chestpos.x..","..chestpos.y..","..chestpos.z
if not towntest_chest.npc[k] then
towntest_chest.npc[k] = minetest.env:add_entity(chestpos, "towntest_npc:builder")
towntest_chest.npc[k]:get_luaentity().chestpos = chestpos
towntest_chest.npc[k]:get_luaentity():moveto({x=chestpos.x,y=chestpos.y+1.5,z=chestpos.z},1)
if not inv:is_empty("builder") then
for i=1,inv:get_size("builder") do
inv:set_stack("builder", i, nil)
end
end
end
local npc = towntest_chest.npc[k]
local npclua = npc:get_luaentity()
if npclua and npclua.target == "reached" then
dprint("build step cancelled because the npc is working")
return --no thinking during working
end
local npcpos = npc:getpos()
if not npcpos then --fallback
npcpos = chestpos
end
local nextnode = {}
-- building plan
if building_plan then
dprint("current building plan chunksize:", #building_plan)
local laterprocnode = {}
local buildable_counter = 0
local really_stuck = false
-- search for next buildable node from builder inventory
dprint("start searching for the next node", #building_plan)
for i,v in ipairs(building_plan) do
-- is payed or for free and the item is at the end of building plan (all next items already built, to avoid all free items are placed at the first)
if inv:contains_item("builder", v.matname) or v.matname == "free" then
local pos = {x=v.x+chestpos.x,y=v.y+chestpos.y,z=v.z+chestpos.z}
local distance = math.abs(pos.x - npcpos.x) + math.abs(pos.y-(npcpos.y-10))*2 + math.abs(pos.z - npcpos.z)
if v.matname ~= "free" or (distance < 20 or i > (#building_plan-2)) then
--buildable and payale / or build the free items if it is really nearly, or if it is at the end of the building plan
buildable_counter = buildable_counter + 1
if not nextnode.v or (distance < nextnode.distance) then
nextnode.v = v
nextnode.i = i
nextnode.pos = pos
nextnode.distance = distance
elseif not laterprocnode.v or (distance > laterprocnode.distance) then -- the widest node in plan
laterprocnode.v = v
laterprocnode.i = i
laterprocnode.pos = pos
laterprocnode.distance = distance
end
end
else
-- not buildable anymore (material used up). remove from current building chunk
table.remove(building_plan,i)
end
--respect "c_npc_imagination"
if i > c_npc_imagination and nextnode.v then
dprint("stuck at:", buildable_counter)
if laterprocnode.v then
--move the widest node to the end of building plan to get a new slot free it next build tick
--maybe an other node can be found nearly
if buildable_counter >= c_npc_imagination-1 then
table.remove(building_plan,laterprocnode.i)
table.insert(building_plan,laterprocnode.v)
dprint("move to end of plan:", laterprocnode.v.name, "distance", laterprocnode.distance)
end
laterprocnode.v = nil
end
break
elseif i > (c_npc_imagination * 5 ) then
dprint("really stuck! try again at the next building step")
really_stuck = true
break
end
end
if really_stuck == true then
-- save current state and search again at the next step
meta:set_string("building_plan", minetest.serialize(building_plan))
return
end
end
-- next buildable node found
if nextnode.v then
dprint("next node:", nextnode.v.name, nextnode.v.matname, "distance", nextnode.distance)
-- check if npc is on the way or waiting. We can change the route in this case
if npclua and npclua.target ~= "reached" then
meta:set_string("building_plan", minetest.serialize(building_plan))
if not npclua.target or npclua.target.x ~= nextnode.pos.x or npclua.target.y ~= nextnode.pos.y+1.5 or npclua.target.z ~= nextnode.pos.z then
if npclua.target then
dprint("route changed!! old route was:", npclua.target.x, npclua.target.y, npclua.target.z)
end
-- move the npc to the build area
npclua:moveto({x=nextnode.pos.x, y=nextnode.pos.y+1.5, z=nextnode.pos.z}, 2, 2, 0, function(self,after_param)
-- take from the inv
if after_param.v.matname then
after_param.inv:remove_item("builder", after_param.v.matname.." 1")
end
-- add the node to the world
minetest.env:add_node(after_param.pos, {name=after_param.v.name,param1=after_param.v.param1,param2=after_param.v.param2})
if after_param.v.meta then
minetest.env:get_meta(after_param.pos):from_table(after_param.v.meta)
end
dprint("placed:", after_param.v.name, after_param.v.matname, "at", after_param.v.x, after_param.v.y, after_param.v.z)
-- update the chest building_plan
local building_plan = minetest.deserialize(meta:get_string("building_plan"))
for i,v in ipairs(building_plan) do
if v.x == after_param.v.x and v.y == after_param.v.y and v.z == after_param.v.z then
table.remove(building_plan,i)
break
end
end
meta:set_string("building_plan", minetest.serialize(building_plan))
-- update the chest building plan
building_plan = minetest.deserialize(meta:get_string("full_plan"))
for i,v in ipairs(building_plan) do
if v.x == after_param.v.x and v.y == after_param.v.y and v.z == after_param.v.z then
table.remove(building_plan,i)
break
end
end
meta:set_string("full_plan", minetest.serialize(building_plan))
end, {pos=nextnode.pos, v=nextnode.v, inv=inv, meta=nextnode.meta})
else
if npclua.target then
dprint("same route recalculated:", npclua.target.x, npclua.target.y, npclua.target.z)
end
end
end
nextnode.v = nil
else
dprint("<<--- get new items and re-sort building plan --->>>")
-- update the needed and sort
local full_plan = minetest.deserialize(meta:get_string("full_plan"))
towntest_chest.update_needed(meta:get_inventory(),full_plan)
local items_needed = true
for i,v in ipairs(full_plan) do
-- check if the chest has the node
if inv:contains_item("main", v.matname) then
items_needed = false
-- check if npc is already moving
if npclua and not npclua.target then
-- move the npc to the chest
npclua:moveto({x=chestpos.x, y=chestpos.y+1.5, z=chestpos.z}, 2, 2, 0, function(self, params)
-- check for food
local inv = params.inv
local full_plan = params.full_plan
local next_plan = {}
if not inv:is_empty("main") then
for i=1,inv:get_size("main") do
-- check if this is a food, if so take it
local stack = inv:get_stack("main", i)
if not stack:is_empty() then
local node = minetest.registered_nodes[stack:get_name()]
if node and node.name == "default:apple" then
local quality = 1
npclua.food = npclua.food + (stack:get_count() * quality * 4)
inv:set_stack("main", i, nil)
elseif node and node.groups.food ~= nil then
local quality = 4 - node.groups.food
npclua.food = npc.foodlua + (stack:get_count() * quality * 4)
inv:set_stack("main", i, nil)
end
end
end
end
for i,v in ipairs(full_plan) do
-- take from the inv
if inv:contains_item("main",v.matname.." 1") and inv:room_for_item("builder",v.matname.." 1") then
inv:add_item("builder",inv:remove_item("main",v.matname.." 1"))
inv:remove_item("needed", v.matname.." 1")
end
-- create next chunk to be processed. only buildable items
if inv:contains_item("builder",v.matname.." 1") then
table.insert(next_plan,v)
end
if i > c_npc_imagination * 20 then --limit the building plan chunk size
break
end
end
meta:set_string("building_plan", minetest.serialize(next_plan)) --save the used order
dprint("next building plan chunk", #next_plan)
end, {inv=inv,full_plan=full_plan})
break --there is a update loop in moveto-function
end
end
if i > c_npc_imagination * 5 then --limit the building plan chunk size
break
end
end
-- stop building and tell the player what we need
if npclua and items_needed then
npclua:moveto({x=chestpos.x,y=chestpos.y+1.5,z=chestpos.z},2)
towntest_chest.set_status(meta,0)
towntest_chest.update_needed(meta:get_inventory(),minetest.deserialize(meta:get_string("full_plan")))
end
end
end
-- formspec - get the chest formspec
towntest_chest.formspec = function(pos,page)
local formspec = ""
-- chest page
if page=="chest" then
formspec = formspec
.."size[10.5,9]"
.."list[current_player;main;0,5;8,4;]"
.."label[0,0; items needed:]"
.."list[current_name;needed;0,0.5;8,2;]"
.."label[0,2.5; put items here to build:]"
.."list[current_name;main;0,3;8,1;]"
.."label[8.5,0; builder:]"
.."list[current_name;builder;8.5,0.5;2,2;]"
.."label[8.5,2.5; lumberjack:]"
.."list[current_name;lumberjack;8.5,3;2,2;]"
return formspec
end
-- main page
formspec = formspec.."size[12,10]"
local pages = towntest_chest.get_files()
local x,y = 0,0
local p
local firstpage = 1
if string.sub(page,0,5) == "page_" then
firstpage = tonumber(string.sub(page,6))
firstpage = (firstpage - 1) * 30 + 1 -- 1, 31, 61, ...
end
local lastpage = #pages
if lastpage >= firstpage + 30 then
lastpage = firstpage + 30 -1
end
for i = firstpage,lastpage,1 do
p = pages[i]
if x == 12 then
y = y+1
x = 0
end
formspec = formspec .."button["..(x)..","..(y)..";4,0.5;building;"..p.."]"
x = x+4
end
if #pages == 0 then
formspec = formspec
.."label[4,4.5; no files found in buildings folder:]"
.."label[4,5.0; "..minetest.get_modpath("towntest_chest").."/buildings".."]"
end
local nav = {}
nav.back = 0 --initialized for nav.next calculation
if firstpage > 1 then
if firstpage - 30 < 1 then
nav.back = 1
else
nav.back = (firstpage - 1) / 30
end
formspec = formspec .."button[1,10;2,0.5;nav;page_"..nav.back.."]"
end
if #pages >= firstpage + 30 then
nav.next = nav.back + 2
formspec = formspec .."button[9,10;2,0.5;nav;page_"..nav.next.."]"
end
return formspec
end
-- on_receive_fields - called when a chest button is submitted
towntest_chest.on_receive_fields = function(pos, formname, fields, sender)
local meta = minetest.env:get_meta(pos)
if fields.building then
local we = towntest_chest.load(fields.building)
if we then
dprint("nodes loaded from file:", #we)
end
local filtered = towntest_chest.do_prepare_building(we)
if filtered then
dprint("nodes filtered:", #filtered)
end
local building_plan = skip_already_placed(filtered,pos)
if building_plan then
dprint("nodes in building plan:", #building_plan)
end
meta:set_string("full_plan", minetest.serialize(building_plan))
meta:set_string("formspec", towntest_chest.formspec(pos,"chest"))
towntest_chest.update_needed(meta:get_inventory(),building_plan)
towntest_chest.set_status(meta,1)
elseif fields.nav then
meta:set_string("formspec", towntest_chest.formspec(pos, fields.nav))
end
end
-- on_construct
towntest_chest.on_construct = function(pos)
-- setup chest meta and inventory
local meta = minetest.env:get_meta(pos)
meta:get_inventory():set_size("main", 8)
meta:get_inventory():set_size("needed", 8*2)
meta:get_inventory():set_size("builder", 2*2)
meta:get_inventory():set_size("lumberjack", 2*2)
meta:set_string("formspec", towntest_chest.formspec(pos, ""))
meta:set_string("building_plan", "") -- delete previous building plan on this node
meta:set_string("full_plan", "") -- delete previous building plan on this node
towntest_chest.set_status(meta, 0) --inactive till a building was selected
dprint("chest initialization done")
end
-- register_node - the chest where you put the items
minetest.register_node("towntest_chest:chest", {
description = "Building Chest",
tiles = {"default_chest_top.png", "default_chest_top.png", "default_chest_side.png",
"default_chest_side.png", "default_chest_side.png", "default_chest_front.png"},
paramtype2 = "facedir",
groups = {snappy=2,choppy=2,oddly_breakable_by_hand=2},
legacy_facedir_simple = true,
sounds = default.node_sound_wood_defaults(),
on_construct = towntest_chest.on_construct,
on_receive_fields = towntest_chest.on_receive_fields,
after_dig_node = function(pos, oldnode, oldmetadata, digger)
local k = pos.x..","..pos.y..","..pos.z
if towntest_chest.npc[k] then
towntest_chest.npc[k]:remove()
end
towntest_chest.npc[k] = nil
end,
on_punch = function(pos)
towntest_chest.set_status(minetest.env:get_meta(pos))
end,
on_metadata_inventory_put = function(pos)
towntest_chest.set_status(minetest.env:get_meta(pos),1)
end,
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
if from_list=="needed" or to_list=="needed" then return 0 end
if from_list=="builder" or to_list=="builder" then return 0 end
if from_list=="lumberjack" or to_list=="lumberjack" then return 0 end
return count
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
if listname=="needed" then return 0 end
if listname=="builder" then return 0 end
if listname=="lumberjack" then return 0 end
return stack:get_count()
end,
allow_metadata_inventory_take = function(pos, listname, index, stack, player)
if listname=="needed" then return 0 end
if listname=="builder" then return 0 end
if listname=="lumberjack" then return 0 end
return stack:get_count()
end,
})
-- register_abm - builds the building
minetest.register_abm({
nodenames = {"towntest_chest:chest"},
interval = 0.5,
chance = 1,
action = towntest_chest.build,
})
minetest.register_craft({
output = 'towntest_chest:chest',
recipe = {
{'default:mese_crystal', 'default:chest_locked', 'default:mese_crystal'},
{'default:book', 'default:diamond', 'default:book'},
{'default:mese_crystal', 'default:chest_locked', 'default:mese_crystal'},
}
})
-- log that we started
minetest.log("action", "[MOD]"..minetest.get_current_modname().." -- loaded from "..modpath)