commit f7ca76fac1209b281bf18d55e56bec10323f9f79 Author: Alexander Weber Date: Thu Jan 12 11:55:55 2017 +0100 initial commit - a plan can be created, most not wirking or not tested diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..18dad83 --- /dev/null +++ b/init.lua @@ -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) diff --git a/mapping.lua b/mapping.lua new file mode 100644 index 0000000..30f6f82 --- /dev/null +++ b/mapping.lua @@ -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 diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..c1a3bee --- /dev/null +++ b/mod.conf @@ -0,0 +1 @@ +name = schemlib diff --git a/plan.lua b/plan.lua new file mode 100644 index 0000000..3a3405c --- /dev/null +++ b/plan.lua @@ -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 diff --git a/save_restore.lua b/save_restore.lua new file mode 100644 index 0000000..9b1d3ec --- /dev/null +++ b/save_restore.lua @@ -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/ 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 diff --git a/schematics.lua b/schematics.lua new file mode 100644 index 0000000..074b001 --- /dev/null +++ b/schematics.lua @@ -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 diff --git a/world.lua b/world.lua new file mode 100644 index 0000000..72a6a06 --- /dev/null +++ b/world.lua @@ -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) + + ]] diff --git a/worldedit_file.lua b/worldedit_file.lua new file mode 100644 index 0000000..a9660b0 --- /dev/null +++ b/worldedit_file.lua @@ -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: + 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: ,,...: +--]] + + +--- 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