initial commit - a plan can be created, most not wirking or not tested
This commit is contained in:
commit
f7ca76fac1
16
init.lua
Normal file
16
init.lua
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
schemlib = {}
|
||||
local modpath = minetest.get_modpath(minetest.get_current_modname())
|
||||
schemlib.modpath = modpath
|
||||
|
||||
|
||||
schemlib.mapping = dofile(modpath.."/mapping.lua")
|
||||
schemlib.worldedit_file = dofile(modpath.."/worldedit_file.lua")
|
||||
schemlib.save_restore = dofile(modpath.."/save_restore.lua")
|
||||
schemlib.schematics = dofile(modpath.."/schematics.lua")
|
||||
schemlib.world = dofile(modpath.."/world.lua")
|
||||
schemlib.plan = dofile(modpath.."/plan.lua")
|
||||
|
||||
-- log that we started
|
||||
minetest.log("action", "[MOD]"..minetest.get_current_modname().." -- loaded from "..modpath)
|
250
mapping.lua
Normal file
250
mapping.lua
Normal file
@ -0,0 +1,250 @@
|
||||
-- debug-print
|
||||
local dprint = print
|
||||
--local dprint = function dummy()
|
||||
|
||||
local mapping = {}
|
||||
|
||||
-- visual for cost_item free for payment
|
||||
mapping.c_free_item = "default:cloud"
|
||||
|
||||
-----------------------------------------------
|
||||
-- door compatibility. Seems the old doors was facedir and now the wallmounted values should be used
|
||||
-----------------------------------------------
|
||||
local function __param2_wallmounted_to_facedir(nodeinfo, pos, wpos)
|
||||
if nodeinfo.param2 == 0 then -- +y?
|
||||
nodeinfo.param2 = 0
|
||||
elseif nodeinfo.param2 == 1 then -- -y?
|
||||
nodeinfo.param2 = 1
|
||||
elseif nodeinfo.param2 == 2 then --unsure
|
||||
nodeinfo.param2 = 3
|
||||
elseif nodeinfo.param2 == 3 then --unsure
|
||||
nodeinfo.param2 = 1
|
||||
elseif nodeinfo.param2 == 4 then --unsure
|
||||
nodeinfo.param2 = 2
|
||||
elseif nodeinfo.param2 == 5 then --unsure
|
||||
nodeinfo.param2 = 0
|
||||
end
|
||||
end
|
||||
|
||||
local u = {}
|
||||
local unknown_nodes_data = u
|
||||
-- Fallback nodes replacement of unknown nodes
|
||||
-- Maybe it is beter to use aliases for unknown notes. But anyway
|
||||
u["xpanes:pane_glass_10"] = { name = "xpanes:pane_10" }
|
||||
u["xpanes:pane_glass_5"] = { name = "xpanes:pane_5" }
|
||||
u["beds:bed_top_blue"] = { name = "beds:bed_top" }
|
||||
u["beds:bed_bottom_blue"] = { name = "beds:bed_bottom" }
|
||||
|
||||
u["homedecor:table_lamp_max"] = { name = "homedecor:table_lamp_white_max" }
|
||||
u["homedecor:refrigerator"] = { name = "homedecor:refrigerator_steel" }
|
||||
|
||||
u["ethereal:green_dirt"] = { name = "default:dirt_with_grass" }
|
||||
|
||||
u["doors:door_wood_b_c"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "0"}}}, custom_function = __param2_wallmounted_to_facedir } --closed
|
||||
u["doors:door_wood_b_o"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "1"}}}, custom_function = __param2_wallmounted_to_facedir } --open
|
||||
u["doors:door_wood_b_1"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "0"}}}} --closed
|
||||
u["doors:door_wood_b_2"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "3"}}}} --closed / reversed ??
|
||||
u["doors:door_wood_a_c"] = {name = "doors:hidden" }
|
||||
u["doors:door_wood_a_o"] = {name = "doors:hidden" }
|
||||
u["doors:door_wood_t_1"] = {name = "doors:hidden" }
|
||||
u["doors:door_wood_t_2"] = {name = "doors:hidden" }
|
||||
|
||||
u["doors:door_glass_b_c"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "0"}}}, custom_function = __param2_wallmounted_to_facedir } --closed
|
||||
u["doors:door_glass_b_o"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "1"}}}, custom_function = __param2_wallmounted_to_facedir } --open
|
||||
u["doors:door_glass_b_1"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "0"}}}} --closed
|
||||
u["doors:door_glass_b_2"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "3"}}}} --closed / reversed ??
|
||||
u["doors:door_glass_a_c"] = {name = "doors:hidden" }
|
||||
u["doors:door_glass_a_o"] = {name = "doors:hidden" }
|
||||
u["doors:door_glass_t_1"] = {name = "doors:hidden" }
|
||||
u["doors:door_glass_t_2"] = {name = "doors:hidden" }
|
||||
|
||||
u["doors:door_steel_b_c"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "0"}}}, custom_function = __param2_wallmounted_to_facedir } --closed
|
||||
u["doors:door_steel_b_o"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "1"}}}, custom_function = __param2_wallmounted_to_facedir } --open
|
||||
u["doors:door_steel_b_1"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "0"}}}} --closed
|
||||
u["doors:door_steel_b_2"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "3"}}}} --closed / reversed ??
|
||||
u["doors:door_steel_a_c"] = {name = "doors:hidden" }
|
||||
u["doors:door_steel_a_o"] = {name = "doors:hidden" }
|
||||
u["doors:door_steel_t_1"] = {name = "doors:hidden" }
|
||||
u["doors:door_steel_t_2"] = {name = "doors:hidden" }
|
||||
|
||||
|
||||
local c = {}
|
||||
local default_replacements = c
|
||||
-- "name" and "cost_item" are optional.
|
||||
-- if name is missed it will not be changed
|
||||
-- if cost_item is missed it will be determinated as usual (from changed name)
|
||||
-- a crazy sample is: instead of cobble place goldblock, use wood as payment
|
||||
-- c["default:cobble"] = { name = "default:goldblock", cost_item = "default:wood" }
|
||||
|
||||
c["beds:bed_top"] = { cost_item = mapping.c_free_item } -- the bottom of the bed is payed, so buld the top for free
|
||||
|
||||
-- it is hard to get a source in survival, so we use buckets. Note, the bucket is lost after usage by NPC
|
||||
c["default:lava_source"] = { cost_item = "bucket:bucket_lava" }
|
||||
c["default:river_water_source"] = { cost_item = "bucket:bucket_river_water" }
|
||||
c["default:water_source"] = { cost_item = "bucket:bucket_water" }
|
||||
|
||||
-- does not sense to set flowing water because it flow away without the source (and will be generated trough source)
|
||||
c["default:water_flowing"] = { name = "" }
|
||||
c["default:lava_flowing"] = { name = "" }
|
||||
c["default:river_water_flowing"] = { name = "" }
|
||||
|
||||
-- pay different dirt types by the sane dirt
|
||||
c["default:dirt_with_dry_grass"] = { cost_item = "default:dirt" }
|
||||
c["default:dirt_with_grass"] = { cost_item = "default:dirt" }
|
||||
c["default:dirt_with_snow"] = { cost_item = "default:dirt" }
|
||||
|
||||
c["xpanes:pane_5"] = { name = "xpanes:pane_flat", param2 = 0 } --unsure
|
||||
c["xpanes:pane_10"] = { name = "xpanes:pane_flat", param2 = 1 } --unsure
|
||||
|
||||
-----------------------------------------------
|
||||
-- copy table of mapping entry
|
||||
-----------------------------------------------
|
||||
function mapping.merge_map_entry(entry1, entry2)
|
||||
if entry2 then
|
||||
return {name = entry1.name or entry2.name,
|
||||
node_def = entry1.node_def or entry2.node_def,
|
||||
content_id = entry1.content_id or entry2.content_id,
|
||||
param2 = entry1.param2 or entry2.param2,
|
||||
meta = entry1.meta or entry2.meta,
|
||||
custom_function = entry1.custom_function or entry2.custom_function,
|
||||
cost_item = entry1.cost_item or entry2.cost_item,
|
||||
}
|
||||
else
|
||||
return {name = entry1.name,
|
||||
content_id = entry1.content_id,
|
||||
node_def = entry1.node_def,
|
||||
param2 = entry1.param2,
|
||||
meta = entry1.meta,
|
||||
custom_function = entry1.custom_function,
|
||||
cost_item = entry1.cost_item}
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------
|
||||
-- is_equal_meta - compare meta information of 2 nodes
|
||||
-- name - Node name to check and map
|
||||
-- return - item name used as payment
|
||||
-----------------------------------------------
|
||||
function mapping.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 mapping.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
|
||||
|
||||
-----------------------------------------------
|
||||
-- Fallback nodes replacement of unknown nodes
|
||||
-----------------------------------------------
|
||||
function mapping.map_unknown(name)
|
||||
local map = unknown_nodes_data[name]
|
||||
if not map or map.name == name then -- no fallback mapping. don't use the node
|
||||
dprint("mapping failed:", name, dump(map))
|
||||
print("unknown nodes in building", name)
|
||||
return nil
|
||||
end
|
||||
|
||||
dprint("mapped", name, "to", map.name)
|
||||
return mapping.merge_map_entry(map)
|
||||
end
|
||||
|
||||
-----------------------------------------------
|
||||
-- Take filters and actions on nodes before building
|
||||
-----------------------------------------------
|
||||
function mapping.map_name(name)
|
||||
-- get mapped registred node name for further mappings
|
||||
local node_chk = minetest.registered_nodes[name]
|
||||
|
||||
--do fallback mapping if not registred node
|
||||
if not node_chk then
|
||||
local fallback = mapping.map_unknown(name)
|
||||
if fallback then
|
||||
dprint("map fallback:", dump(fallback))
|
||||
local fbmapped = mapping.map_name(fallback.name)
|
||||
if fbmapped then
|
||||
return mapping.merge_map_entry(fbmapped, fallback) --merge fallback values into the mapped node
|
||||
end
|
||||
end
|
||||
dprint("unmapped node", name)
|
||||
return
|
||||
end
|
||||
|
||||
-- get default replacement
|
||||
local map = default_replacements[name]
|
||||
local mr -- mapped return table
|
||||
if not map then
|
||||
mr = {}
|
||||
mr.name = name
|
||||
mr.node_def = node_chk
|
||||
else
|
||||
mr = mapping.merge_map_entry(map)
|
||||
if mr.name == nil then
|
||||
mr.name = name
|
||||
end
|
||||
end
|
||||
|
||||
--disabled by mapping
|
||||
if mr.name == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
mr.node_def = minetest.registered_nodes[mr.name]
|
||||
|
||||
-- determine cost_item
|
||||
if not mr.cost_item then
|
||||
|
||||
--Check for price or if it is free
|
||||
local recipe = minetest.get_craft_recipe(mr.name)
|
||||
if (mr.node_def.groups.not_in_creative_inventory and --not in creative
|
||||
not (mr.node_def.groups.not_in_creative_inventory == 0) and
|
||||
(not recipe or not recipe.items)) --and not craftable
|
||||
|
||||
or (not mr.node_def.description or mr.node_def.description == "") then -- no description
|
||||
if mr.node_def.drop and mr.node_def.drop ~= "" then
|
||||
-- use possible drop as payment
|
||||
if type(mr.node_def.drop) == "table" then -- drop table
|
||||
mr.cost_item = mr.node_def.drop[1] -- use the first one
|
||||
else
|
||||
mr.cost_item = mr.node_def.drop
|
||||
end
|
||||
else --something not supported, but known
|
||||
mr.cost_item = mapping.c_free_item -- 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
|
||||
mr.cost_item = mr.name
|
||||
end
|
||||
end
|
||||
|
||||
mr.content_id = minetest.get_content_id(mr.name)
|
||||
dprint("map", name, "to", mr.name, mr.param2, mr.cost_item)
|
||||
return mr
|
||||
end
|
||||
|
||||
-----------------------------------------------
|
||||
-- create a "mappednodes" using the data from analyze_* files
|
||||
-----------------------------------------------
|
||||
function mapping.do_mapping(data)
|
||||
data.mappednodes = {}
|
||||
for node_id, name in ipairs(data.nodenames) do
|
||||
data.mappednodes[node_id] = mapping.map_name(name)
|
||||
end
|
||||
end
|
||||
|
||||
return mapping
|
454
plan.lua
Normal file
454
plan.lua
Normal file
@ -0,0 +1,454 @@
|
||||
--[[ API
|
||||
|
||||
data types (and usual names):
|
||||
- plan_pos - a relative position vector used in plan. Note: the relative 0,0,0 in plan will be the placed to the defined world_pos position
|
||||
- world_pos - a absolute ("real") position vector in the world
|
||||
|
||||
- plan_node - a abstract node description used in plan till build
|
||||
- buildable_node - a enhanced plan node ready for build
|
||||
|
||||
- status
|
||||
"new" - created
|
||||
"ready" - ready for processing
|
||||
??? - custom status possible
|
||||
|
||||
- plan_type - a type of plan to handle different types
|
||||
- house
|
||||
- lumber
|
||||
|
||||
public class-methods
|
||||
- new(plan_id [,anchor_pos]) - Constructor - create a new plan object with unique plan_id (WIP)
|
||||
- get(plan_id) - get a existing plan object with unique plan_id
|
||||
- get_all() - get a plan list
|
||||
- save_all() - write plan definitions to files in world
|
||||
- load_all() - read all plan definitions from files in world
|
||||
- remove(plan_id) - remove a plan from world
|
||||
|
||||
public object methods
|
||||
- add_node(plan_pos, plan_node) - add a node to plan (WIP) - "nodenames" handling is missed
|
||||
- get_node(plan_pos) - get a node from plan (OK)
|
||||
- del_node(plan_pos) - delete a node from plan (OK)
|
||||
- get_node_next_to_pos(plan_pos) - get the nearest node to pos (low-prio)
|
||||
- get_node_random() - get a random existing plan_pos from plan (OK)
|
||||
- get_chunk_nodes(plan_pos) - get a list of all nodes from chunk of a pos (OK)
|
||||
|
||||
- read_from_schem_file(file) - read from WorldEdit or mts file (OK)
|
||||
- get_status() - get status (low-prio)
|
||||
- set_status(status) - set status (low-prio)
|
||||
- set_type(plan_type) - set type (low-prio)
|
||||
- get_type(plan_type) - get type (low-prio)
|
||||
- set_anchor_pos(world_pos) - set the world position the plan should be applied anchor at 0,0,0 in plan (low-prio)
|
||||
- get_anchor_pos() - get the world position the plan should be applied anchor at 0,0,0 in plan (low-prio)
|
||||
|
||||
- get_world_pos(plan_pos) - get a world position for a plan position (OK)
|
||||
- get_plan_pos(world_pos) - get a plan position for a world position (OK)
|
||||
- get_buildable_node(plan_pos) - get a plan node ready to build (OK)
|
||||
- load_plan() - load a plan state from file in world-directory (low-prio) (:scm_data_cache)
|
||||
- save_plan() - store the current plan to a file in world directory and set them valid (low-prio) (:scm_data_cache)
|
||||
- apply_flood_with_air
|
||||
(add_max, add_min, add_top) - Fill a building with air (OK)
|
||||
- do_add_node(buildable_node) - Place node to world using "add_node" and remove them from plan (OK)
|
||||
- do_add_chunk(plan_pos) - Place a node (OK)
|
||||
- do_add_chunk_voxel(plan_pos) - Place a node (OK)
|
||||
|
||||
Internals
|
||||
private class attributes
|
||||
- plan_list - a simple list with all known plans
|
||||
|
||||
private object atributes
|
||||
-- allways loaded in list
|
||||
- plan_id - a id of the plan (=filename)
|
||||
- status - plan status
|
||||
- anchor_pos - position vector in world
|
||||
- data.nodenames - a list of node names for node_id
|
||||
- data.ground_y - explicit ground adjustment for anchor_pos
|
||||
- data.min_pos - minimal {x,y,z} vector
|
||||
- data.max_pos - maximal {x,y,z} vector
|
||||
|
||||
-- save the cache data using save_cache()
|
||||
- data.nodecount - count of nodes in scm_data_cache
|
||||
- data.scm_data_cache - all plan nodes
|
||||
|
||||
-- will be rebuild on demand
|
||||
- data.prepared_cache - cache of prepared buildable nodes
|
||||
]]
|
||||
|
||||
-- debug-print
|
||||
local dprint = print
|
||||
--local dprint = function dummy()
|
||||
|
||||
|
||||
local mapping = schemlib.mapping
|
||||
local save_restore = schemlib.save_restore
|
||||
local modpath = schemlib.modpath
|
||||
local schematics = schemlib.schematics
|
||||
|
||||
local plan = {}
|
||||
plan.plan_list = {}
|
||||
|
||||
function plan.get(plan_id)
|
||||
if plan.plan_list ~= nil then
|
||||
return plan.plan_list[plan_id]
|
||||
end
|
||||
end
|
||||
|
||||
function plan.get_all()
|
||||
--TODO: list files + merge with plan_list
|
||||
-- Output table entries:
|
||||
-- entry[plan_id] = { plan_id=, status=, anchor_pos=, ground_y=, min_pos=, max_pos=, node_count= }
|
||||
return plan.plan_list
|
||||
end
|
||||
|
||||
|
||||
plan.new = function( plan_id , anchor_pos)
|
||||
local self = {}
|
||||
self.plan_id = plan_id
|
||||
self.anchor_pos = anchor_pos
|
||||
self.data = {}
|
||||
self.status = "new"
|
||||
-- self.plan_type = nil
|
||||
|
||||
if self.plan_id ~= nil then
|
||||
plan.plan_list[self.plan_id] = self
|
||||
end
|
||||
|
||||
function self.add_node(self, node)
|
||||
-- insert new
|
||||
if self.data.scm_data_cache[node.y] == nil then
|
||||
self.data.scm_data_cache[node.y] = {}
|
||||
end
|
||||
if self.data.scm_data_cache[node.y][node.x] == nil then
|
||||
self.data.scm_data_cache[node.y][node.x] = {}
|
||||
end
|
||||
self.data.nodecount = self.data.nodecount + 1
|
||||
self.data.scm_data_cache[node.y][node.x][node.z] = node
|
||||
end
|
||||
|
||||
|
||||
function self.get_node(self, plan_pos)
|
||||
local pos = plan_pos
|
||||
assert(pos.x, "pos without xyz")
|
||||
if self.data.scm_data_cache[pos.y] == nil then
|
||||
return nil
|
||||
end
|
||||
if self.data.scm_data_cache[pos.y][pos.x] == nil then
|
||||
return nil
|
||||
end
|
||||
if self.data.scm_data_cache[pos.y][pos.x][pos.z] == nil then
|
||||
return nil
|
||||
end
|
||||
return self.data.scm_data_cache[pos.y][pos.x][pos.z]
|
||||
end
|
||||
|
||||
function self.del_node(self, pos)
|
||||
if self.data.scm_data_cache[pos.y] ~= nil then
|
||||
if self.data.scm_data_cache[pos.y][pos.x] ~= nil then
|
||||
if self.data.scm_data_cache[pos.y][pos.x][pos.z] ~= nil then
|
||||
self.data.nodecount = self.data.nodecount - 1
|
||||
self.data.scm_data_cache[pos.y][pos.x][pos.z] = nil
|
||||
end
|
||||
if next(self.data.scm_data_cache[pos.y][pos.x]) == nil then
|
||||
self.data.scm_data_cache[pos.y][pos.x] = nil
|
||||
end
|
||||
end
|
||||
if next(self.data.scm_data_cache[pos.y]) == nil then
|
||||
self.data.scm_data_cache[pos.y] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if self.data.prepared_cache and self.data.prepared_cache[pos.y] ~= nil then
|
||||
if self.data.prepared_cache[pos.y][pos.x]then
|
||||
if self.data.prepared_cache[pos.y][pos.x][pos.z] ~= nil then
|
||||
self.data.prepared_cache[pos.y][pos.x][pos.z] = nil
|
||||
end
|
||||
if next(self.data.prepared_cache[pos.y][pos.x]) == nil then
|
||||
self.data.prepared_cache[pos.y][pos.x] = nil
|
||||
end
|
||||
end
|
||||
if next(self.data.prepared_cache[pos.y]) == nil then
|
||||
self.data.prepared_cache[pos.y] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function self.apply_flood_with_air(add_max, add_min, add_top)
|
||||
self.data.ground_y = math.floor(self.data.ground_y)
|
||||
if add_max == nil then
|
||||
add_max = 3
|
||||
end
|
||||
if add_max == nil then
|
||||
add_max = 0
|
||||
end
|
||||
if add_top == nil then
|
||||
add_top = 5
|
||||
end
|
||||
|
||||
-- define nodename-ID for air
|
||||
local air_id = #self.data.nodenames + 1
|
||||
self.data.nodenames[ air_id ] = "air"
|
||||
|
||||
dprint("create flatting plan")
|
||||
for y = self.data.min_pos.y, self.data.max_pos.y + add_top do -- with additional 5 on top
|
||||
--calculate additional grounding
|
||||
if y > self.data.ground_y then --only over ground
|
||||
local high = y-self.data.ground_y
|
||||
add_min = high + 1
|
||||
if add_min > add_max then --set to max
|
||||
add_min = add_max
|
||||
end
|
||||
end
|
||||
|
||||
dprint("flat level:", y)
|
||||
|
||||
for x = self.data.min_pos.x - add_min, self.data.max_pos.x + add_min do
|
||||
for z = self.data.min_pos.z - add_min, self.data.max_pos.z + add_min do
|
||||
local airnode = {x=x, y=y, z=z, name_id=air_id}
|
||||
if self:get_scm_node(airnode) == nil then
|
||||
self:add_node(airnode)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
dprint("flatting plan done")
|
||||
end
|
||||
|
||||
function self.get_world_pos(self,pos)
|
||||
return { x=pos.x+self.chest.pos.x,
|
||||
y=pos.y+self.chest.pos.y - self.data.ground_y - 1,
|
||||
z=pos.z+self.chest.pos.z
|
||||
}
|
||||
end
|
||||
|
||||
-- revert get_world_pos
|
||||
function self.get_plan_pos(self,pos)
|
||||
return { x=pos.x-self.chest.pos.x,
|
||||
y=pos.y-self.chest.pos.y + self.data.ground_y + 1,
|
||||
z=pos.z-self.chest.pos.z
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
-- get nodes for selection which one should be build
|
||||
-- skip parameter is randomized
|
||||
function self.get_node_random(self)
|
||||
dprint("get something from list")
|
||||
|
||||
-- get random existing y
|
||||
local keyset = {}
|
||||
for k in pairs(self.data.scm_data_cache) do table.insert(keyset, k) end
|
||||
if #keyset == 0 then --finished
|
||||
return nil
|
||||
end
|
||||
local y = keyset[math.random(#keyset)]
|
||||
|
||||
-- get random existing x
|
||||
keyset = {}
|
||||
for k in pairs(self.data.scm_data_cache[y]) do table.insert(keyset, k) end
|
||||
local x = keyset[math.random(#keyset)]
|
||||
|
||||
-- get random existing z
|
||||
keyset = {}
|
||||
for k in pairs(self.data.scm_data_cache[y][x]) do table.insert(keyset, k) end
|
||||
local z = keyset[math.random(#keyset)]
|
||||
|
||||
if z ~= nil then
|
||||
return {x=x,y=y,z=z}
|
||||
end
|
||||
end
|
||||
|
||||
-- to be able working with forceload chunks
|
||||
function self.get_chunk_nodes(self, node)
|
||||
-- calculate the begin of the chunk
|
||||
--local BLOCKSIZE = core.MAP_BLOCKSIZE
|
||||
local BLOCKSIZE = 16
|
||||
local wpos = self:get_world_pos(node)
|
||||
local minp = {}
|
||||
minp.x = (math.floor(wpos.x/BLOCKSIZE))*BLOCKSIZE
|
||||
minp.y = (math.floor(wpos.y/BLOCKSIZE))*BLOCKSIZE
|
||||
minp.z = (math.floor(wpos.z/BLOCKSIZE))*BLOCKSIZE
|
||||
local maxp = vector.add(minp, 16)
|
||||
|
||||
dprint("nodes for chunk (real-pos)", minetest.pos_to_string(minp), minetest.pos_to_string(maxp))
|
||||
|
||||
local minv = self:get_plan_pos(minp)
|
||||
local maxv = self:get_plan_pos(maxp)
|
||||
dprint("nodes for chunk (plan-pos)", minetest.pos_to_string(minv), minetest.pos_to_string(maxv))
|
||||
|
||||
local ret = {}
|
||||
for y = minv.y, maxv.y do
|
||||
if self.data.scm_data_cache[y] ~= nil then
|
||||
for x = minv.x, maxv.x do
|
||||
if self.data.scm_data_cache[y][x] ~= nil then
|
||||
for z = minv.z, maxv.z do
|
||||
if self.data.scm_data_cache[y][x][z] ~= nil then
|
||||
local pos = {x=x,y=y,z=z}
|
||||
local wpos = self:get_world_pos(pos)
|
||||
table.insert(ret, {pos = pos, wpos = wpos, node=self:get_buildable_node(pos, wpos)})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
dprint("nodes in chunk to build", #ret)
|
||||
return ret
|
||||
end
|
||||
|
||||
|
||||
function self.read_from_schem_file(self, filename)
|
||||
local file = save_restore.file_access(filename, "r")
|
||||
if file == nil then
|
||||
dprint("error: could not open file \"" .. filename .. "\"")
|
||||
self.data = nil
|
||||
else
|
||||
-- different file types
|
||||
if string.find( filename, '.mts', -4 ) then
|
||||
self.data = schematics.analyze_mts_file(file)
|
||||
end
|
||||
if string.find( filename, '.we', -3 ) or string.find( filename, '.wem', -4 ) then
|
||||
self.data = schematics.analyze_we_file(file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- prepare node for build
|
||||
function self.get_buildable_node(self, pos, wpos)
|
||||
-- first run, generate mapping data
|
||||
if self.data.mappednodes == nil then
|
||||
mapping.do_mapping(self.data)
|
||||
end
|
||||
|
||||
-- get from cache
|
||||
if self.data.prepared_cache ~= nil and
|
||||
self.data.prepared_cache[pos.y] ~= nil and
|
||||
self.data.prepared_cache[pos.y][pos.x] ~= nil and
|
||||
self.data.prepared_cache[pos.y][pos.x][pos.z] ~= nil then
|
||||
return self.data.prepared_cache[pos.y][pos.x][pos.z]
|
||||
end
|
||||
|
||||
-- get scm data
|
||||
local scm_node = self:get_node(pos)
|
||||
if scm_node == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
--get mapping data
|
||||
local map = self.data.mappednodes[scm_node.name_id]
|
||||
if map == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local node = mapping.merge_map_entry(map, scm_node)
|
||||
|
||||
if node.custom_function ~= nil then
|
||||
node.custom_function(node, pos, wpos)
|
||||
end
|
||||
|
||||
-- maybe node name is changed in custom function. Update the content_id in this case
|
||||
node.content_id = minetest.get_content_id(node.name)
|
||||
node.node_def = minetest.registered_nodes[node.name]
|
||||
|
||||
-- store the mapped node info in cache
|
||||
if self.data.prepared_cache == nil then
|
||||
self.data.prepared_cache = {}
|
||||
end
|
||||
if self.data.prepared_cache[pos.y] == nil then
|
||||
self.data.prepared_cache[pos.y] = {}
|
||||
end
|
||||
if self.data.prepared_cache[pos.y][pos.x] == nil then
|
||||
self.data.prepared_cache[pos.y][pos.x] = {}
|
||||
end
|
||||
self.data.prepared_cache[pos.y][pos.x][pos.z] = node
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
function self.do_add_node(self, buildable_node)
|
||||
if buildable_node.node then
|
||||
minetest.env:add_node(buildable_node.wpos, buildable_node.node)
|
||||
if buildable_node.node.meta then
|
||||
minetest.env:get_meta(buildable_node.wpos):from_table(buildable_node.node.meta)
|
||||
end
|
||||
end
|
||||
self:del_node(buildable_node.pos)
|
||||
end
|
||||
|
||||
function self.do_add_chunk(self, plan_pos)
|
||||
local chunk_pos = self.plan:get_world_pos(plan_pos)
|
||||
dprint("---build chunk", minetest.pos_to_string(plan_pos))
|
||||
|
||||
local chunk_nodes = self:get_chunk_nodes(self:get_plan_pos(chunk_pos))
|
||||
dprint("Instant build of chunk: nodes:", #chunk_nodes)
|
||||
for idx, nodeplan in ipairs(chunk_nodes) do
|
||||
--TODO: call "add_node"
|
||||
self:do_add_node(nodeplan)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function self.do_add_chunk_voxel(self, plan_pos)
|
||||
local chunk_pos = self.plan:get_world_pos(plan_pos)
|
||||
dprint("---build chunk uning voxel", minetest.pos_to_string(plan_pos))
|
||||
|
||||
-- work on VoxelArea
|
||||
local vm = minetest.get_voxel_manip()
|
||||
local minp, maxp = vm:read_from_map(chunk_pos, chunk_pos)
|
||||
local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
|
||||
local data = vm:get_data()
|
||||
local param2_data = vm:get_param2_data()
|
||||
local light_fix = {}
|
||||
local meta_fix = {}
|
||||
-- for idx in a:iterp(vector.add(minp, 8), vector.subtract(maxp, 8)) do -- do not touch for beter light update
|
||||
for idx, origdata in pairs(data) do -- do not touch for beter light update
|
||||
local wpos = a:position(idx)
|
||||
local pos = self:get_plan_pos(wpos)
|
||||
local node = self:get_buildable_node(pos, wpos)
|
||||
if node and node.content_id then
|
||||
-- write to voxel
|
||||
data[idx] = node.content_id
|
||||
param2_data[idx] = node.param2
|
||||
|
||||
-- mark for light update
|
||||
assert(node.node_def, dump(node))
|
||||
if node.node_def.light_source and node.node_def.light_source > 0 then
|
||||
table.insert(light_fix, {pos = wpos, node = node})
|
||||
end
|
||||
if node.meta then
|
||||
table.insert(meta_fix, {pos = wpos, node = node})
|
||||
end
|
||||
self.plan:remove_node(node)
|
||||
end
|
||||
self.plan:remove_node(pos) --if exists
|
||||
end
|
||||
|
||||
-- store the changed map data
|
||||
vm:set_data(data)
|
||||
vm:set_param2_data(param2_data)
|
||||
vm:calc_lighting()
|
||||
vm:update_liquids()
|
||||
vm:write_to_map()
|
||||
vm:update_map()
|
||||
|
||||
-- fix the lights
|
||||
dprint("fix lights", #light_fix)
|
||||
for _, fix in ipairs(light_fix) do
|
||||
minetest.env:add_node(fix.pos, fix.node)
|
||||
end
|
||||
|
||||
dprint("process meta", #meta_fix)
|
||||
for _, fix in ipairs(meta_fix) do
|
||||
minetest.env:get_meta(fix.pos):from_table(fix.node.meta)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------
|
||||
|
||||
--------------------
|
||||
-- TODO: save the reference to a global accessable table
|
||||
--------------------
|
||||
return self -- the plan object
|
||||
end
|
||||
|
||||
return plan
|
89
save_restore.lua
Normal file
89
save_restore.lua
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
-- reserve the namespace
|
||||
local save_restore = {}
|
||||
|
||||
-- TODO: if this gets more versatile, add sanity checks for filename
|
||||
-- TODO: apart from allowing filenames, schems/<filename> also needs to be allowed
|
||||
|
||||
-- TODO: save and restore ought to be library functions and not implemented in each individual mod!
|
||||
save_restore.save_data = function( filename, data )
|
||||
|
||||
local path = minetest.get_worldpath()..'/'..filename;
|
||||
|
||||
local file = io.open( path, 'w' );
|
||||
if( file ) then
|
||||
file:write( minetest.serialize( data ));
|
||||
file:close();
|
||||
else
|
||||
print("[save_restore] Error: Savefile '"..tostring( path ).."' could not be written.");
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
save_restore.restore_data = function( filename )
|
||||
local file = io.open( filename, 'r' );
|
||||
if( file ) then
|
||||
local data = file:read("*all");
|
||||
file:close();
|
||||
return minetest.deserialize( data );
|
||||
else
|
||||
print("[save_restore] Error: Savefile '"..tostring( filename ).."' not found.");
|
||||
return {}; -- return empty table
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
save_restore.file_exists = function( filename )
|
||||
|
||||
local file = save_restore.file_access( filename, 'r' );
|
||||
if( file ) then
|
||||
file:close();
|
||||
return true;
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
save_restore.create_schems_directory = function()
|
||||
local directory = minetest.get_worldpath()..'/schems';
|
||||
if( not( save_restore.file_exists( directory ))) then
|
||||
if( minetest.mkdir ) then
|
||||
minetest.mkdir( directory );
|
||||
else
|
||||
os.execute("mkdir \""..directory.. "\"");
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- we only need the function io.open in a version that can read schematic files from diffrent places,
|
||||
-- even if a secure environment is enforced; this does require an exception for the mod
|
||||
local ie_io_open = io.open;
|
||||
if( minetest.request_insecure_environment ) then
|
||||
local ie, req_ie = _G, minetest.request_insecure_environment
|
||||
if req_ie then ie = req_ie() end
|
||||
if ie then
|
||||
ie_io_open = ie.io.open;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- only a certain type of files can be read and written
|
||||
save_restore.file_access = function( path, params )
|
||||
if( (params=='r' or params=='rb')
|
||||
and ( string.find( path, '.mts', -4 )
|
||||
or string.find( path, '.schematic', -11 )
|
||||
or string.find( path, '.we', -3 )
|
||||
or string.find( path, '.wem', -4 ) )) then
|
||||
return ie_io_open( path, params );
|
||||
elseif( (params=='w' or params=='wb')
|
||||
and ( string.find( path, '.mts', -4 )
|
||||
or string.find( path, '.schematic', -11 )
|
||||
or string.find( path, '.we', -3 )
|
||||
or string.find( path, '.wem', -4 ) )) then
|
||||
return ie_io_open( path, params );
|
||||
end
|
||||
end
|
||||
|
||||
return save_restore
|
271
schematics.lua
Normal file
271
schematics.lua
Normal file
@ -0,0 +1,271 @@
|
||||
local schematics = {}
|
||||
|
||||
schematics.analyze_mts_file = function(file)
|
||||
--[[ taken from src/mg_schematic.cpp:
|
||||
Minetest Schematic File Format
|
||||
|
||||
All values are stored in big-endian byte order.
|
||||
[u32] signature: 'MTSM'
|
||||
[u16] version: 3
|
||||
[u16] size X
|
||||
[u16] size Y
|
||||
[u16] size Z
|
||||
For each Y:
|
||||
[u8] slice probability value
|
||||
[Name-ID table] Name ID Mapping Table
|
||||
[u16] name-id count
|
||||
For each name-id mapping:
|
||||
[u16] name length
|
||||
[u8[] ] name
|
||||
ZLib deflated {
|
||||
For each node in schematic: (for z, y, x)
|
||||
[u16] content
|
||||
For each node in schematic:
|
||||
[u8] probability of occurance (param1)
|
||||
For each node in schematic:
|
||||
[u8] param2
|
||||
}
|
||||
|
||||
Version changes:
|
||||
1 - Initial version
|
||||
2 - Fixed messy never/always place; 0 probability is now never, 0xFF is always
|
||||
3 - Added y-slice probabilities; this allows for variable height structures
|
||||
--]]
|
||||
|
||||
-- taken from https://github.com/MirceaKitsune/minetest_mods_structures/blob/master/structures_io.lua (Taokis Sructures I/O mod)
|
||||
-- gets the size of a structure file
|
||||
-- nodenames: contains all the node names that are used in the schematic
|
||||
|
||||
local size = { x = 0, y = 0, z = 0, version = 0 }
|
||||
local version = 0;
|
||||
|
||||
-- thanks to sfan5 for this advanced code that reads the size from schematic files
|
||||
local read_s16 = function(fi)
|
||||
return string.byte(fi:read(1)) * 256 + string.byte(fi:read(1))
|
||||
end
|
||||
|
||||
local function get_schematic_size(f)
|
||||
-- make sure those are the first 4 characters, otherwise this might be a corrupt file
|
||||
if f:read(4) ~= "MTSM" then
|
||||
return nil
|
||||
end
|
||||
-- advance 2 more characters
|
||||
local version = read_s16(f); --f:read(2)
|
||||
-- the next characters here are our size, read them
|
||||
return read_s16(f), read_s16(f), read_s16(f), version
|
||||
end
|
||||
|
||||
size.x, size.y, size.z, size.version = get_schematic_size(file)
|
||||
|
||||
-- read the slice probability for each y value that was introduced in version 3
|
||||
if( size.version >= 3 ) then
|
||||
-- the probability is not very intresting for buildings so we just skip it
|
||||
file:read( size.y )
|
||||
end
|
||||
|
||||
-- this list is not yet used for anything
|
||||
local nodenames = {}
|
||||
local ground_id = {}
|
||||
local is_air = 0
|
||||
|
||||
-- after that: read_s16 (2 bytes) to find out how many diffrent nodenames (node_name_count) are present in the file
|
||||
local node_name_count = read_s16( file )
|
||||
|
||||
for i = 1, node_name_count do
|
||||
-- the length of the next name
|
||||
local name_length = read_s16( file )
|
||||
-- the text of the next name
|
||||
local name_text = file:read( name_length )
|
||||
nodenames[i] = name_text
|
||||
if string.sub(name_text, 1, 18) == "default:dirt_with_" or
|
||||
name_text == "farming:soil_wet" then
|
||||
ground_id[i] = true
|
||||
elseif( name_text == 'air' ) then
|
||||
is_air = i;
|
||||
end
|
||||
end
|
||||
|
||||
-- decompression was recently added; if it is not yet present, we need to use normal place_schematic
|
||||
if( minetest.decompress == nil) then
|
||||
file.close(file);
|
||||
return nil; -- normal place_schematic is no longer supported as minetest.decompress is now part of the release version of minetest
|
||||
end
|
||||
|
||||
local compressed_data = file:read( "*all" );
|
||||
local data_string = minetest.decompress(compressed_data, "deflate" );
|
||||
file.close(file)
|
||||
|
||||
local p2offset = (size.x*size.y*size.z)*3;
|
||||
local i = 1;
|
||||
|
||||
local scm = {};
|
||||
local min_pos = {}
|
||||
local max_pos = {}
|
||||
local nodecount = 0
|
||||
local ground_y = -1 --if nothing defined, it is under the building
|
||||
local groundnode_count = 0
|
||||
|
||||
for z = 1, size.z do
|
||||
for y = 1, size.y do
|
||||
for x = 1, size.x do
|
||||
local id = string.byte( data_string, i ) * 256 + string.byte( data_string, i+1 );
|
||||
i = i + 2;
|
||||
local p2 = string.byte( data_string, p2offset + math.floor(i/2));
|
||||
id = id+1;
|
||||
if( id ~= is_air ) then
|
||||
-- use node
|
||||
if( not( scm[y] )) then
|
||||
scm[y] = {};
|
||||
end
|
||||
if( not( scm[y][x] )) then
|
||||
scm[y][x] = {};
|
||||
end
|
||||
scm[y][x][z] = {name_id = id, param2 = p2};
|
||||
nodecount = nodecount + 1
|
||||
|
||||
-- adjust position information
|
||||
if not max_pos.x or x > max_pos.x then
|
||||
max_pos.x = x
|
||||
end
|
||||
if not max_pos.y or y > max_pos.y then
|
||||
max_pos.y = y
|
||||
end
|
||||
if not max_pos.z or z > max_pos.z then
|
||||
max_pos.z = z
|
||||
end
|
||||
if not min_pos.x or x < min_pos.x then
|
||||
min_pos.x = x
|
||||
end
|
||||
if not min_pos.y or y < min_pos.y then
|
||||
min_pos.y = y
|
||||
end
|
||||
if not min_pos.z or z < min_pos.z then
|
||||
min_pos.z = z
|
||||
end
|
||||
|
||||
-- calculate ground_y value
|
||||
if ground_id[id] then
|
||||
groundnode_count = groundnode_count + 1
|
||||
if groundnode_count == 1 then
|
||||
ground_y = y
|
||||
else
|
||||
ground_y = ground_y + (y - ground_y) / groundnode_count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return { min_pos = min_pos, -- minimal {x,y,z} vector
|
||||
max_pos = max_pos, -- maximal {x,y,z} vector
|
||||
nodenames = nodenames, -- nodenames[1] = "default:sample"
|
||||
scm_data_cache = scm, -- scm[y][x][z] = { name_id, ent.param2 }
|
||||
nodecount = nodecount, -- integer, count
|
||||
ground_y = ground_y } -- average ground high
|
||||
end
|
||||
|
||||
|
||||
schematics.analyze_we_file = function(file)
|
||||
-- returning parameters
|
||||
local nodenames = {}
|
||||
local scm = {}
|
||||
local all_meta = {}
|
||||
local min_pos = {}
|
||||
local max_pos = {}
|
||||
local ground_y = -1 --if nothing defined, it is under the building
|
||||
local nodecount = 0
|
||||
|
||||
-- helper
|
||||
local nodes = schemlib.worldedit_file.load_schematic(file:read("*a"))
|
||||
local nodenames_id = {}
|
||||
local ground_id = {}
|
||||
local groundnode_count = 0
|
||||
|
||||
-- analyze the file
|
||||
for i, ent in ipairs( nodes ) do
|
||||
-- get nodename_id and analyze ground elements
|
||||
local name_id = nodenames_id[ent.name]
|
||||
if not name_id then
|
||||
name_id = #nodenames + 1
|
||||
nodenames_id[ent.name] = name_id
|
||||
nodenames[name_id] = ent.name
|
||||
if string.sub(ent.name, 1, 18) == "default:dirt_with_" or
|
||||
ent.name == "farming:soil_wet" then
|
||||
ground_id[name_id] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- calculate ground_y value
|
||||
if ground_id[name_id] then
|
||||
groundnode_count = groundnode_count + 1
|
||||
if groundnode_count == 1 then
|
||||
ground_y = ent.y
|
||||
else
|
||||
ground_y = ground_y + (ent.y - ground_y) / groundnode_count
|
||||
end
|
||||
end
|
||||
|
||||
-- adjust position information
|
||||
if not max_pos.x or ent.x > max_pos.x then
|
||||
max_pos.x = ent.x
|
||||
end
|
||||
if not max_pos.y or ent.y > max_pos.y then
|
||||
max_pos.y = ent.y
|
||||
end
|
||||
if not max_pos.z or ent.z > max_pos.z then
|
||||
max_pos.z = ent.z
|
||||
end
|
||||
if not min_pos.x or ent.x < min_pos.x then
|
||||
min_pos.x = ent.x
|
||||
end
|
||||
if not min_pos.y or ent.y < min_pos.y then
|
||||
min_pos.y = ent.y
|
||||
end
|
||||
if not min_pos.z or ent.z < min_pos.z then
|
||||
min_pos.z = ent.z
|
||||
end
|
||||
|
||||
-- build to scm data tree
|
||||
if scm[ent.y] == nil then
|
||||
scm[ent.y] = {}
|
||||
end
|
||||
if scm[ent.y][ent.x] == nil then
|
||||
scm[ent.y][ent.x] = {}
|
||||
end
|
||||
if ent.param2 == nil then
|
||||
ent.param2 = 0
|
||||
end
|
||||
|
||||
-- metadata is only of intrest if it is not empty
|
||||
if( ent.meta and (ent.meta.fields or ent.meta.inventory)) then
|
||||
local has_meta = false
|
||||
for _,v in pairs( ent.meta.fields ) do
|
||||
has_meta = true
|
||||
break
|
||||
end
|
||||
for _,v in pairs(ent.meta.inventory) do
|
||||
has_meta = true
|
||||
break
|
||||
end
|
||||
if has_meta ~= true then
|
||||
ent.meta = nil
|
||||
end
|
||||
else
|
||||
ent.meta = nil
|
||||
end
|
||||
|
||||
scm[ent.y][ent.x][ent.z] = {name_id = name_id, param2 = ent.param2, meta = ent.meta}
|
||||
|
||||
nodecount = nodecount + 1
|
||||
end
|
||||
|
||||
return { min_pos = min_pos, -- minimal {x,y,z} vector
|
||||
max_pos = max_pos, -- maximal {x,y,z} vector
|
||||
nodenames = nodenames, -- nodenames[1] = "default:sample"
|
||||
scm_data_cache = scm, -- scm[y][x][z] = { name_id=, param2=, meta= }
|
||||
nodecount = nodecount, -- integer, count
|
||||
ground_y = ground_y } -- average ground high
|
||||
end
|
||||
|
||||
return schematics
|
8
world.lua
Normal file
8
world.lua
Normal file
@ -0,0 +1,8 @@
|
||||
--[[
|
||||
Class "world"
|
||||
- propose_y(world_pos)
|
||||
- plan_is_placeble(world_pos) - Plan is placeble to the position
|
||||
-- including check if an other plan in this area
|
||||
-- including check for is_ground_content ~= false in this area (nil is like true)
|
||||
|
||||
]]
|
137
worldedit_file.lua
Normal file
137
worldedit_file.lua
Normal file
@ -0,0 +1,137 @@
|
||||
------------------------------------------------------------------------------------------
|
||||
-- This is the file
|
||||
-- https://github.com/Uberi/Minetest-WorldEdit/blob/master/worldedit/serialization.lua
|
||||
-- Changes:
|
||||
-- * worldedit namespace renamed to worldeit_file
|
||||
-- * eliminiated functions that are not needed
|
||||
-- * made function load_schematic non-local
|
||||
-- * originx, originy and originz are now passed as parameters to worldedit_file.load_schematic;
|
||||
-- they are required for an old file format
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
local worldedit_file = {} -- add the namespace
|
||||
|
||||
--- Schematic serialization and deserialiation.
|
||||
-- @module worldedit.serialization
|
||||
|
||||
worldedit_file.LATEST_SERIALIZATION_VERSION = 5
|
||||
local LATEST_SERIALIZATION_HEADER = worldedit_file.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 worldedit_file.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 worldedit_file.load_schematic(value, we_origin)
|
||||
local version, header, content = worldedit_file.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
|
||||
|
||||
return worldedit_file
|
Loading…
x
Reference in New Issue
Block a user