Big update around persistance

master
Alexander Weber 2018-07-30 22:27:09 +02:00
parent 284b2eba05
commit c43bc12e40
9 changed files with 647 additions and 403 deletions

102
README.md
View File

@ -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 - <schemlib_npc>: 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

View File

@ -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

View File

@ -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"

View File

@ -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
--------------------------------------

View File

@ -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)

678
plan.lua
View File

@ -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)

175
plan_manager.lua Normal file
View File

@ -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

View File

@ -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

9
settingtypes.txt Normal file
View File

@ -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