From c43bc12e401618eda4bb01eaa253866aeba3663e Mon Sep 17 00:00:00 2001 From: Alexander Weber Date: Mon, 30 Jul 2018 22:27:09 +0200 Subject: [PATCH] Big update around persistance --- README.md | 102 +++++-- init.lua | 4 +- mapping.lua | 5 +- node.lua | 38 ++- npc_ai.lua | 12 +- plan.lua | 678 +++++++++++++++++++++++++---------------------- plan_manager.lua | 175 ++++++++++++ save_restore.lua | 27 -- settingtypes.txt | 9 + 9 files changed, 647 insertions(+), 403 deletions(-) create mode 100644 plan_manager.lua delete mode 100644 save_restore.lua create mode 100644 settingtypes.txt diff --git a/README.md b/README.md index 6c18f95..c72625e 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,19 @@ License: LGPLv2 ## data types (and usual names): - node_obj - object representing a node in plan - plan_obj - object representing a whole plan - - 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_meta_obj - Simplified plan object that does not contain nodes data + - plan_pos - a relative position vector used in plan. Note: the relative + - world_pos - a absolute ("real") position vector in the world + - anchor_pos - World position assigned to plan for plan_pos<=>world_pos calculations ## Plan object +The plan is the building draft that can be readed from file or be generated. +The plan manages the nodes and some attributes how the target building should be placed. +The plan does have two stages, the first is the preparation stage the plan does not have +an anchor to the "real world". In this stage no world interactions are possible. +The second stage is after the plan get the anchor attribute set. +In this stage the world interaction methods like place node are possible. + ### class-methods - plan_obj = schemlib.plan.new([plan_id][,anchor_pos]) - Constructor - create a new plan object @@ -39,75 +48,118 @@ License: LGPLv2 - plan_obj:del_node(plan_pos) - delete a node from plan - plan_obj:get_random_plan_pos() - get a random existing plan_pos from plan - plan_obj:read_from_schem_file(file) - read from WorldEdit or mts file + - plan_obj:apply_flood_with_air + (add_max, add_min, add_top) - Fill a building with air - plan_obj:propose_anchor(world_pos, bool, add_xz) - propose anchor pos nearly given world_pos to be placed. if bool is given true a check will be done to prevent overbuilding of existing structures additional space to check for all sites can be given by add_xz (default 3) - returns "false, world_pos" in case of error. The world_pos is the issued not buildable position in this case - - plan_obj:apply_flood_with_air - (add_max, add_min, add_top) - Fill a building with air - - plan_obj:get_status() - get the plan status. Returns values are "new", "build" and "finished" - - plan_obj:set_status(status) - set the plan status. Created plan is new, allowed new stati are "build" and "finished" #### Methods interferring with the real world (anchor_pos exists or needs to be given optional) - plan_obj:get_world_pos(plan_pos[,anchor_pos]) - get a world position for a plan position + - plan_obj:get_plan_pos(world_pos[,anchor_pos]) - get a plan position for a world position - plan_obj:get_world_minp([anchor_pos]) - get lowest world position - plan_obj:get_world_maxp([anchor_pos]) - get highest world position - plan_obj:contains(world_pos[,anchor_pos]) - check if the given world position is in the plan - - plan_obj:get_plan_pos(world_pos[,anchor_pos]) - get a plan position for a world position - plan_obj:check_overlap(pos1, pos2[,add_distance][,anchor_pos]) - check if the plan overlap the area in pos1/pos2 - plan_obj:get_chunk_nodes(plan_pos[,anchor_pos]) - get a list of all nodes from chunk of a pos - plan_obj:do_add_chunk_place(plan_pos) - Place all nodes for chunk in real world using add_node() + - plan_obj:load_region(min_world_pos[, max_world_pos]) - Load a Voxel-Manip for faster lookups to the real world - plan_obj:do_add_chunk_voxel(plan_pos) - Place all nodes for chunk in real world using voxelmanip after emerge area - plan_obj:do_add_chunk_mapgen() - Place all nodes for current chunk in on_mapgen. Used internally for registered chunks in plan.mapgen_process - plan_obj:do_add_all_voxel_async() - Place all plan nodes in multiple async calls of do_add_chunk_voxel() - plan_obj:do_add_all_mapgen_async() - Register all nodes for mapgen processing - - plan_obj:load_region(min_world_pos[, max_world_pos]) - Load a Voxel-Manip for faster lookups to the real world -### Hooks - - plan_obj:on_status() - if defined, is called from get_plan_status() to get custom updates +#### Processing related + - plan_obj:get_status() - get the plan status. Returns values are "new", "build", "finished" or custom value like "pause" + - plan_obj:set_status(status) - set the plan status. Created plan is new, allowed new stati are "build" and "finished" + - status = "new" - Plan is in design mode + - status = "build" - do_add_all_voxel_async is running + - status = "finished" - Processing is done ### Attributes - plan_obj.plan_id - a id of the plan - - plan_obj.status - plan status - - plan_obj.anchor_pos - position vector in world - - plan_obj.data.nodeinfos - a list of node information for name_id with counter (list={pos_hash,...}, count=1}) - - plan_obj.data.ground_y - explicit ground adjustment for anchor_pos - - plan_obj.data.nodecount - count of the nodes in plan - - plan_obj.data.groundnode_count - count of nodes found for ground_y determination (internal) + - plan_obj.data.status - plan status - plan_obj.data.min_pos - minimal {x,y,z} vector - plan_obj.data.max_pos - maximal {x,y,z} vector - - plan_obj.facedir - Plan rotation - x+ axis supported only (values 0-3) - - plan_obj.mirrored - (bool) Mirrored build - mirror to z-axis. Note: if you need x-axis mirror - just rotate the building by 2 in addition + - plan_obj.data.groundnode_count - count of nodes found for ground_y determination (internal) + - plan_obj.data.nodecount - count of the nodes in plan + - plan_obj.data.nodeinfos - a list of node information for name_id with counter (list={pos_hash,...}, count=1}) + - plan_obj.data.ground_y - explicit ground adjustment for anchor_pos + - plan_obj.data.facedir - Plan rotation - x+ axis supported only (values 0-3) + - plan_obj.data.mirrored - (bool) Mirrored build - mirror to z-axis. Note: if you need x-axis mirror - just rotate the building by 2 in addition + - plan_obj.data.anchor_pos - position vector in world + - plan_obj.data.npc_build - : if true, the building is allowed to be build by schemlib_npc ## Node object +The node object represents one node on plan. This object does manage node rotations, +mapping to costs item and compatibility mapping for older worldedit files. +A node is usually assigned to a plan, the most methods require the plan assignment. + ### class-methods - - node_obj = schemlib.plan.new(data) - Constructor - create a new node object with given data + - node_obj = schemlib.plan.new([plan_id], [anchor_pos]) - Constructor - create a new node object. plan_id and anchor_pos is optional ### object-methods - node_obj:get_world_pos() - nodes assigned to plan only + - node_obj:rotate_facedir(facedir) - rotate the node - is internally used for plan rotation in get_mapped() - supported 0-3 (x+ axis) only - node_obj:get_mapped() - get mapped data for this node as it should be placed - returns a table {name=, param2=, meta=, content_id=, node_def=, final_nod_name=} - name, param2, meta - data used to place node - content_id, node_def - game references, VoxelMap ID and registered_nodes definition - final_node_name - if set, the node is not deleted from plan by place(). Contains the node name to be placed at the end. used for replacing by air before build the node - world_node_name - contains the node name currently placed to the world - param2_plan_rotation - param2 value before rotation - - node_obj:place() - place node to world using "add_node" and remove them from plan - - node_obj:remove_from_plan() - remove this node from plan - node_obj:get_under() - returns the node under this one if exists in plan - node_obj:get_above() - returns the node above this one if exists in plan - node_obj:get_attached_to() - returns the position the node is attached to - - node_obj:rotate_facedir(facedir) - rotate the node - is internally used for plan rotation in get_mapped() - supported 0-3 (x+ axis) only + - node_obj:place() - place node to world using "add_node" and remove them from plan + - node_obj:remove_from_plan() - remove this node from plan ### object-attributes - node_obj.name - original node name without mapping - node_obj.data - table with original param2 / meta / prob - - assigned in plan:add_node(plan_pos, node_obj) method - node_obj.plan - assigned plan - node_obj.nodeinfo - assigned nodeinfo in plan +Node mapping functions + - schemlib.mapping.is_equal_meta(data,data) - Recursivelly compare data. Primary developed to check node metadata for changes + - schemlib.mapping.map_unknown(item_name) - Internally used to map unknown nodes + - schemlib.mapping.map(name, plan) - Get mapping informations without any callback calls + + +## Plan Manager object +The plan manager does manage the persistance for WIP plans. Also the manager allow to check overlaps to other buildings. +Usually the manager does not store the full plan but the parameters including file how to load or +generate the plan again. The work can be resumed by starting new because +already placed nodes / chunks are passed +The plan manager is a singleton that means you have only one instance in game. Therefore implemented as functions + +### Functions + - schemlib.plan_manager.get_plan_meta(plan_id) - Get Meta-Plan-Object + - schemlib.plan_manager.get_plan(plan_id) - Get Plan Object from persistance manager + - schemlib.plan_manager.set_plan(plan_obj) - Add plan to persistance manager. Note, the definitely plan_obj.plan_id must be set. + - schemlib.plan_manager.delete_plan(plan_id) - Remove plan from persistance manager + +### plan_meta_class +Simplified plan class that works on metadata only, without nodes operations. Next methods are available: +adjust_building_info, get_world_pos, get_plan_pos, get_world_minp, get_world_maxp, contains, check_overlap + +### Plan manager settings +Note: all modified plans will be saved on shutdown independing on this parameters + +#### schemlib.save_interval (Plan manager save interval) int 10 0 +Save interval in seconds for plan changes in manager. 0 disables the interval saving. + +#### schemlib.save_maxnodes (Maximum building size for autosave) int 10000 0 +Maximum building size in nodes that are handled in interval save to avoid performance issue. 0 enables saving for all sizes. +Note: all (including big) modified plans will be saved on shutdown + + ## Builder NPC AI object +The builder NPC AI provides the logic how to build a building by NPC. +Basically an NPC get the next nearly buildable node from plan, and the method how to place, +but the navigation needs to be done in NPC Framework + ### class-methods - npc_ai_obj = schemlib.npc_ai.new(plan_obj, build_distance) - Constructor - create a new NPC AI handler for this plan. Build distance is the lenght of npc @@ -117,7 +169,7 @@ License: LGPLv2 next methods internally used in plan_target_get - npc_ai_obj:get_if_buildable(node_obj) - Check the node_obj if it can be built in the world. Compares if similar node already at the place - - npc_ai_obj:get_node_rating(node, npcpos) - internally used - rate a node for importance to build at the next + - npc_ai_obj:get_node_rating(node, npcpos) - internally used - rate a node for importance to build at the next step - npc_ai_obj:prefer_target(npcpos, nodeslist) - Does rating of all nodes in nodeslist and returns the highest rated node ### object-attributes diff --git a/init.lua b/init.lua index 58bc743..8e6e6ed 100644 --- a/init.lua +++ b/init.lua @@ -1,5 +1,3 @@ - - schemlib = {} local modpath = minetest.get_modpath(minetest.get_current_modname()) schemlib.modpath = modpath @@ -7,8 +5,8 @@ schemlib.modpath = modpath schemlib.mapping = dofile(modpath.."/mapping.lua") schemlib.node = dofile(modpath.."/node.lua") schemlib.worldedit_file = dofile(modpath.."/worldedit_file.lua") -schemlib.save_restore = dofile(modpath.."/save_restore.lua") schemlib.plan = dofile(modpath.."/plan.lua") +schemlib.plan_manager = dofile(modpath.."/plan_manager.lua") schemlib.npc_ai = dofile(modpath.."/npc_ai.lua") -- log that we started diff --git a/mapping.lua b/mapping.lua index 2ed698d..2225238 100644 --- a/mapping.lua +++ b/mapping.lua @@ -136,7 +136,7 @@ local default_replacements = { ----------------------------------------------- -- Handle doors mirroring (_a vs _b) ----------------------------------------------- -function __mirror_doors(mr) +local function __mirror_doors(mr) if not mr.node_def.door then return end @@ -242,7 +242,7 @@ function mapping.map(name, plan) local node_def = minetest.registered_nodes[mr.name] mr.node_def = node_def - if plan and plan.mirrored then + if plan and plan.data.mirrored then __mirror_doors(mr) end @@ -310,6 +310,7 @@ minetest.after(0, function() mapping._airlike_contend_ids[minetest.get_content_id(name)] = name end end + mapping._protected_content_ids[minetest.get_content_id("default:ice")] = nil --allow ice removal mapping._over_surface_content_ids[minetest.get_content_id("air")] = "air" mapping._over_surface_content_ids[minetest.get_content_id("default:snow")] = "default:snow" diff --git a/node.lua b/node.lua index 011bdbd..d5c94a2 100644 --- a/node.lua +++ b/node.lua @@ -4,15 +4,14 @@ local mapping = schemlib.mapping -- Node class -------------------------------------- local node_class = {} -node_class.__index = node_class +local node_class_mt = {__index = node_class} local node = {} node.node_class = node_class ------------------------------------- -- Create new node -------------------------------------- function node.new(data) - local self = setmetatable({}, node_class) - self.__index = node_class + local self = setmetatable({}, node_class_mt) self.name = data.name assert(self.name, "No name given for node object") @@ -53,7 +52,7 @@ function node_class:rotate_facedir(facedir) if mapped.node_def.paramtype2 == "wallmounted" then local param2_dir = mapped.param2 % 8 local param2_color = mapped.param2 - param2_dir - if self.plan.mirrored then + if self.plan.data.mirrored then param2_dir = node.rotation_wallmounted_mirrored_map[param2_dir] end mapped.param2 = node.rotation_wallmounted_map[facedir][param2_dir] + param2_color @@ -61,15 +60,14 @@ function node_class:rotate_facedir(facedir) -- rotate facedir local param2_dir = mapped.param2 % 32 local param2_color = mapped.param2 - param2_dir - if self.plan.mirrored then + if self.plan.data.mirrored then param2_dir = node.rotation_facedir_mirrored_map[param2_dir] end mapped.param2 = node.rotation_facedir_map[facedir][param2_dir] + param2_color end - - end + ------------------------------------- -- Get all information to build the node -------------------------------------- @@ -77,21 +75,20 @@ function node_class:get_mapped() if self.mapped == 'unknown' then return end - - local mappedinfo = self.nodeinfo.mapped + local mappedinfo = self.plan.mapping_cache[self.name] if not mappedinfo then mappedinfo = mapping.map(self.name, self.plan) - self.nodeinfo.mapped = mappedinfo + self.plan.mapping_cache[self.name] = mappedinfo self.mapped = nil end if not mappedinfo or mappedinfo == 'unknown' then - self.nodeinfo.mapped = 'unknown' + self.plan.mapping_cache[self.name] = 'unknown' self.mapped = 'unknown' return end - if self.mapped and self.mapped.name == mappedinfo.name_orig then + if self.mapped then return self.mapped end @@ -110,7 +107,7 @@ function node_class:get_mapped() self.mapped = mapped self.cost_item = mapped.cost_item -- workaround / backwards compatibility to npcf_builder - self:rotate_facedir(self.plan.facedir) + self:rotate_facedir(self.plan.data.facedir) return mapped end @@ -122,6 +119,13 @@ function node_class:get_under() return self.plan:get_node({x=self._plan_pos.x, y=self._plan_pos.y-1, z=self._plan_pos.z}) end +-------------------------------------- +-- get node above this one if exists +-------------------------------------- +function node_class:get_above() + return self.plan:get_node({x=self._plan_pos.x, y=self._plan_pos.y+1, z=self._plan_pos.z}) +end + -------------------------------------- -- get plan_pos of attached node, or nil if not attached -------------------------------------- @@ -143,14 +147,6 @@ function node_class:get_attached_to() end end --------------------------------------- --- get node above this one if exists --------------------------------------- -function node_class:get_above() - return self.plan:get_node({x=self._plan_pos.x, y=self._plan_pos.y+1, z=self._plan_pos.z}) -end - - -------------------------------------- -- add/build a node -------------------------------------- diff --git a/npc_ai.lua b/npc_ai.lua index b1a057c..c55c084 100644 --- a/npc_ai.lua +++ b/npc_ai.lua @@ -6,14 +6,12 @@ local mapping = schemlib.mapping local npc_ai = {} local npc_ai_class = {} -npc_ai_class.__index = npc_ai_class - +local mpc_ai_class_mt = {__index = npc_ai_class} -------------------------------------- -- Create NPC-AI object -------------------------------------- function npc_ai.new(plan, build_distance) - local self = setmetatable({}, npc_ai_class) - self.__index = npc_ai_class + local self = setmetatable({}, mpc_ai_class_mt) self.plan = plan self.build_distance = build_distance or 3 return self @@ -93,7 +91,6 @@ end -- Get rating for node which one should be built at next -------------------------------------- function npc_ai_class:get_node_rating(node, npcpos) - local world_pos = node:get_world_pos() local mapped = node:get_mapped() local distance_pos = table.copy(world_pos) @@ -204,7 +201,10 @@ function npc_ai_class:plan_target_get(npcpos) local chunk_nodes, min_world_pos, max_world_pos = self.plan:get_chunk_nodes(npcpos_plan) -- add last selection to the current chunk to compare if self.lasttarget_pos then - table.insert(chunk_nodes, self.plan:get_node(self.lasttarget_pos)) + local last_target_node = self.plan:get_node(self.lasttarget_pos) + if last_target_node then + table.insert(chunk_nodes, last_target_node) + end end dprint("Chunk loaeded: nodes:", #chunk_nodes) self.plan:load_region(min_world_pos, max_world_pos) diff --git a/plan.lua b/plan.lua index 9149bb5..25e53d4 100644 --- a/plan.lua +++ b/plan.lua @@ -8,82 +8,98 @@ local node = schemlib.node local mapping = schemlib.mapping -------------------------------------- --- Plan class +-- Plan class -------------------------------------- local plan_class = {} -plan_class.__index = plan_class - +local plan_class_mt = {__index = plan_class} -------------------------------------- --- Plan class-methods and attributes +-- Plan class-methods and attributes -------------------------------------- local plan = { - mapgen_process = {} + mapgen_process = {}, + plan_class = plan_class } local mapgen_process = plan.mapgen_process -------------------------------------- --- Create new plan object +-- Create new plan object -------------------------------------- function plan.new(plan_id , anchor_pos) - local self = setmetatable({}, plan_class) - self.__index = plan_class + local self = setmetatable({}, plan_class_mt) self.plan_id = plan_id - self.anchor_pos = anchor_pos - self.facedir = 0 - self.mirrored = false + self.scm_data_cache = {} + self.mapping_cache = {} self.data = { + status = "new", min_pos = {}, max_pos = {}, groundnode_count = 0, - ground_y = -1, --if nothing defined, it is under the building - scm_data_cache = {}, nodeinfos = {}, nodecount = 0, + ground_y = -1, --if nothing defined, it is under the building + + facedir = 0, + mirrored = false, + anchor_pos = anchor_pos, } - self.status = "new" return self -- the plan object end -------------------------------------- ---Add node to plan +-- Add node to plan -------------------------------------- function plan_class:add_node(plan_pos, node) - -- build 3d cache tree - self.data.scm_data_cache[plan_pos.y] = self.data.scm_data_cache[plan_pos.y] or {} - self.data.scm_data_cache[plan_pos.y][plan_pos.x] = self.data.scm_data_cache[plan_pos.y][plan_pos.x] or {} - local replaced_node = self.data.scm_data_cache[plan_pos.y][plan_pos.x][plan_pos.z] - if not replaced_node then - self.data.nodecount = self.data.nodecount + 1 + -- check if any old node is replaced + local replaced_node = self:get_node(plan_pos) + if replaced_node then + self.data.nodeinfos[replaced_node.name].count = self.data.nodeinfos[replaced_node.name].count - 1 else - self.data.nodeinfos[replaced_node.name].count = self.data.nodeinfos[replaced_node.name].count-1 + self.data.nodecount = self.data.nodecount + 1 + self.scm_data_cache[plan_pos.y] = self.scm_data_cache[plan_pos.y] or {} + self.scm_data_cache[plan_pos.y][plan_pos.x] = self.scm_data_cache[plan_pos.y][plan_pos.x] or {} end - -- insert to nodeinfos - local nodeinfo = self.data.nodeinfos[node.name] + -- Parse input data + local node_name, node_data, node_meta + if type(node) == "string" then + node_name = node + node_data = node + else + node_name = node.name + if node.meta then + if (node.meta.fields and next(node.meta.fields)) or + (node.meta.inventory and next(node.meta.inventory)) then + node_meta = node.meta + end + end + end + + -- Adjust nodeinfo and prepare mapping cache + local nodeinfo = self.data.nodeinfos[node_name] if not nodeinfo then - nodeinfo = {name_orig = node.name, count = 1} - self.data.nodeinfos[node.name] = nodeinfo + nodeinfo = {name = node_name, count = 1} + self.data.nodeinfos[node_name] = nodeinfo else nodeinfo.count = nodeinfo.count + 1 end - node.nodeinfo = nodeinfo - local def = minetest.registered_nodes[node.name] - if not node.data.meta and not node.data.prob and def and - (not def.paramtype2 or def.paramtype2 == "none") then - -- Deduplicated node - nodeinfo.deduplicated_node = nodeinfo.deduplicated_node or { - name = node.name, - nodeinfo = nodeinfo, - deduplicated = true, - } - self.data.scm_data_cache[plan_pos.y][plan_pos.x][plan_pos.z] = nodeinfo.deduplicated_node - else - self.data.scm_data_cache[plan_pos.y][plan_pos.x][plan_pos.z] = node + + -- Check if storage could be stripped to name only + if not node_data and not node_meta and not node.prob then + local def = minetest.registered_nodes[node_name] + if def and (not def.paramtype2 or def.paramtype2 == "none") then + node_data = node.name + end end + + if not node_data then + node_data = { name = node_name, meta = node_meta, prob = node.prob, param2 = node.param2 } + end + self.scm_data_cache[plan_pos.y][plan_pos.x][plan_pos.z] = node_data + self.modified = true end -------------------------------------- ---Adjust building size and ground info +-- Adjust building size and ground info -------------------------------------- function plan_class:adjust_building_info(plan_pos, node) -- adjust min/max position information @@ -121,236 +137,53 @@ end -- Get node from plan -------------------------------------- function plan_class:get_node(plan_pos) - local cached_node = self.data.scm_data_cache[plan_pos.y] and - self.data.scm_data_cache[plan_pos.y][plan_pos.x] and - self.data.scm_data_cache[plan_pos.y][plan_pos.x][plan_pos.z] + local cached_node = self.scm_data_cache[plan_pos.y] and + self.scm_data_cache[plan_pos.y][plan_pos.x] and + self.scm_data_cache[plan_pos.y][plan_pos.x][plan_pos.z] if not cached_node then return end - local dedup_node = cached_node - if cached_node.deduplicated then - dedup_node = node.new(cached_node) - dedup_node.nodeinfo = cached_node.nodeinfo + local ret_node + if type(cached_node) == "string" then + ret_node = node.new({name = cached_node}) + else + ret_node = node.new(cached_node) end - dedup_node.plan = self - dedup_node._plan_pos = plan_pos - return dedup_node + ret_node.nodeinfo = self.data.nodeinfos[ret_node.name] + ret_node.plan = self + ret_node._plan_pos = plan_pos + return ret_node end -------------------------------------- ---Delete node from plan +-- Delete node from plan -------------------------------------- function plan_class:del_node(pos) - if self.data.scm_data_cache[pos.y] then - if self.data.scm_data_cache[pos.y][pos.x] then - if self.data.scm_data_cache[pos.y][pos.x][pos.z] then - local oldnode = self.data.scm_data_cache[pos.y][pos.x][pos.z] - self.data.nodeinfos[oldnode.name].count = self.data.nodeinfos[oldnode.name].count - 1 - self.data.nodecount = self.data.nodecount - 1 - self.data.scm_data_cache[pos.y][pos.x][pos.z] = nil - end - if not next(self.data.scm_data_cache[pos.y][pos.x]) then - self.data.scm_data_cache[pos.y][pos.x] = nil - end - end - if not next(self.data.scm_data_cache[pos.y]) then - self.data.scm_data_cache[pos.y] = nil - end - end -end - --------------------------------------- ---Flood ta buildingplan with air --------------------------------------- -function plan_class:apply_flood_with_air(add_max, add_min, add_top) - self.data.ground_y = math.floor(self.data.ground_y) - add_max = add_max or 3 - add_min = add_min or 0 - add_top = add_top or 5 - - -- cache air_id - local air_id - - dprint("create flatting plan") - for y = self.data.min_pos.y, self.data.max_pos.y + add_top do - --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) - local air_node = {name = "air"} - 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 pos = {x=x, y=y, z=z} - if not self:get_node(pos) then - self:add_node(pos, node.new(air_node)) + if self.scm_data_cache[pos.y] then + if self.scm_data_cache[pos.y][pos.x] then + if self.scm_data_cache[pos.y][pos.x][pos.z] then + local oldnode = self.scm_data_cache[pos.y][pos.x][pos.z] + if type(oldnode) == "table" then + oldnode = oldnode.name end + self.data.nodeinfos[oldnode].count = self.data.nodeinfos[oldnode].count - 1 + self.data.nodecount = self.data.nodecount - 1 + self.scm_data_cache[pos.y][pos.x][pos.z] = nil + end + if not next(self.scm_data_cache[pos.y][pos.x]) then + self.scm_data_cache[pos.y][pos.x] = nil end end + if not next(self.scm_data_cache[pos.y]) then + self.scm_data_cache[pos.y] = nil + end end - dprint("flatting plan done") + self.modified = true end -------------------------------------- ---Get world position relative to plan position +-- Get a random position of an existing node in plan -------------------------------------- -function plan_class:get_world_pos(plan_pos, anchor_pos) - local apos = anchor_pos or self.anchor_pos - local pos - if self.mirrored then - pos = table.copy(plan_pos) - pos.x = -pos.x - else - pos = plan_pos - end - local facedir_rotated = { - [0] = function(pos,apos) return { - x=pos.x+apos.x, - y=pos.y+apos.y, - z=pos.z+apos.z, - }end, - [1] = function(pos,apos) return { - x=pos.z+apos.x, - y=pos.y+apos.y, - z=-pos.x+apos.z, - } end, - [2] = function(pos,apos) return { - x=-pos.x+apos.x, - y=pos.y+apos.y, - z=-pos.z+apos.z, - } end, - [3] = function(pos,apos) return { - x=-pos.z+apos.x, - y=pos.y+apos.y, - z=pos.x+apos.z, - } end, - } - local ret = facedir_rotated[self.facedir](pos, apos) - ret.y = ret.y - self.data.ground_y - 1 - return ret -end - --------------------------------------- ---Get world minimum position relative to plan position --------------------------------------- -function plan_class:get_world_minp(anchor_pos) - local pos = self:get_world_pos(self.data.min_pos, anchor_pos) - local pos2 = self:get_world_pos(self.data.max_pos, anchor_pos) - if pos2.x < pos.x then - pos.x = pos2.x - end - if pos2.y < pos.y then - pos.y = pos2.y - end - if pos2.z < pos.z then - pos.z = pos2.z - end - return pos -end - --------------------------------------- ---Get world maximum relative to plan position --------------------------------------- -function plan_class:get_world_maxp(anchor_pos) - local pos = self:get_world_pos(self.data.max_pos, anchor_pos) - local pos2 = self:get_world_pos(self.data.min_pos, anchor_pos) - if pos2.x > pos.x then - pos.x = pos2.x - end - if pos2.y > pos.y then - pos.y = pos2.y - end - if pos2.z > pos.z then - pos.z = pos2.z - end - return pos -end - --------------------------------------- ---Check if world position is in plan --------------------------------------- -function plan_class:contains(chkpos, anchor_pos) - local minp = self:get_world_minp(anchor_pos) - local maxp = self:get_world_maxp(anchor_pos) - - return (chkpos.x >= minp.x) and (chkpos.x <= maxp.x) and - (chkpos.y >= minp.y) and (chkpos.y <= maxp.y) and - (chkpos.z >= minp.z) and (chkpos.z <= maxp.z) -end - - --------------------------------------- ---Check if the plan overlaps with given area --------------------------------------- -function plan_class:check_overlap(minp, maxp, add_distance, anchor_pos) - - add_distance = add_distance or 0 - - local minp_a = vector.subtract(minp, add_distance) - local maxp_a = vector.add(maxp, add_distance) - - local minp_b = vector.subtract(self:get_world_minp(anchor_pos), add_distance) - local maxp_b = vector.add(self:get_world_maxp(anchor_pos), add_distance) - - return ((minp_a.x >= minp_b.x and minp_a.x <= maxp_b.x) or - (maxp_a.x >= minp_b.x and maxp_a.x <= maxp_b.x) or - (minp_b.x >= minp_a.x and minp_b.x <= maxp_a.x) or - (maxp_b.x >= minp_a.x and maxp_b.x <= maxp_a.x)) - and - ((minp_a.y >= minp_b.y and minp_a.y <= maxp_b.y) or - (maxp_a.y >= minp_b.y and maxp_a.y <= maxp_b.y) or - (minp_b.y >= minp_a.y and minp_b.y <= maxp_a.y) or - (maxp_b.y >= minp_a.y and maxp_b.y <= maxp_a.y)) - and - ((minp_a.z >= minp_b.z and minp_a.z <= maxp_b.z) or - (maxp_a.z >= minp_b.z and maxp_a.z <= maxp_b.z) or - (minp_b.z >= minp_a.z and minp_b.z <= maxp_a.z) or - (maxp_b.z >= minp_a.z and maxp_b.z <= maxp_a.z)) -end --------------------------------------- ---Get plan position relative to world position --------------------------------------- -function plan_class:get_plan_pos(world_pos, anchor_pos) - local apos = anchor_pos or self.anchor_pos - local facedir_rotated = { - [0] = function(pos,apos) return { - x=pos.x-apos.x, - y=pos.y-apos.y, - z=pos.z-apos.z - } end, - [1] = function(pos,apos) return { - x=-(pos.z-apos.z), - y=pos.y-apos.y, - z=(pos.x-apos.x), - } end, - [2] = function(pos,apos) return { - x=-(pos.x-apos.x), - y=pos.y-apos.y, - z=-(pos.z-apos.z), - } end, - [3] = function(pos,apos) return { - x=pos.z-apos.z, - y=pos.y-apos.y, - z=-(pos.x-apos.x), - } end, - } - local ret = facedir_rotated[self.facedir](world_pos, apos) - - if self.mirrored then - ret.x = -ret.x - end - ret.y = ret.y + self.data.ground_y + 1 - return ret -end - - -------------------------------------- - --Get a random position of an existing node in plan - -------------------------------------- -- get nodes for selection which one should be build -- skip parameter is randomized function plan_class:get_random_plan_pos() @@ -358,7 +191,7 @@ function plan_class:get_random_plan_pos() -- get random existing y local keyset = {} - for k in pairs(self.data.scm_data_cache) do table.insert(keyset, k) end + for k in pairs(self.scm_data_cache) do table.insert(keyset, k) end if #keyset == 0 then --finished return end @@ -366,12 +199,12 @@ function plan_class:get_random_plan_pos() -- get random existing x keyset = {} - for k in pairs(self.data.scm_data_cache[y]) do table.insert(keyset, k) end + for k in pairs(self.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 + for k in pairs(self.scm_data_cache[y][x]) do table.insert(keyset, k) end local z = keyset[math.random(#keyset)] if z then @@ -379,44 +212,6 @@ function plan_class:get_random_plan_pos() end end --------------------------------------- ---Get a nodes list for a world chunk --------------------------------------- -function plan_class:get_chunk_nodes(plan_pos, anchor_pos) --- calculate the begin of the chunk - --local BLOCKSIZE = core.MAP_BLOCKSIZE - local BLOCKSIZE = 16 - local wpos = self:get_world_pos(plan_pos, anchor_pos) - 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] then - for x = minv.x, maxv.x do - if self.data.scm_data_cache[y][x] then - for z = minv.z, maxv.z do - if self.data.scm_data_cache[y][x][z] then - table.insert(ret, self:get_node({x=x, y=y,z=z})) - end - end - end - end - end - end - dprint("nodes in chunk to build", #ret) - return ret, minp, maxp -- minp/maxp are worldpos -end - -------------------------------------- -- Generate a plan from schematics file -------------------------------------- @@ -442,9 +237,8 @@ function plan_class:read_from_schem_file(filename) y = math.floor((i-1)/schematic.size.x) % schematic.size.y, x = (i-1) % schematic.size.x } - local new_node = node.new(ent) - self:add_node(pos, new_node) - self:adjust_building_info(pos, new_node) + self:add_node(pos, ent) + self:adjust_building_info(pos, ent) end end -- WorldEdit files @@ -458,15 +252,50 @@ function plan_class:read_from_schem_file(filename) -- analyze the file for i, ent in ipairs( nodes ) do local pos = {x=ent.x, y=ent.y, z=ent.z} - local new_node = node.new(ent) - self:add_node(pos, new_node) - self:adjust_building_info(pos, new_node) + self:add_node(pos, ent) + self:adjust_building_info(pos, ent) end end end -------------------------------------- ---Propose anchor position for the plan +-- Flood ta buildingplan with air +-------------------------------------- +function plan_class:apply_flood_with_air(add_max, add_min, add_top) + self.data.ground_y = math.floor(self.data.ground_y) + add_max = add_max or 3 + add_min = add_min or 0 + add_top = add_top or 5 + + -- cache air_id + local air_id + + dprint("create flatting plan") + for y = self.data.min_pos.y, self.data.max_pos.y + add_top do + --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 pos = {x=x, y=y, z=z} + if not self:get_node(pos) then + self:add_node(pos, "air") + end + end + end + end + dprint("flatting plan done") +end + +-------------------------------------- +-- Propose anchor position for the plan -------------------------------------- function plan_class:propose_anchor(world_pos, do_check, add_xz) add_xz = add_xz or 4 --distance to other buildings to check should be the same additional air filler + distance @@ -605,7 +434,218 @@ function plan_class:propose_anchor(world_pos, do_check, add_xz) end -------------------------------------- ---add/build a chunk +-- Get world position relative to plan position +-------------------------------------- +function plan_class:get_world_pos(plan_pos, anchor_pos) + local apos = anchor_pos or self.data.anchor_pos + local pos + if self.data.mirrored then + pos = {x=plan_pos.x, y=plan_pos.y, z=plan_pos.z} + pos.x = -pos.x + else + pos = plan_pos + end + local facedir_rotated = { + [0] = function(pos,apos) return { + x=pos.x+apos.x, + y=pos.y+apos.y, + z=pos.z+apos.z, + }end, + [1] = function(pos,apos) return { + x=pos.z+apos.x, + y=pos.y+apos.y, + z=-pos.x+apos.z, + } end, + [2] = function(pos,apos) return { + x=-pos.x+apos.x, + y=pos.y+apos.y, + z=-pos.z+apos.z, + } end, + [3] = function(pos,apos) return { + x=-pos.z+apos.x, + y=pos.y+apos.y, + z=pos.x+apos.z, + } end, + } + local ret = facedir_rotated[self.data.facedir](pos, apos) + ret.y = ret.y - self.data.ground_y - 1 + return ret +end + +-------------------------------------- +-- Get plan position relative to world position +-------------------------------------- +function plan_class:get_plan_pos(world_pos, anchor_pos) + local apos = anchor_pos or self.data.anchor_pos + local facedir_rotated = { + [0] = function(pos,apos) return { + x=pos.x-apos.x, + y=pos.y-apos.y, + z=pos.z-apos.z + } end, + [1] = function(pos,apos) return { + x=-(pos.z-apos.z), + y=pos.y-apos.y, + z=(pos.x-apos.x), + } end, + [2] = function(pos,apos) return { + x=-(pos.x-apos.x), + y=pos.y-apos.y, + z=-(pos.z-apos.z), + } end, + [3] = function(pos,apos) return { + x=pos.z-apos.z, + y=pos.y-apos.y, + z=-(pos.x-apos.x), + } end, + } + local ret = facedir_rotated[self.data.facedir](world_pos, apos) + if self.data.mirrored then + ret.x = -ret.x + end + ret.y = ret.y + self.data.ground_y + 1 + return ret +end + +-------------------------------------- +-- Get world minimum position relative to plan position +-------------------------------------- +function plan_class:get_world_minp(anchor_pos) + local pos = self:get_world_pos(self.data.min_pos, anchor_pos) + local pos2 = self:get_world_pos(self.data.max_pos, anchor_pos) + if pos2.x < pos.x then + pos.x = pos2.x + end + if pos2.y < pos.y then + pos.y = pos2.y + end + if pos2.z < pos.z then + pos.z = pos2.z + end + return pos +end + +-------------------------------------- +-- Get world maximum relative to plan position +-------------------------------------- +function plan_class:get_world_maxp(anchor_pos) + local pos = self:get_world_pos(self.data.max_pos, anchor_pos) + local pos2 = self:get_world_pos(self.data.min_pos, anchor_pos) + if pos2.x > pos.x then + pos.x = pos2.x + end + if pos2.y > pos.y then + pos.y = pos2.y + end + if pos2.z > pos.z then + pos.z = pos2.z + end + return pos +end + +-------------------------------------- +-- Check if world position is in plan +-------------------------------------- +function plan_class:contains(chkpos, anchor_pos) + local minp = self:get_world_minp(anchor_pos) + local maxp = self:get_world_maxp(anchor_pos) + + return (chkpos.x >= minp.x) and (chkpos.x <= maxp.x) and + (chkpos.y >= minp.y) and (chkpos.y <= maxp.y) and + (chkpos.z >= minp.z) and (chkpos.z <= maxp.z) +end + +-------------------------------------- +-- Check if the plan overlaps with given area +-------------------------------------- +function plan_class:check_overlap(minp, maxp, add_distance, anchor_pos) + add_distance = add_distance or 0 + + local minp_a = vector.subtract(minp, add_distance) + local maxp_a = vector.add(maxp, add_distance) + + local minp_b = vector.subtract(self:get_world_minp(anchor_pos), add_distance) + local maxp_b = vector.add(self:get_world_maxp(anchor_pos), add_distance) + + local overlap_pos = {} + + overlap_pos.x = + (minp_a.x >= minp_b.x and minp_a.x <= maxp_b.x) and math.floor((minp_a.x+maxp_b.x)/2) or + (maxp_a.x >= minp_b.x and maxp_a.x <= maxp_b.x) and math.floor((maxp_a.x+minp_b.x)/2) or + (minp_b.x >= minp_a.x and minp_b.x <= maxp_a.x) and math.floor((minp_b.x+maxp_a.x)/2) or + (maxp_b.x >= minp_a.x and maxp_b.x <= maxp_a.x) and math.floor((maxp_b.x+minp_a.x)/2) + + if not overlap_pos.x then + return + end + + overlap_pos.z = + (minp_a.z >= minp_b.z and minp_a.z <= maxp_b.z) and math.floor((minp_a.z+maxp_b.z)/2) or + (maxp_a.z >= minp_b.z and maxp_a.z <= maxp_b.z) and math.floor((maxp_a.z+minp_b.z)/2) or + (minp_b.z >= minp_a.z and minp_b.z <= maxp_a.z) and math.floor((minp_b.z+maxp_a.z)/2) or + (maxp_b.z >= minp_a.z and maxp_b.z <= maxp_a.z) and math.floor((maxp_b.z+minp_a.z)/2) + if not overlap_pos.z then + return + end + + overlap_pos.y = + (minp_a.y >= minp_b.y and minp_a.y <= maxp_b.y) and math.floor((minp_a.y+maxp_b.y)/2) or + (maxp_a.y >= minp_b.y and maxp_a.y <= maxp_b.y) and math.floor((maxp_a.y+minp_b.y)/2) or + (minp_b.y >= minp_a.y and minp_b.y <= maxp_a.y) and math.floor((minp_b.y+maxp_a.y)/2) or + (maxp_b.y >= minp_a.y and maxp_b.y <= maxp_a.y) and math.floor((maxp_b.y+minp_a.y)/2) + if not overlap_pos.y then + return + end + + dprint("Overlap", + "minp_a:"..minetest.pos_to_string(minp_a), + "maxp_a:"..minetest.pos_to_string(maxp_a), + "minp_b:"..minetest.pos_to_string(minp_b), + "maxp_b:"..minetest.pos_to_string(maxp_b), + "=>"..minetest.pos_to_string(overlap_pos)) + return overlap_pos +end + +-------------------------------------- +-- Get a nodes list for a world chunk +-------------------------------------- +function plan_class:get_chunk_nodes(plan_pos, anchor_pos) +-- calculate the begin of the chunk + --local BLOCKSIZE = core.MAP_BLOCKSIZE + local BLOCKSIZE = 16 + local wpos = self:get_world_pos(plan_pos, anchor_pos) + 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.scm_data_cache[y] then + for x = minv.x, maxv.x do + if self.scm_data_cache[y][x] then + for z = minv.z, maxv.z do + if self.scm_data_cache[y][x][z] then + table.insert(ret, self:get_node({x=x, y=y,z=z})) + end + end + end + end + end + end + dprint("nodes in chunk to build", #ret) + return ret, minp, maxp -- minp/maxp are worldpos +end + +-------------------------------------- +-- Add/build a chunk -------------------------------------- function plan_class:do_add_chunk_place(plan_pos) dprint("---build chunk", minetest.pos_to_string(plan_pos)) @@ -617,7 +657,7 @@ function plan_class:do_add_chunk_place(plan_pos) end -------------------------------------- --- Load a region to the voxel +-- Load a region to the voxel -------------------------------------- function plan_class:load_region(min_world_pos, max_world_pos) if not max_world_pos then @@ -631,7 +671,7 @@ function plan_class:load_region(min_world_pos, max_world_pos) end -------------------------------------- --- add/build a chunk using VoxelArea (internal usage) +-- Add/build a chunk using VoxelArea (internal usage) -------------------------------------- function plan_class:do_add_chunk_voxel_int() local meta_fix = {} @@ -684,6 +724,9 @@ function plan_class:do_add_chunk_voxel_int() end end +-------------------------------------- +-- Local function for emergeblocks callback +-------------------------------------- local function emergeblocks_callback(pos, action, num_calls_remaining, ctx) if not ctx.total_blocks then ctx.total_blocks = num_calls_remaining + 1 @@ -703,7 +746,7 @@ local function emergeblocks_callback(pos, action, num_calls_remaining, ctx) end -------------------------------------- --- add/build a chunk using VoxelArea +-- Add/build a chunk using VoxelArea -------------------------------------- function plan_class:do_add_chunk_voxel(plan_pos, after_call_func) -- Register for on_generate build @@ -720,7 +763,7 @@ function plan_class:do_add_chunk_voxel(plan_pos, after_call_func) end -------------------------------------- --- add/build a chunk using VoxelArea called from mapgen +-- Add/build a chunk using VoxelArea called from mapgen -------------------------------------- function plan_class:do_add_chunk_mapgen() plan._vm, plan._vm_minp, plan._vm_maxp = minetest.get_mapgen_object("voxelmanip") @@ -757,7 +800,7 @@ function plan_class:do_add_all_voxel_async() end -------------------------------------- ----add/build a chunk using VoxelArea +-- Add/build a chunk using VoxelArea -------------------------------------- function plan_class:do_add_all_mapgen_async() -- Register for on_generate build @@ -781,33 +824,30 @@ function plan_class:do_add_all_mapgen_async() end -------------------------------------- ---- Get the building status. (new, build, pause, finished) +-- Get the building status. (new, build, pause, finished) -------------------------------------- function plan_class:get_status() - if self.status == "build" then + if self.data.status == "build" then if self.data.nodecount == 0 then dprint("finished by nodecount 0 in get_status") - self.status = "finished" + self.data.status = "finished" end end - if self.on_status then -- trigger updates trough this hook - self:on_status(self.status) - end - return self.status + return self.data.status end -------------------------------------- ---- Set the building status. (new, build, pause, finished) +-- Set the building status. (new, build, pause, finished) -------------------------------------- function plan_class:set_status(status) - self.status = status - if self.on_status then -- trigger updates trough this hook - self:on_status(self.status) + self.data.status = status + if status == "build" then + minetest.after(0.1, self.do_add_all_voxel_async, self) end end -------------------------------------- ----Process registered on generated chunks +-- Process registered on generated chunks -------------------------------------- minetest.register_on_generated(function(minp, maxp, blockseed) local pos_hash = minetest.hash_node_position(minp) diff --git a/plan_manager.lua b/plan_manager.lua new file mode 100644 index 0000000..6e2510c --- /dev/null +++ b/plan_manager.lua @@ -0,0 +1,175 @@ +local save_interval = minetest.settings:get("schemlib.save_interval") or 10 +local save_maxnodes = minetest.settings:get("schemlib.save_maxnodes") or 10000 + +local storage = minetest.get_mod_storage() + +local plan_manager = {} +local plan_list = {} +local plan_meta_list = minetest.deserialize(storage:get_string("$PLAN_META_LIST$")) or {} + +-- light methods without access to scm_data_cache +local plan_meta_class = { + adjust_building_info = schemlib.plan.plan_class.adjust_building_info, + get_world_pos = schemlib.plan.plan_class.get_world_pos, + get_plan_pos = schemlib.plan.plan_class.get_plan_pos, + get_world_minp = schemlib.plan.plan_class.get_world_minp, + get_world_maxp = schemlib.plan.plan_class.get_world_maxp, + contains = schemlib.plan.plan_class.contains, + check_overlap = schemlib.plan.plan_class.check_overlap, +} +local plan_meta_class_mt = {__index = plan_meta_class} + +plan_manager.plan_meta_list = plan_meta_list + +----------------------------------------------- +-- Get separate stored plan metadata for plan +----------------------------------------------- +function plan_manager.get_plan_meta(plan_id) + if plan_meta_list[plan_id] then + local plan_meta = setmetatable({}, plan_meta_class_mt) + plan_meta.plan_id = plan_id + plan_meta.data = plan_meta_list[plan_id] + return plan_meta + end +end + +----------------------------------------------- +-- Get persistant plan +----------------------------------------------- +function plan_manager.get_plan(plan_id) + if plan_list[plan_id] then + return plan_list[plan_id] + end + + if not plan_meta_list[plan_id] then + return + end + + local plan = schemlib.plan.new(plan_id) + plan.data = plan_meta_list[plan_id] + + if not plan.data.save_chunk_count then + plan.scm_data_cache = minetest.deserialize(storage:get_string(plan_id)) + else + plan.scm_data_cache = {} + for i = 1, plan.data.save_chunk_count do + local chunk = minetest.deserialize(storage:get_string(plan_id.."$"..i)) + for y, ydata in pairs(chunk) do + plan.scm_data_cache[y] = ydata + end + end + end + + local nodecount = 0 + for y, ydata in pairs(plan.scm_data_cache) do + for x, xdata in pairs(ydata) do + for z, node in pairs(xdata) do + nodecount = nodecount + 1 + end + end + end + plan.data.nodecount = nodecount + plan_list[plan_id] = plan + return plan +end + +-------------------------------------- +-- Set/Save plan in plan manager +-------------------------------------- +function plan_manager.set_plan(plan) + plan_manager.delete_plan(plan.plan_id) + + plan_list[plan.plan_id] = plan + plan_meta_list[plan.plan_id] = plan.data + + if plan.data.nodecount <= 50000 then + plan.data.save_chunk_count = nil + storage:set_string(plan.plan_id, minetest.serialize(plan.scm_data_cache)) + else + -- Split data to avoid error main function has more than 65536 constants + local chunk = {} + local chunk_size = 0 + local chunk_count = 0 + for y, ydata in pairs(plan.scm_data_cache) do + chunk[y] = ydata + for x, xdata in pairs(ydata) do + for z, node in pairs(xdata) do + chunk_size = chunk_size + 1 + end + end + if chunk_size > 50000 then + chunk_count = chunk_count + 1 + storage:set_string(plan.plan_id.."$"..chunk_count, minetest.serialize(chunk)) + chunk = {} + chunk_size = 0 + end + end + if chunk_size > 0 then + chunk_count = chunk_count + 1 + storage:set_string(plan.plan_id.."$"..chunk_count, minetest.serialize(chunk)) + end + plan.data.save_chunk_count = chunk_count + end + plan.modified = false + storage:set_string("$PLAN_META_LIST$", minetest.serialize(plan_meta_list)) +end + +-------------------------------------- +-- Remove plan from manager +-------------------------------------- +function plan_manager.delete_plan(plan_id) + local plan_meta = plan_manager.get_plan_meta(plan_id) + if not plan_meta then + return + end + storage:set_string(plan_id, "") + if plan_meta.data.save_chunk_count then + for i = 1, plan_meta.data.save_chunk_count do + storage:set_string(plan_id.."$"..i,"") + end + end + plan_list[plan_id] = nil + plan_meta_list[plan_id] = nil + storage:set_string("$PLAN_META_LIST$", minetest.serialize(plan_meta_list)) +end + +-------------------------------------- +-- Do trigger processing +-------------------------------------- +for plan_id, data in pairs(plan_meta_list) do + if data.status == "build" then + local plan = plan_manager.get_plan(plan_id) + minetest.after(0, plan.do_add_all_voxel_async, plan) + end +end + +-------------------------------------- +-- Save all on shutdown +-------------------------------------- +minetest.register_on_shutdown(function() + for plan_id, plan in pairs(plan_list) do + if plan.modified then + plan_manager.set_plan(plan) + end + end +end) + +-------------------------------------- +-- Save in intervals +-------------------------------------- +local function save_chain() + for plan_id, plan in pairs(plan_list) do + if plan.modified and + (save_maxnodes == 0 or plan.data.nodecount <= save_maxnodes) then + plan_manager.set_plan(plan) + end + end + minetest.after(save_interval, save_chain) +end + +if save_interval > 0 then + minetest.after(save_interval, save_chain) +end + + +return plan_manager diff --git a/save_restore.lua b/save_restore.lua deleted file mode 100644 index 185fe93..0000000 --- a/save_restore.lua +++ /dev/null @@ -1,27 +0,0 @@ -local save_restore = {} - -function save_restore.save_data(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( minetest.get_worldpath()..'/'..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 - -return save_restore diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..c458cbf --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,9 @@ +# Save interval in seconds for plan changes in manager. +# 0 disables the interval saving +# Note: all modified plans will be saved on shutdown +schemlib.save_interval (Plan manager save interval) int 10 0 + +# Maximum building size in nodes that are handled in interval save +# to avoid performance issue. 0 enables saving for all sizes. +# Note: all (including big) modified plans will be saved on shutdown +schemlib.save_maxnodes (Maximum building size for autosave) int 10000 0