initial commit - a plan can be created, most not wirking or not tested

This commit is contained in:
Alexander Weber 2017-01-12 11:55:55 +01:00
commit f7ca76fac1
8 changed files with 1226 additions and 0 deletions

16
init.lua Normal file
View 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
View 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

1
mod.conf Normal file
View File

@ -0,0 +1 @@
name = schemlib

454
plan.lua Normal file
View 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
View 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
View 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
View 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
View 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