Using original WorldEdit code to parse WorldEdit files (see towntest_chest/worldedit-serialization.lua)

code cleanup and bugfixes
master
Alexander Weber 2016-09-21 22:56:21 +02:00 committed by Elinvention
parent 21aeba2bf8
commit 4e39bddf66
2 changed files with 198 additions and 68 deletions

View File

@ -14,10 +14,8 @@ 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 = 600
local modpath = minetest.get_modpath(minetest.get_current_modname())
-- expose api
towntest_chest = {}
@ -25,11 +23,17 @@ 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
@ -48,7 +52,7 @@ end
-- load
-- filename - the building file to load
-- return - string containing the pos and nodes to build
-- 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")
@ -57,12 +61,14 @@ towntest_chest.load = function(filename)
return
end
-- load the building starting from the lowest y
local building_plan = towntest_chest.get_table(file:read("*a"))
table.sort(building_plan,function(a,b) return a.y<b.y end) -- sort by y to prefer lower nodes in building order
local building_plan = towntest_chest.we_load_schematic(file:read("*a"))
return building_plan
end
-- mapname Take filters and actions on nodes before building. Currently the payment item determination and check for registred node only
-- name - Node name to check and map
-- return - item name used as payment
local function mapname(name)
-- no name given - something wrong
if not name then
@ -102,6 +108,10 @@ local function mapname(name)
end
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)
@ -128,77 +138,56 @@ local function is_equal_meta(a,b)
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 then -- right node is at the place. there are no costs to touch them
if not def.meta then
building_plan[idx] = nil --no metadata handling needed. nothing to do
-- --same item without metadata. nothing to do
elseif is_equal_meta(minetest.get_meta(pos):to_table(), def.meta) then
building_plan[idx] = nil
-- --same metadata. Nothing to do
else
building_plan[idx].matname = "free"
def.matname = "free" --metadata correction for free
table.insert(building_out, def)
end
elseif mapname(node_placed.name) == mapname(def.name) then
building_plan[idx].matname = "free" --same price. Check/set for free
def.matname = "free" --same price. Check/set for free
table.insert(building_out, def)
else
table.insert(building_out, def) --rebuild for payment as usual
end
end
return building_plan
return building_out
end
-- get_table - convert building table to string
-- building_string - string containing pos and nodes to build
-- return - table containing pos and nodes to build
towntest_chest.get_table = function(building_string)
local building = {}
local wefile = {}
local idx, def
local retpos = string.find(string.sub(building_string,0,10), "return")
if retpos then
local exe, err, ok
exe,err = loadstring(string.sub(building_string,retpos))
if exe then
ok, wefile = pcall(exe)
end
for idx,def in pairs(wefile) 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
if not def.matname then
def.matname = mapname(def.name)
end
if def.matname then -- found
-- the node will be built
table.insert(building, {x=def.x,y=def.y,z=def.z,name=def.name,param1=def.param1,param2=def.param2,meta=def.meta,matname=def.matname})
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
if not def.matname then
def.matname = mapname(def.name)
end
end
else
for x, y, z, name, param1, param2 in building_string:gmatch("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)%s+([^%s]+)%s+(%d+)%s+(%d+)[^\r\n]*[\r\n]*") do
if tonumber(x)~=0 or tonumber(y)~=0 or tonumber(z)~=0 then
local matname = mapname(name)
if matname then
table.insert(building, {x=x,y=y,z=z,name=name,param1=param1,param2=param2,matname=matname})
end
if def.matname then -- found
-- the node will be built
table.insert(building_out, {x=def.x,y=def.y,z=def.z,name=def.name,param1=def.param1,param2=def.param2,meta=def.meta,matname=def.matname})
end
end
end
return building
return building_out
end
-- get_string - convert building string to table
-- building - table containing pos and nodes to build
-- return - string containing pos and nodes to build
towntest_chest.get_string = function(building)
local building_string = ""
if building_string then
building_string = "return "..dump(building)
end
return building_string
end
-- update_needed - updates the needed inventory in the chest
-- inv - inventory object of the chest
@ -245,8 +234,7 @@ 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
local building_plan = towntest_chest.get_table(meta:get_string("building_plan"))
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
@ -259,21 +247,23 @@ towntest_chest.build = function(chestpos)
inv:set_stack("builder", i, nil)
end
end
towntest_chest.update_needed(meta:get_inventory(),building_plan)
if building_plan then
towntest_chest.update_needed(meta:get_inventory(),building_plan)
end
end
local npc = towntest_chest.npc[k]
local npclua = npc:get_luaentity()
-- no building plan
if building_plan=="" then
if not building_plan then
-- move the npc to the chest
dprint("no building plan")
npclua:moveto({x=chestpos.x,y=chestpos.y+1.5,z=chestpos.z},2,2)
towntest_chest.set_status(meta,0)
return
end
-- search for next buildable node from builder inventory
local npcpos = npc:getpos()
if not npcpos then --fallback
@ -334,7 +324,7 @@ towntest_chest.build = function(chestpos)
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", towntest_chest.get_string(building_plan))
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
@ -352,14 +342,14 @@ towntest_chest.build = function(chestpos)
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 = towntest_chest.get_table(meta:get_string("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", towntest_chest.get_string(building_plan))
meta:set_string("building_plan", minetest.serialize(building_plan))
end, {pos=nextnode.pos, v=nextnode.v, inv=inv, meta=nextnode.meta})
else
if npclua.target then
@ -371,9 +361,16 @@ towntest_chest.build = function(chestpos)
else
dprint("<<--- get new items and re-sort building plan --->>>")
-- sort by y to prefer lower nodes in building order. At the same level prefer nodes nearly the chest
table.sort(building_plan,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
)
-- try to get items from chest into builder inventory
table.sort(building_plan,function(a,b) return a.y<b.y end) -- sort by y to prefer lowest nodes in building order
meta:set_string("building_plan", towntest_chest.get_string(building_plan)) --save the used order
meta:set_string("building_plan", minetest.serialize(building_plan)) --save the used order
local items_needed = true
for i,v in ipairs(building_plan) do
-- check if the chest has the node
@ -498,8 +495,17 @@ end
towntest_chest.on_receive_fields = function(pos, formname, fields, sender)
local meta = minetest.env:get_meta(pos)
if fields.building then
local building_plan = skip_already_placed(towntest_chest.load(fields.building),pos)
meta:set_string("building_plan", towntest_chest.get_string(building_plan))
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)
local building_plan = skip_already_placed(filtered,pos)
if building_plan then
dprint("nodes in building plan:", #building_plan)
end
meta:set_string("building_plan", minetest.serialize(building_plan))
meta:set_string("formspec", towntest_chest.formspec(pos,"chest"))
towntest_chest.set_status(meta,1)
towntest_chest.update_needed(meta:get_inventory(),building_plan)
@ -517,7 +523,9 @@ towntest_chest.on_construct = function(pos)
meta:get_inventory():set_size("builder", 2*2)
meta:get_inventory():set_size("lumberjack", 2*2)
meta:set_string("formspec", towntest_chest.formspec(pos, ""))
towntest_chest.set_status(meta, 1)
meta:set_string("building_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

View File

@ -0,0 +1,122 @@
--- Schematic serialization and deserialiation.
-- @module worldedit.serialization
--worldedit.LATEST_SERIALIZATION_VERSION = 5
--local LATEST_SERIALIZATION_HEADER = worldedit.LATEST_SERIALIZATION_VERSION .. ":"
--[[
Serialization version history:
1: Original format. Serialized Lua table with a weird linked format...
2: Position and node seperated into sub-tables in fields `1` and `2`.
3: List of nodes, one per line, with fields seperated by spaces.
Format: <X> <Y> <Z> <Name> <Param1> <Param2>
4: Serialized Lua table containing a list of nodes with `x`, `y`, `z`,
`name`, `param1`, `param2`, and `meta` fields.
5: Added header and made `param1`, `param2`, and `meta` fields optional.
Header format: <Version>,<ExtraHeaderField1>,...:<Content>
--]]
--- Reads the header of serialized data.
-- @param value Serialized WorldEdit data.
-- @return The version as a positive natural number, or 0 for unknown versions.
-- @return Extra header fields as a list of strings, or nil if not supported.
-- @return Content (data after header).
function towntest_chest.we_read_header(value)
if value:find("^[0-9]+[%-:]") then
local header_end = value:find(":", 1, true)
local header = value:sub(1, header_end - 1):split(",")
local version = tonumber(header[1])
table.remove(header, 1)
local content = value:sub(header_end + 1)
return version, header, content
end
-- Old versions that didn't include a header with a version number
if value:find("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)") and not value:find("%{") then -- List format
return 3, nil, value
elseif value:find("^[^\"']+%{%d+%}") then
if value:find("%[\"meta\"%]") then -- Meta flat table format
return 2, nil, value
end
return 1, nil, value -- Flat table format
elseif value:find("%{") then -- Raw nested table format
return 4, nil, value
end
return nil
end
--- Loads the schematic in `value` into a node list in the latest format.
-- Contains code based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile)
-- by ChillCode, available under the MIT license.
-- @return A node list in the latest format, or nil on failure.
function towntest_chest.we_load_schematic(value)
local version, header, content = towntest_chest.we_read_header(value)
local nodes = {}
if version == 1 or version == 2 then -- Original flat table format
local tables = minetest.deserialize(content)
if not tables then return nil end
-- Transform the node table into an array of nodes
for i = 1, #tables do
for j, v in pairs(tables[i]) do
if type(v) == "table" then
tables[i][j] = tables[v[1]]
end
end
end
nodes = tables[1]
if version == 1 then --original flat table format
for i, entry in ipairs(nodes) do
local pos = entry[1]
entry.x, entry.y, entry.z = pos.x, pos.y, pos.z
entry[1] = nil
local node = entry[2]
entry.name, entry.param1, entry.param2 = node.name, node.param1, node.param2
entry[2] = nil
end
end
elseif version == 3 then -- List format
for x, y, z, name, param1, param2 in content:gmatch(
"([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)%s+" ..
"([^%s]+)%s+(%d+)%s+(%d+)[^\r\n]*[\r\n]*") do
param1, param2 = tonumber(param1), tonumber(param2)
table.insert(nodes, {
x = tonumber(x),
y = tonumber(y),
z = tonumber(z),
name = name,
param1 = param1 ~= 0 and param1 or nil,
param2 = param2 ~= 0 and param2 or nil,
})
end
elseif version == 4 or version == 5 then -- Nested table format
if not jit then
-- This is broken for larger tables in the current version of LuaJIT
nodes = minetest.deserialize(content)
else
-- XXX: This is a filthy hack that works surprisingly well - in LuaJIT, `minetest.deserialize` will fail due to the register limit
nodes = {}
content = content:gsub("return%s*{", "", 1):gsub("}%s*$", "", 1) -- remove the starting and ending values to leave only the node data
local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end)
local startpos, startpos1, endpos = 1, 1
while true do -- go through each individual node entry (except the last)
startpos, endpos = escaped:find("},%s*{", startpos)
if not startpos then
break
end
local current = content:sub(startpos1, startpos)
local entry = minetest.deserialize("return " .. current)
table.insert(nodes, entry)
startpos, startpos1 = endpos, endpos
end
local entry = minetest.deserialize("return " .. content:sub(startpos1)) -- process the last entry
table.insert(nodes, entry)
end
else
return nil
end
return nodes
end