From 07fc95fe30d6fbcb8257dc86ff375bd0d0fec46e Mon Sep 17 00:00:00 2001 From: Alexander Weber Date: Sat, 15 Jul 2017 21:31:59 +0200 Subject: [PATCH] Port to schemlib / and schemlib_builder_npcf In last test the builder lost some nodes :-( --- README.md | 1 + chest.lua | 148 +---- depends.txt | 2 +- files.lua | 49 -- init.lua | 36 +- libs/hsl/analyze_mts_file.lua | 170 ----- libs/hsl/analyze_we_file.lua | 112 ---- libs/hsl/handle_schematics_meta.lua | 164 ----- libs/hsl/handle_schematics_misc.lua | 109 --- libs/hsl/init.lua | 28 - libs/hsl/rotate.lua | 118 ---- libs/hsl/save_restore.lua | 89 --- libs/hsl/worldedit_file.lua | 137 ---- mapping.lua | 250 ------- npcf-worker.lua | 488 +------------- plan.lua | 246 ------- smartfs-forms.lua | 26 +- smartfs.lua | 997 ++++++++++++++++------------ 18 files changed, 674 insertions(+), 2496 deletions(-) delete mode 100644 files.lua delete mode 100644 libs/hsl/analyze_mts_file.lua delete mode 100644 libs/hsl/analyze_we_file.lua delete mode 100644 libs/hsl/handle_schematics_meta.lua delete mode 100644 libs/hsl/handle_schematics_misc.lua delete mode 100644 libs/hsl/init.lua delete mode 100644 libs/hsl/rotate.lua delete mode 100644 libs/hsl/save_restore.lua delete mode 100644 libs/hsl/worldedit_file.lua delete mode 100644 mapping.lua delete mode 100644 plan.lua diff --git a/README.md b/README.md index 567a791..304cbff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # TownChest A minetest mod contains a chest with building definitions. The chest can spawn a NPC that does build the building for you. +The mod uses the schemlib framework to handle the buildings ![Screenshot](https://raw.github.com/bell07/minetest-townchest/master/screenshot.png) diff --git a/chest.lua b/chest.lua index 4d1ab63..e001603 100644 --- a/chest.lua +++ b/chest.lua @@ -1,4 +1,6 @@ +--local dprint = townchest.dprint local dprint = townchest.dprint_off --debug + local smartfs = townchest.smartfs local ASYNC_WAIT=0.2 -- schould be > 0 to restrict performance consumption @@ -93,8 +95,9 @@ townchest.chest.new = function() -- Create the task that should be managed by chest -------------------------------------- function self.set_rawdata(self, taskname) - - self.plan = townchest.plan.new(self) + self.plan = schemlib.plan.new(minetest.pos_to_string(self.pos), self.pos) + self.plan.chest = self + self.plan.on_status = townchest.npc.plan_update_hook if taskname then self.info.taskname = taskname @@ -109,10 +112,9 @@ townchest.chest.new = function() self:persist_info() return end + self.plan:read_from_schem_file(townchest.modpath.."/buildings/"..self.info.filename) - self.plan.data = townchest.files.readfile(self.info.filename) - - if not self.plan.data then + if self.plan.data.nodecount == 0 then self.infotext = "No building found in ".. self.info.filename self:set_form("status") self.current_stage = "select" @@ -123,37 +125,33 @@ townchest.chest.new = function() end elseif self.info.taskname == "generate" then - self.plan.data = {} - self.plan.data.min_pos = { x=1, y=1, z=1 } - self.plan.data.max_pos = { x=self.info.genblock.x, y=self.info.genblock.y, z=self.info.genblock.z} - self.plan.data.nodecount = 0 - self.plan.data.ground_y = 0 - self.plan.data.nodenames = {} - self.plan.data.scm_data_cache = {} - + -- set directly instead of counting each step + self.plan.data.min_pos = { x=1, y=1, z=1 } + self.plan.data.max_pos = { x=self.info.genblock.x, y=self.info.genblock.y, z=self.info.genblock.z} + self.plan.data.ground_y = 0 + local filler_node = {name = "default:cobble"} if self.info.genblock.variant == 1 then -- nothing special, just let fill them with air + elseif self.info.genblock.variant == 2 then - table.insert(self.plan.data.nodenames, "default:cobble") -- index 1 -- Fill with stone for x = 1, self.info.genblock.x do for y = 1, self.info.genblock.y do for z = 1, self.info.genblock.z do - self.plan:add_node({x=x,y=y,z=z, name_id = 1}) + self.plan:add_node({x=x,y=y,z=z}, schemlib.node.new(filler_node)) end end end elseif self.info.genblock.variant == 3 then -- Build a box - table.insert(self.plan.data.nodenames, "default:cobble") -- index 1 for x = 1, self.info.genblock.x do for y = 1, self.info.genblock.y do for z = 1, self.info.genblock.z do if x == 1 or x == self.info.genblock.x or y == 1 or y == self.info.genblock.y or z == 1 or z == self.info.genblock.z then - self.plan:add_node({x=x,y=y,z=z, name_id = 1}) + self.plan:add_node({x=x,y=y,z=z}, schemlib.node.new(filler_node)) end end end @@ -164,12 +162,11 @@ townchest.chest.new = function() -- Build a plate elseif self.info.genblock.variant == 4 then - table.insert(self.plan.data.nodenames, "default:cobble") -- index 1 local y = self.plan.data.min_pos.y self.plan.data.max_pos.y = self.plan.data.min_pos.y for x = 1, self.info.genblock.x do for z = 1, self.info.genblock.z do - self.plan:add_node({x=x,y=y,z=z, name_id = 1}) + self.plan:add_node({x=x,y=y,z=z}, schemlib.node.new(filler_node)) end end -- build ground level under chest @@ -199,7 +196,6 @@ townchest.chest.new = function() chest:run_async(func) end end - self:persist_info() minetest.after(ASYNC_WAIT, async_call, self.pos) end @@ -208,127 +204,55 @@ townchest.chest.new = function() -- Async task: Post-processing of plan preparation -------------------------------------- function self.prepare_building_plan(self) - self.plan:flood_with_air() - - -- self.plan:do_mapping() -- on demand called + self.plan:apply_flood_with_air(3, 0, 5) --(add_max, add_min, add_top) + self.plan:del_node(self.plan:get_plan_pos(self.pos)) -- Do not override the chest node self.current_stage = "ready" self:set_form("build_status") - self:run_async(self.instant_build_chunk) --just trigger, there is a check if active + if self.info.npc_build == true then + self.plan:set_status("build") + townchest.npc.enable_build(self.plan) + end + if self.info.instantbuild == true then + self.plan:set_status("build") + self:run_async(self.instant_build_chunk) + end end -------------------------------------- -- Async Task: Do a instant build step -------------------------------------- function self.instant_build_chunk(self) + dprint("chunk processing called", self.info.instantbuild) if not self.info.instantbuild == true then --instantbuild disabled return end dprint("--- Instant build is running") - local startingpos = self.plan:get_random_node_pos() - if not startingpos then + local random_pos = self.plan:get_random_plan_pos() + if not random_pos then self.info.instantbuild = false return false end - local chunk_pos = self.plan:get_world_pos(startingpos) - dprint("---build chunk", minetest.pos_to_string(startingpos)) - --- TODO: in customizing switchable implementation ---[[ --- implementation with VoxelArea - bad gameplay responsivity :( - back to per-node update - -- work on VoxelArea - local vm = minetest.get_voxel_manip() - local minp, maxp = vm:read_from_map(chunk_pos, chunk_pos) - local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp}) - local data = vm:get_data() - local param2_data = vm:get_param2_data() - local light_fix = {} - local meta_fix = {} --- for idx in a:iterp(vector.add(minp, 8), vector.subtract(maxp, 8)) do -- do not touch for beter light update - for idx, origdata in pairs(data) do -- do not touch for beter light update - local wpos = a:position(idx) - local pos = self.plan:get_plan_pos(wpos) - if wpos.x ~= self.pos.x or wpos.y ~= self.pos.y or wpos.z ~= self.pos.z then --skip chest pos - local node = self.plan:prepare_node_for_build(pos, wpos) - if node and node.content_id then - -- write to voxel - data[idx] = node.content_id - param2_data[idx] = node.param2 - - -- mark for light update - assert(node.node_def, dump(node)) - if node.node_def.light_source and node.node_def.light_source > 0 then - table.insert(light_fix, {pos = wpos, node = node}) - end - if node.meta then - table.insert(meta_fix, {pos = wpos, node = node}) - end - self.plan:remove_node(node) - --TODO: metadata - end - end - self.plan:remove_node(pos) --if exists - end - - -- store the changed map data - vm:set_data(data) - vm:set_param2_data(param2_data) - vm:calc_lighting() - vm:update_liquids() - vm:write_to_map() - vm:update_map() - - -- fix the lights - dprint("fix lights", #light_fix) - for _, fix in ipairs(light_fix) do - minetest.env:add_node(fix.pos, fix.node) - end - - dprint("process meta", #meta_fix) - for _, fix in ipairs(meta_fix) do - minetest.env:get_meta(fix.pos):from_table(fix.node.meta) - end -]] - - -- implementation using usual "add_node" - local chunk_nodes = self.plan:get_nodes_for_chunk(self.plan:get_plan_pos(chunk_pos)) - dprint("Instant build of chunk: nodes:", #chunk_nodes) - for idx, nodeplan in ipairs(chunk_nodes) do - if nodeplan.wpos.x ~= self.pos.x or nodeplan.wpos.y ~= self.pos.y or nodeplan.wpos.z ~= self.pos.z then --skip chest pos - if nodeplan.node then - minetest.env:add_node(nodeplan.wpos, nodeplan.node) - if nodeplan.node.meta then - minetest.env:get_meta(nodeplan.wpos):from_table(nodeplan.node.meta) - end - end - end - self.plan:remove_node(nodeplan.pos) - end + dprint("---build chunk", minetest.pos_to_string(random_pos)) +-- self.plan:do_add_chunk(random_pos) + self.plan:do_add_chunk_voxel(random_pos) -- chunk done handle next chunk call dprint("instant nodes left:", self.plan.data.nodecount) - self:update_info("build_status") - if self.plan.data.nodecount > 0 then + if self.plan:get_status() == "build" then + self:update_info("build_status") --start next plan chain return true else -- finished. disable processing + self.info.npc_build = false self.info.instantbuild = false + self:update_info("build_status") return false end end - -------------------------------------- - -- Check if the chest is ready to build something - -------------------------------------- - function self.npc_build_allowed(self) - if self.current_stage ~= "ready" then - return false - else - return self.info.npc_build - end - end - function self.update_statistics(self) if self.current_stage == "ready" then --update building status in case of ready (or build in process after ready) self:update_info("build_status") diff --git a/depends.txt b/depends.txt index 9454ee8..2a74d44 100644 --- a/depends.txt +++ b/depends.txt @@ -1,2 +1,2 @@ default -npcf +schemlib_builder_npcf? diff --git a/files.lua b/files.lua deleted file mode 100644 index 05e6ba1..0000000 --- a/files.lua +++ /dev/null @@ -1,49 +0,0 @@ -local dprint = townchest.dprint_off --debug - -files = {} - ------------------------------------------------ --- get files --- no input parameters --- returns a table containing buildings ------------------------------------------------ -function files.get() - local files = minetest.get_dir_list(townchest.modpath..'/buildings/', false) or {} - local i, t = 0, {} - for _,filename in ipairs(files) do - if filename ~= "." and filename ~= ".." then - i = i + 1 - t[i] = filename - end - end - - table.sort(t,function(a,b) return a= 3 ) then - -- the probability is not very intresting for buildings so we just skip it - file:read( size.y ) - end - - -- this list is not yet used for anything - local nodenames = {} - local ground_id = {} - local is_air = 0 - - -- after that: read_s16 (2 bytes) to find out how many diffrent nodenames (node_name_count) are present in the file - local node_name_count = read_s16( file ) - - for i = 1, node_name_count do - -- the length of the next name - local name_length = read_s16( file ) - -- the text of the next name - local name_text = file:read( name_length ) - nodenames[i] = name_text - if string.sub(name_text, 1, 18) == "default:dirt_with_" or - name_text == "farming:soil_wet" then - ground_id[i] = true - elseif( name_text == 'air' ) then - is_air = i; - end - end - - -- decompression was recently added; if it is not yet present, we need to use normal place_schematic - if( minetest.decompress == nil) then - file.close(file); - return nil; -- normal place_schematic is no longer supported as minetest.decompress is now part of the release version of minetest - end - - local compressed_data = file:read( "*all" ); - local data_string = minetest.decompress(compressed_data, "deflate" ); - file.close(file) - - local p2offset = (size.x*size.y*size.z)*3; - local i = 1; - - local scm = {}; - local min_pos = {} - local max_pos = {} - local nodecount = 0 - local ground_y = -1 --if nothing defined, it is under the building - local groundnode_count = 0 - - for z = 1, size.z do - for y = 1, size.y do - for x = 1, size.x do - local id = string.byte( data_string, i ) * 256 + string.byte( data_string, i+1 ); - i = i + 2; - local p2 = string.byte( data_string, p2offset + math.floor(i/2)); - id = id+1; - if( id ~= is_air ) then - -- use node - if( not( scm[y] )) then - scm[y] = {}; - end - if( not( scm[y][x] )) then - scm[y][x] = {}; - end - scm[y][x][z] = {name_id = id, param2 = p2}; - nodecount = nodecount + 1 - - -- adjust position information - if not max_pos.x or x > max_pos.x then - max_pos.x = x - end - if not max_pos.y or y > max_pos.y then - max_pos.y = y - end - if not max_pos.z or z > max_pos.z then - max_pos.z = z - end - if not min_pos.x or x < min_pos.x then - min_pos.x = x - end - if not min_pos.y or y < min_pos.y then - min_pos.y = y - end - if not min_pos.z or z < min_pos.z then - min_pos.z = z - end - - -- calculate ground_y value - if ground_id[id] then - groundnode_count = groundnode_count + 1 - if groundnode_count == 1 then - ground_y = y - else - ground_y = ground_y + (y - ground_y) / groundnode_count - end - end - end - end - end - end - - return { min_pos = min_pos, -- minimal {x,y,z} vector - max_pos = max_pos, -- maximal {x,y,z} vector - nodenames = nodenames, -- nodenames[1] = "default:sample" - scm_data_cache = scm, -- scm[y][x][z] = { name_id, ent.param2 } - nodecount = nodecount, -- integer, count - ground_y = ground_y } -- average ground high -end - -return handle_schematics diff --git a/libs/hsl/analyze_we_file.lua b/libs/hsl/analyze_we_file.lua deleted file mode 100644 index f3b8c4c..0000000 --- a/libs/hsl/analyze_we_file.lua +++ /dev/null @@ -1,112 +0,0 @@ -local handle_schematics = {} - --- receive parameter modpath -local modpath = ... - --- deserialize worldedit savefiles -local worldedit_file = dofile(modpath.."worldedit_file.lua") - - -handle_schematics.analyze_we_file = function(file) - -- returning parameters - local nodenames = {} - local scm = {} - local all_meta = {} - local min_pos = {} - local max_pos = {} - local ground_y = -1 --if nothing defined, it is under the building - local nodecount = 0 - - -- helper - local nodes = worldedit_file.load_schematic(file:read("*a")) - local nodenames_id = {} - local ground_id = {} - local groundnode_count = 0 - - -- analyze the file - for i, ent in ipairs( nodes ) do - -- get nodename_id and analyze ground elements - local name_id = nodenames_id[ent.name] - if not name_id then - name_id = #nodenames + 1 - nodenames_id[ent.name] = name_id - nodenames[name_id] = ent.name - if string.sub(ent.name, 1, 18) == "default:dirt_with_" or - ent.name == "farming:soil_wet" then - ground_id[name_id] = true - end - end - - -- calculate ground_y value - if ground_id[name_id] then - groundnode_count = groundnode_count + 1 - if groundnode_count == 1 then - ground_y = ent.y - else - ground_y = ground_y + (ent.y - ground_y) / groundnode_count - end - end - - -- adjust position information - if not max_pos.x or ent.x > max_pos.x then - max_pos.x = ent.x - end - if not max_pos.y or ent.y > max_pos.y then - max_pos.y = ent.y - end - if not max_pos.z or ent.z > max_pos.z then - max_pos.z = ent.z - end - if not min_pos.x or ent.x < min_pos.x then - min_pos.x = ent.x - end - if not min_pos.y or ent.y < min_pos.y then - min_pos.y = ent.y - end - if not min_pos.z or ent.z < min_pos.z then - min_pos.z = ent.z - end - - -- build to scm data tree - if scm[ent.y] == nil then - scm[ent.y] = {} - end - if scm[ent.y][ent.x] == nil then - scm[ent.y][ent.x] = {} - end - if ent.param2 == nil then - ent.param2 = 0 - end - - -- metadata is only of intrest if it is not empty - if( ent.meta and (ent.meta.fields or ent.meta.inventory)) then - local has_meta = false - for _,v in pairs( ent.meta.fields ) do - has_meta = true - break - end - for _,v in pairs(ent.meta.inventory) do - has_meta = true - break - end - if has_meta ~= true then - ent.meta = nil - end - else - ent.meta = nil - end - - scm[ent.y][ent.x][ent.z] = {name_id = name_id, param2 = ent.param2, meta = ent.meta} - - nodecount = nodecount + 1 - end - - return { min_pos = min_pos, -- minimal {x,y,z} vector - max_pos = max_pos, -- maximal {x,y,z} vector - nodenames = nodenames, -- nodenames[1] = "default:sample" - scm_data_cache = scm, -- scm[y][x][z] = { name_id=, param2=, meta= } - nodecount = nodecount, -- integer, count - ground_y = ground_y } -- average ground high -end - -return handle_schematics diff --git a/libs/hsl/handle_schematics_meta.lua b/libs/hsl/handle_schematics_meta.lua deleted file mode 100644 index f91b1d7..0000000 --- a/libs/hsl/handle_schematics_meta.lua +++ /dev/null @@ -1,164 +0,0 @@ -local handle_schematics = {} - -handle_schematics.sort_pos_get_size = function( p1, p2 ) - local res = {x=p1.x, y=p1.y, z=p1.z, - sizex = math.abs( p1.x - p2.x )+1, - sizey = math.abs( p1.y - p2.y )+1, - sizez = math.abs( p1.z - p2.z )+1}; - if( p2.x < p1.x ) then - res.x = p2.x; - end - if( p2.y < p1.y ) then - res.y = p2.y; - end - if( p2.z < p1.z ) then - res.z = p2.z; - end - return res; -end - - - -local handle_schematics_get_meta_table = function( pos, all_meta, start_pos ) - local m = minetest.get_meta( pos ):to_table(); - local empty_meta = true; - - -- the inventory part contains functions and cannot be fed to minetest.serialize directly - local invlist = {}; - local count_inv = 0; - local inv_is_empty = true; - for name, list in pairs( m.inventory ) do - invlist[ name ] = {}; - count_inv = count_inv + 1; - for i, stack in ipairs(list) do - if( not( stack:is_empty())) then - invlist[ name ][ i ] = stack:to_string(); - empty_meta = false; - inv_is_empty = false; - end - end - end - -- the fields part at least is unproblematic - local count_fields = 0; - if( empty_meta and m.fields ) then - for k,v in pairs( m.fields ) do - empty_meta = false; - count_fields = count_fields + 1; - end - end - - -- ignore default:sign_wall without text on it - if( count_inv==0 - and count_fields<=3 and m.fields.formspec and m.fields.infotext - and m.fields.formspec == "field[text;;${text}]" - and m.fields.infotext == "\"\"") then - -- also consider signs empty if their text has been set once and deleted afterwards - if( not( m.fields.text ) or m.fields.text == "" ) then -print('SKIPPING empty sign AT '..minetest.pos_to_string( pos)..' while saving metadata.'); - empty_meta = true; - end - - elseif( count_inv > 0 and inv_is_empty - and count_fields>0 and m.fields.formspec ) then - - local n = minetest.get_node( pos ); - if( n and n.name - and (n.name=='default:chest' or n.name=='default:chest_locked' or n.name=='default:bookshelf' - or n.name=='default:furnace' or n.name=='default:furnace_active' - or n.name=='cottages:shelf' or n.name=='cottages:anvil' or n.name=='cottages:threshing_floor' )) then -print('SKIPPING empty '..tostring(n.name)..' AT '..minetest.pos_to_string( pos )..' while saving metadata.'); - empty_meta = true; - end - end - - - -- only save if there is something to be saved - if( not( empty_meta )) then - -- positions are stored as relative positions - all_meta[ #all_meta+1 ] = { - x=pos.x-start_pos.x, - y=pos.y-start_pos.y, - z=pos.z-start_pos.z, - fields = m.fields, - inventory = invlist}; - end -end - --- reads metadata values from start_pos to end_pos and stores them in a file -handle_schematics.save_meta = function( start_pos, end_pos, filename ) - local all_meta = {}; - local p = handle_schematics.sort_pos_get_size( start_pos, end_pos ); - - if( minetest.find_nodes_with_meta ) then - for _,pos in ipairs( minetest.find_nodes_with_meta( start_pos, end_pos )) do - handle_schematics_get_meta_table( pos, all_meta, p ); - end - else - for x=p.x, p.x+p.sizex do - for y=p.y, p.y+p.sizey do - for z=p.z, p.z+p.sizez do - handle_schematics_get_meta_table( {x=x-p.x, y=y-p.y, z=z-p.z}, all_meta, p ); - end - end - end - end - - if( #all_meta > 0 ) then - save_restore.save_data( 'schems/'..filename..'.meta', all_meta ); - end -end - --- all metadata values will be deleted when this function is called, --- making the area ready for new voxelmanip/schematic data -handle_schematics.clear_meta = function( start_pos, end_pos ) - local empty_meta = { inventory = {}, fields = {} }; - - if( minetest.find_nodes_with_meta ) then - for _,pos in ipairs( minetest.find_nodes_with_meta( start_pos, end_pos )) do - local meta = minetest.get_meta( pos ); - meta:from_table( empty_meta ); - end - end -end - - --- restore metadata from file --- TODO: use relative instead of absolute positions (already done for .we files) --- TODO: handle mirror -handle_schematics.restore_meta = function( filename, all_meta, start_pos, end_pos, rotate, mirror ) - - if( not( all_meta ) and filename ) then - all_meta = save_restore.restore_data( filename..'.meta' ); - end - for _,pos in ipairs( all_meta ) do - local p = {}; - if( rotate == 0 ) then - p = {x=start_pos.x+pos.x-1, y=start_pos.y+pos.y-1, z=start_pos.z+pos.z-1}; - elseif( rotate == 1 ) then - p = {x=start_pos.x+pos.z-1, y=start_pos.y+pos.y-1, z=end_pos.z -pos.x+1}; - elseif( rotate == 2 ) then - p = {x=end_pos.x -pos.x+1, y=start_pos.y+pos.y-1, z=end_pos.z -pos.z+1}; - elseif( rotate == 3 ) then - p = {x=end_pos.x -pos.z+1, y=start_pos.y+pos.y-1, z=start_pos.z+pos.x-1}; - end - local meta = minetest.get_meta( p ); - meta:from_table( {inventory = pos.inventory, fields = pos.fields }); - end -end - - --- return true on success; will overwrite existing files -handle_schematics.create_schematic_with_meta = function( p1, p2, base_filename ) - - -- create directory for the schematics (same path as WorldEdit uses) - save_restore.create_schems_directory(); - local complete_filename = minetest.get_worldpath()..'/schems/'..base_filename..'.mts'; - -- actually create the schematic - minetest.create_schematic( p1, p2, nil, complete_filename, nil); - -- save metadata; the file will only be created if there is any metadata that is to be saved - handle_schematics.save_meta( p1, p2, base_filename ); - - return save_restore.file_exists( complete_filename ); -end - -return handle_schematics diff --git a/libs/hsl/handle_schematics_misc.lua b/libs/hsl/handle_schematics_misc.lua deleted file mode 100644 index 2213008..0000000 --- a/libs/hsl/handle_schematics_misc.lua +++ /dev/null @@ -1,109 +0,0 @@ -local handle_schematics = {} - --- helper function; sorts by the second element of the table -local function handle_schematics_comp(a,b) - if (a[2] > b[2]) then - return true; - end -end - --- create a statistic about how frequent each node name occoured -handle_schematics.count_nodes = function( data ) - local statistic = {}; - -- make sure all node names are counted (air may sometimes be included without occouring) - for id=1, #data.nodenames do - statistic[ id ] = { id, 0}; - end - - for z = 1, data.size.z do - for y = 1, data.size.y do - for x = 1, data.size.x do - - local a = data.scm_data_cache[y][x][z]; - if( a ) then - local id = 0; - if( type( a )=='table' ) then - id = a[1]; - else - id = a; - end - if( statistic[ id ] and statistic[ id ][ 2 ] ) then - statistic[ id ] = { id, statistic[ id ][ 2 ]+1 }; - end - end - end - end - end - table.sort( statistic, handle_schematics_comp ); - return statistic; -end - - --- this function makes sure that the building will always extend to the right and in front of the build chest -handle_schematics.translate_param2_to_rotation = function( param2, mirror, start_pos, orig_max, rotated, burried, orients, yoff ) - - -- mg_villages stores available rotations of buildings in orients={0,1,2,3] format - if( orients and #orients and orients[1]~=0) then - -- reset rotated - else we'd apply it twice - rotated = 0; - if( orients[1]==1 ) then - rotated = rotated + 90; - elseif( orients[1]==2 ) then - rotated = rotated + 180; - elseif( orients[1]==3 ) then - rotated = rotated + 270; - end - if( rotated >= 360 ) then - rotated = rotated % 360; - end - end - - local max = {x=orig_max.x, y=orig_max.y, z=orig_max.z}; - -- if the schematic has been saved in a rotated way, swapping x and z may be necessary - if( rotated==90 or rotated==270) then - max.x = orig_max.z; - max.z = orig_max.x; - end - - -- the building may have a cellar or something alike - if( burried and burried ~= 0 and yoff == nil ) then - start_pos.y = start_pos.y - burried; - end - - -- make sure the building always extends forward and to the right of the player - local rotate = 0; - if( param2 == 0 ) then rotate = 270; if( mirror==1 ) then start_pos.x = start_pos.x - max.x + max.z; end -- z gets larger - elseif( param2 == 1 ) then rotate = 0; start_pos.z = start_pos.z - max.z; -- x gets larger - elseif( param2 == 2 ) then rotate = 90; start_pos.z = start_pos.z - max.x; - if( mirror==0 ) then start_pos.x = start_pos.x - max.z; -- z gets smaller - else start_pos.x = start_pos.x - max.x; end - elseif( param2 == 3 ) then rotate = 180; start_pos.x = start_pos.x - max.x; -- x gets smaller - end - - if( param2 == 1 or param2 == 0) then - start_pos.z = start_pos.z + 1; - elseif( param2 == 1 or param2 == 2 ) then - start_pos.x = start_pos.x + 1; - end - if( param2 == 1 ) then - start_pos.x = start_pos.x + 1; - end - - rotate = rotate + rotated; - -- make sure the rotation does not reach or exceed 360 degree - if( rotate >= 360 ) then - rotate = rotate - 360; - end - -- rotate dimensions when needed - if( param2==0 or param2==2) then - local tmp = max.x; - max.x = max.z; - max.z = tmp; - end - - return { rotate=rotate, start_pos = {x=start_pos.x, y=start_pos.y, z=start_pos.z}, - end_pos = {x=(start_pos.x+max.x-1), y=(start_pos.y+max.y-1), z=(start_pos.z+max.z-1) }, - max = {x=max.x, y=max.y, z=max.z}}; -end - -return handle_schematics diff --git a/libs/hsl/init.lua b/libs/hsl/init.lua deleted file mode 100644 index c6eb9c8..0000000 --- a/libs/hsl/init.lua +++ /dev/null @@ -1,28 +0,0 @@ ---[[ handle_schematics library - extracted from https://github.com/Sokomine/handle_schematics - see https://github.com/Sokomine/handle_schematics/issues/7 -]] - -local hsl = {} - --- temporary path assignment till the hsl is own mod -local modpath = minetest.get_modpath(minetest.get_current_modname()) -modpath = modpath..'/libs/hsl/' - --- reads and analyzes .mts files (minetest schematics) -hsl.analyze_mts = dofile(modpath.."/analyze_mts_file.lua") - --- reads and analyzes worldedit files -hsl.analyze_we = assert(loadfile(modpath.."/analyze_we_file.lua"))(modpath) - --- handles rotation and mirroring -hsl.rotate = dofile(modpath.."/rotate.lua") - --- count nodes, take param2 into account for rotation etc. -hsl.misc = dofile(modpath.."/handle_schematics_misc.lua") - --- store and restore metadata -hsl.save_restore = dofile(modpath.."/save_restore.lua"); -hsl.meta = dofile(modpath.."/handle_schematics_meta.lua"); - -return hsl diff --git a/libs/hsl/rotate.lua b/libs/hsl/rotate.lua deleted file mode 100644 index 0786131..0000000 --- a/libs/hsl/rotate.lua +++ /dev/null @@ -1,118 +0,0 @@ -local handle_schematics = {} - -local rotate_facedir = function(facedir) - return ({1, 2, 3, 0, - 13, 14, 15, 12, - 17, 18, 19, 16, - 9, 10, 11, 8, - 5, 6, 7, 4, - 21, 22, 23, 20})[facedir+1] -end - - --- accessd through handle_schematics.mirror_facedir[ (rotation%2)+1 ][ facedir+1 ] -handle_schematics.mirror_facedir = - {{ 2, 1, 0, 3, -- 0, 1, 2, 3 - 8, 9, 10, 11, -- 4, 5, 6, 7 - 4, 5, 6, 7, -- 8, 9,10,11 - 12, 13, 14, 15, --12,13,14,15 - 16, 17, 18, 19, --16,17,18,19 - 22, 21, 20, 23 --20,21,22,23 - }, - { 0, 3, 2, 1, -- 0, 1, 2, 3 - 4, 7, 6, 5, -- 4, 5, 6, 7 - 8, 9, 10, 11, -- 8, 9,10,11 - 16, 17, 18, 19, --12,13,14,15 - 12, 15, 14, 13, --16,17,18,19 - 20, 23, 22, 21 --20,21,22,23 - }}; - -local rotate_wallmounted = function(wallmounted) - return ({0, 1, 5, 4, 2, 3})[wallmounted+1] -end - -handle_schematics.get_param2_rotated = function( paramtype2, p2 ) - local p2r = {}; - p2r[ 1 ] = p2; - if( paramtype2 == 'wallmounted' ) then - for i = 2,4 do - p2r[ i ] = rotate_wallmounted( p2r[ i-1 ]); - end - elseif( paramtype2 == 'facedir' ) then - for i = 2,4 do - p2r[ i ] = rotate_facedir( p2r[ i-1 ]); - end - p2r[5]=1; -- indicate that it is wallmounted - else - return { p2, p2, p2, p2 }; - end - return p2r; -end - - -handle_schematics.mirrored_node = {}; - -handle_schematics.add_mirrored_node_type = function( name, mirrored_name ) - handle_schematics.mirrored_node[ name ] = mirrored_name; - local id = minetest.get_content_id( name ); - local id_mi = minetest.get_content_id( mirrored_name ); - local c_ignore = minetest.get_content_id( 'ignore' ); - if( id and id_mi and id ~= c_ignore and id_mi ~= c_ignore ) then - handle_schematics.mirrored_node[ id ] = id_mi; - end -end - -local door_materials = {'wood','steel','glass','obsidian_glass'}; -for _,material in ipairs( door_materials ) do - handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_b_1', 'doors:door_'..material..'_b_2' ); - handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_t_1', 'doors:door_'..material..'_t_2' ); - handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_b_2', 'doors:door_'..material..'_b_1' ); - handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_t_2', 'doors:door_'..material..'_t_1' ); -end - - - - -handle_schematics.rotation_table = {}; -handle_schematics.rotation_table[ 'facedir' ] = {}; -handle_schematics.rotation_table[ 'wallmounted' ] = {}; - - -for paramtype2,v in pairs( handle_schematics.rotation_table ) do - for param2 = 0,23 do - - if( param2 < 6 or paramtype2 == 'facedir' ) then - local param2list = handle_schematics.get_param2_rotated( paramtype2, param2); - - handle_schematics.rotation_table[ paramtype2 ][ param2+1 ] = {}; - - for rotation = 0,3 do - local np2 = param2list[ rotation + 1]; - local mirror_x = np2; - local mirror_z = np2; - - -- mirror_x - if( #param2list==5) then - mirror_x = handle_schematics.mirror_facedir[ (( rotation +1)%2)+1 ][ np2+1 ]; - elseif( #param2list<5 - and (( rotation%2==1 and (np2==4 or np2==5)) - or ( rotation%2==0 and (np2==2 or np2==3)))) then - mirror_x = param2list[ ( rotation + 2)%4 +1]; - end - - -- mirror_z - if( #param2list==5) then - mirror_z = handle_schematics.mirror_facedir[ (rotation %2)+1 ][ np2+1 ]; - elseif( #param2list<5 - and (( rotation%2==0 and (np2==4 or np2==5)) - or ( rotation%2==1 and (np2==2 or np2==3)))) then - mirror_z = param2list[ ( rotation + 2)%4 +1]; - end - - handle_schematics.rotation_table[ paramtype2 ][ param2+1 ][ rotation+1 ] = { np2, mirror_x, mirror_z }; - end - end - end -end - -return handle_schematics diff --git a/libs/hsl/save_restore.lua b/libs/hsl/save_restore.lua deleted file mode 100644 index 9b1d3ec..0000000 --- a/libs/hsl/save_restore.lua +++ /dev/null @@ -1,89 +0,0 @@ - --- reserve the namespace -local save_restore = {} - --- TODO: if this gets more versatile, add sanity checks for filename --- TODO: apart from allowing filenames, schems/ also needs to be allowed - --- TODO: save and restore ought to be library functions and not implemented in each individual mod! -save_restore.save_data = function( filename, data ) - - local path = minetest.get_worldpath()..'/'..filename; - - local file = io.open( path, 'w' ); - if( file ) then - file:write( minetest.serialize( data )); - file:close(); - else - print("[save_restore] Error: Savefile '"..tostring( path ).."' could not be written."); - end -end - - -save_restore.restore_data = function( filename ) - local file = io.open( filename, 'r' ); - if( file ) then - local data = file:read("*all"); - file:close(); - return minetest.deserialize( data ); - else - print("[save_restore] Error: Savefile '"..tostring( filename ).."' not found."); - return {}; -- return empty table - end -end - - - -save_restore.file_exists = function( filename ) - - local file = save_restore.file_access( filename, 'r' ); - if( file ) then - file:close(); - return true; - end - return; -end - - -save_restore.create_schems_directory = function() - local directory = minetest.get_worldpath()..'/schems'; - if( not( save_restore.file_exists( directory ))) then - if( minetest.mkdir ) then - minetest.mkdir( directory ); - else - os.execute("mkdir \""..directory.. "\""); - end - end -end - - --- we only need the function io.open in a version that can read schematic files from diffrent places, --- even if a secure environment is enforced; this does require an exception for the mod -local ie_io_open = io.open; -if( minetest.request_insecure_environment ) then - local ie, req_ie = _G, minetest.request_insecure_environment - if req_ie then ie = req_ie() end - if ie then - ie_io_open = ie.io.open; - end -end - - --- only a certain type of files can be read and written -save_restore.file_access = function( path, params ) - if( (params=='r' or params=='rb') - and ( string.find( path, '.mts', -4 ) - or string.find( path, '.schematic', -11 ) - or string.find( path, '.we', -3 ) - or string.find( path, '.wem', -4 ) )) then - return ie_io_open( path, params ); - elseif( (params=='w' or params=='wb') - and ( string.find( path, '.mts', -4 ) - or string.find( path, '.schematic', -11 ) - or string.find( path, '.we', -3 ) - or string.find( path, '.wem', -4 ) )) then - return ie_io_open( path, params ); - end -end - -return save_restore diff --git a/libs/hsl/worldedit_file.lua b/libs/hsl/worldedit_file.lua deleted file mode 100644 index a9660b0..0000000 --- a/libs/hsl/worldedit_file.lua +++ /dev/null @@ -1,137 +0,0 @@ ------------------------------------------------------------------------------------------- --- This is the file --- https://github.com/Uberi/Minetest-WorldEdit/blob/master/worldedit/serialization.lua --- Changes: --- * worldedit namespace renamed to worldeit_file --- * eliminiated functions that are not needed --- * made function load_schematic non-local --- * originx, originy and originz are now passed as parameters to worldedit_file.load_schematic; --- they are required for an old file format ------------------------------------------------------------------------------------------- - -local worldedit_file = {} -- add the namespace - ---- Schematic serialization and deserialiation. --- @module worldedit.serialization - -worldedit_file.LATEST_SERIALIZATION_VERSION = 5 -local LATEST_SERIALIZATION_HEADER = worldedit_file.LATEST_SERIALIZATION_VERSION .. ":" - - ---[[ -Serialization version history: - 1: Original format. Serialized Lua table with a weird linked format... - 2: Position and node seperated into sub-tables in fields `1` and `2`. - 3: List of nodes, one per line, with fields seperated by spaces. - Format: - 4: Serialized Lua table containing a list of nodes with `x`, `y`, `z`, - `name`, `param1`, `param2`, and `meta` fields. - 5: Added header and made `param1`, `param2`, and `meta` fields optional. - Header format: ,,...: ---]] - - ---- Reads the header of serialized data. --- @param value Serialized WorldEdit data. --- @return The version as a positive natural number, or 0 for unknown versions. --- @return Extra header fields as a list of strings, or nil if not supported. --- @return Content (data after header). -function worldedit_file.read_header(value) - if value:find("^[0-9]+[%-:]") then - local header_end = value:find(":", 1, true) - local header = value:sub(1, header_end - 1):split(",") - local version = tonumber(header[1]) - table.remove(header, 1) - local content = value:sub(header_end + 1) - return version, header, content - end - -- Old versions that didn't include a header with a version number - if value:find("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)") and not value:find("%{") then -- List format - return 3, nil, value - elseif value:find("^[^\"']+%{%d+%}") then - if value:find("%[\"meta\"%]") then -- Meta flat table format - return 2, nil, value - end - return 1, nil, value -- Flat table format - elseif value:find("%{") then -- Raw nested table format - return 4, nil, value - end - return nil -end - - ---- Loads the schematic in `value` into a node list in the latest format. --- Contains code based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile) --- by ChillCode, available under the MIT license. --- @return A node list in the latest format, or nil on failure. -function worldedit_file.load_schematic(value, we_origin) - local version, header, content = worldedit_file.read_header(value) - local nodes = {} - if version == 1 or version == 2 then -- Original flat table format - local tables = minetest.deserialize(content) - if not tables then return nil end - - -- Transform the node table into an array of nodes - for i = 1, #tables do - for j, v in pairs(tables[i]) do - if type(v) == "table" then - tables[i][j] = tables[v[1]] - end - end - end - nodes = tables[1] - - if version == 1 then --original flat table format - for i, entry in ipairs(nodes) do - local pos = entry[1] - entry.x, entry.y, entry.z = pos.x, pos.y, pos.z - entry[1] = nil - local node = entry[2] - entry.name, entry.param1, entry.param2 = node.name, node.param1, node.param2 - entry[2] = nil - end - end - elseif version == 3 then -- List format - for x, y, z, name, param1, param2 in content:gmatch( - "([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)%s+" .. - "([^%s]+)%s+(%d+)%s+(%d+)[^\r\n]*[\r\n]*") do - param1, param2 = tonumber(param1), tonumber(param2) - table.insert(nodes, { - x = tonumber(x), - y = tonumber(y), - z = tonumber(z), - name = name, - param1 = param1 ~= 0 and param1 or nil, - param2 = param2 ~= 0 and param2 or nil, - }) - end - elseif version == 4 or version == 5 then -- Nested table format - if not jit then - -- This is broken for larger tables in the current version of LuaJIT - nodes = minetest.deserialize(content) - else - -- XXX: This is a filthy hack that works surprisingly well - in LuaJIT, `minetest.deserialize` will fail due to the register limit - nodes = {} - content = content:gsub("return%s*{", "", 1):gsub("}%s*$", "", 1) -- remove the starting and ending values to leave only the node data - local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end) - local startpos, startpos1, endpos = 1, 1 - while true do -- go through each individual node entry (except the last) - startpos, endpos = escaped:find("},%s*{", startpos) - if not startpos then - break - end - local current = content:sub(startpos1, startpos) - local entry = minetest.deserialize("return " .. current) - table.insert(nodes, entry) - startpos, startpos1 = endpos, endpos - end - local entry = minetest.deserialize("return " .. content:sub(startpos1)) -- process the last entry - table.insert(nodes, entry) - end - else - return nil - end - return nodes -end - -return worldedit_file diff --git a/mapping.lua b/mapping.lua deleted file mode 100644 index 57236c1..0000000 --- a/mapping.lua +++ /dev/null @@ -1,250 +0,0 @@ -local dprint = townchest.dprint_off --debug ---local dprint = townchest.dprint - - -local mapping = {} -townchest.mapping = mapping - --- visual for cost_item free for payment -mapping.c_free_item = "default:cloud" - - ------------------------------------------------ --- door compatibility. Seems the old doors was facedir and now the wallmounted values should be used ------------------------------------------------ -local function __param2_wallmounted_to_facedir(nodeinfo, pos, wpos) - if nodeinfo.param2 == 0 then -- +y? - nodeinfo.param2 = 0 - elseif nodeinfo.param2 == 1 then -- -y? - nodeinfo.param2 = 1 - elseif nodeinfo.param2 == 2 then --unsure - nodeinfo.param2 = 3 - elseif nodeinfo.param2 == 3 then --unsure - nodeinfo.param2 = 1 - elseif nodeinfo.param2 == 4 then --unsure - nodeinfo.param2 = 2 - elseif nodeinfo.param2 == 5 then --unsure - nodeinfo.param2 = 0 - end -end - -local u = {} -local unknown_nodes_data = u --- Fallback nodes replacement of unknown nodes --- Maybe it is beter to use aliases for unknown notes. But anyway -u["xpanes:pane_glass_10"] = { name = "xpanes:pane_10" } -u["xpanes:pane_glass_5"] = { name = "xpanes:pane_5" } -u["beds:bed_top_blue"] = { name = "beds:bed_top" } -u["beds:bed_bottom_blue"] = { name = "beds:bed_bottom" } - -u["homedecor:table_lamp_max"] = { name = "homedecor:table_lamp_white_max" } -u["homedecor:refrigerator"] = { name = "homedecor:refrigerator_steel" } - -u["ethereal:green_dirt"] = { name = "default:dirt_with_grass" } - -u["doors:door_wood_b_c"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "0"}}}, custom_function = __param2_wallmounted_to_facedir } --closed -u["doors:door_wood_b_o"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "1"}}}, custom_function = __param2_wallmounted_to_facedir } --open -u["doors:door_wood_b_1"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "0"}}}} --closed -u["doors:door_wood_b_2"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "3"}}}} --closed / reversed ?? -u["doors:door_wood_a_c"] = {name = "doors:hidden" } -u["doors:door_wood_a_o"] = {name = "doors:hidden" } -u["doors:door_wood_t_1"] = {name = "doors:hidden" } -u["doors:door_wood_t_2"] = {name = "doors:hidden" } - -u["doors:door_glass_b_c"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "0"}}}, custom_function = __param2_wallmounted_to_facedir } --closed -u["doors:door_glass_b_o"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "1"}}}, custom_function = __param2_wallmounted_to_facedir } --open -u["doors:door_glass_b_1"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "0"}}}} --closed -u["doors:door_glass_b_2"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "3"}}}} --closed / reversed ?? -u["doors:door_glass_a_c"] = {name = "doors:hidden" } -u["doors:door_glass_a_o"] = {name = "doors:hidden" } -u["doors:door_glass_t_1"] = {name = "doors:hidden" } -u["doors:door_glass_t_2"] = {name = "doors:hidden" } - -u["doors:door_steel_b_c"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "0"}}}, custom_function = __param2_wallmounted_to_facedir } --closed -u["doors:door_steel_b_o"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "1"}}}, custom_function = __param2_wallmounted_to_facedir } --open -u["doors:door_steel_b_1"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "0"}}}} --closed -u["doors:door_steel_b_2"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "3"}}}} --closed / reversed ?? -u["doors:door_steel_a_c"] = {name = "doors:hidden" } -u["doors:door_steel_a_o"] = {name = "doors:hidden" } -u["doors:door_steel_t_1"] = {name = "doors:hidden" } -u["doors:door_steel_t_2"] = {name = "doors:hidden" } - - -local c = {} -local default_replacements = c --- "name" and "cost_item" are optional. --- if name is missed it will not be changed --- if cost_item is missed it will be determinated as usual (from changed name) --- a crazy sample is: instead of cobble place goldblock, use wood as payment --- c["default:cobble"] = { name = "default:goldblock", cost_item = "default:wood" } - -c["beds:bed_top"] = { cost_item = mapping.c_free_item } -- the bottom of the bed is payed, so buld the top for free - --- it is hard to get a source in survival, so we use buckets. Note, the bucket is lost after usage by NPC -c["default:lava_source"] = { cost_item = "bucket:bucket_lava" } -c["default:river_water_source"] = { cost_item = "bucket:bucket_river_water" } -c["default:water_source"] = { cost_item = "bucket:bucket_water" } - --- does not sense to set flowing water because it flow away without the source (and will be generated trough source) -c["default:water_flowing"] = { name = "" } -c["default:lava_flowing"] = { name = "" } -c["default:river_water_flowing"] = { name = "" } - --- pay different dirt types by the sane dirt -c["default:dirt_with_dry_grass"] = { cost_item = "default:dirt" } -c["default:dirt_with_grass"] = { cost_item = "default:dirt" } -c["default:dirt_with_snow"] = { cost_item = "default:dirt" } - -c["xpanes:pane_5"] = { name = "xpanes:pane_flat", param2 = 0 } --unsure -c["xpanes:pane_10"] = { name = "xpanes:pane_flat", param2 = 1 } --unsure - ------------------------------------------------ --- copy table of mapping entry ------------------------------------------------ -function mapping.merge_map_entry(entry1, entry2) - if entry2 then - return {name = entry1.name or entry2.name, - node_def = entry1.node_def or entry2.node_def, - content_id = entry1.content_id or entry2.content_id, - param2 = entry1.param2 or entry2.param2, - meta = entry1.meta or entry2.meta, - custom_function = entry1.custom_function or entry2.custom_function, - cost_item = entry1.cost_item or entry2.cost_item, - } - else - return {name = entry1.name, - content_id = entry1.content_id, - node_def = entry1.node_def, - param2 = entry1.param2, - meta = entry1.meta, - custom_function = entry1.custom_function, - cost_item = entry1.cost_item} - end -end - - ----------------------------------------------- - -- is_equal_meta - compare meta information of 2 nodes - -- name - Node name to check and map - -- return - item name used as payment - ----------------------------------------------- -function mapping.is_equal_meta(a,b) - local typa = type(a) - local typb = type(b) - if typa ~= typb then - return false - end - - if typa == "table" then - if #a ~= #b then - return false - else - for i,v in ipairs(a) do - if not mapping.is_equal_meta(a[i],b[i]) then - return false - end - end - return true - end - else - if a == b then - return true - end - end -end - ------------------------------------------------ --- Fallback nodes replacement of unknown nodes ------------------------------------------------ -function mapping.map_unknown(name) - local map = unknown_nodes_data[name] - if not map or map.name == name then -- no fallback mapping. don't use the node - dprint("mapping failed:", name, dump(map)) - print("unknown nodes in building", name) - return nil - end - - dprint("mapped", name, "to", map.name) - return mapping.merge_map_entry(map) -end - ------------------------------------------------ --- Take filters and actions on nodes before building ------------------------------------------------ -function mapping.map_name(name) --- get mapped registred node name for further mappings - local node_chk = minetest.registered_nodes[name] - - --do fallback mapping if not registred node - if not node_chk then - local fallback = mapping.map_unknown(name) - if fallback then - dprint("map fallback:", dump(fallback)) - local fbmapped = mapping.map_name(fallback.name) - if fbmapped then - return mapping.merge_map_entry(fbmapped, fallback) --merge fallback values into the mapped node - end - end - dprint("unmapped node", name) - return - end - - -- get default replacement - local map = default_replacements[name] - local mr -- mapped return table - if not map then - mr = {} - mr.name = name - mr.node_def = node_chk - else - mr = mapping.merge_map_entry(map) - if mr.name == nil then - mr.name = name - end - end - - --disabled by mapping - if mr.name == "" then - return nil - end - - mr.node_def = minetest.registered_nodes[mr.name] - - -- determine cost_item - if not mr.cost_item then - - --Check for price or if it is free - local recipe = minetest.get_craft_recipe(mr.name) - if (mr.node_def.groups.not_in_creative_inventory and --not in creative - not (mr.node_def.groups.not_in_creative_inventory == 0) and - (not recipe or not recipe.items)) --and not craftable - - or (not mr.node_def.description or mr.node_def.description == "") then -- no description - if mr.node_def.drop and mr.node_def.drop ~= "" then - -- use possible drop as payment - if type(mr.node_def.drop) == "table" then -- drop table - mr.cost_item = mr.node_def.drop[1] -- use the first one - else - mr.cost_item = mr.node_def.drop - end - else --something not supported, but known - mr.cost_item = mapping.c_free_item -- will be build for free. they are something like doors:hidden or second part of coffee lrfurn:coffeetable_back - end - else -- build for payment the 1:1 - mr.cost_item = mr.name - end - end - - mr.content_id = minetest.get_content_id(mr.name) - dprint("map", name, "to", mr.name, mr.param2, mr.cost_item) - return mr -end - ------------------------------------------------ --- create a "mappednodes" using the data from analyze_* files ------------------------------------------------ -function mapping.do_mapping(data) - data.mappednodes = {} - for node_id, name in ipairs(data.nodenames) do - data.mappednodes[node_id] = mapping.map_name(name) - end -end diff --git a/npcf-worker.lua b/npcf-worker.lua index 79bc080..0191355 100644 --- a/npcf-worker.lua +++ b/npcf-worker.lua @@ -5,469 +5,43 @@ local MAX_SPEED = 5 local BUILD_DISTANCE = 3 local HOME_RANGE = 10 - -townchest.npc = { - spawn_nearly = function(pos, owner) - local npcid = tostring(math.random(10000)) - npcf.index[npcid] = owner --owner - local ref = { - id = npcid, - pos = {x=(pos.x+math.random(0,4)-4),y=(pos.y + 0.5),z=(pos.z+math.random(0,4)-4)}, - yaw = math.random(math.pi), - name = "townchest:npcf_builder", - owner = owner, - } - local npc = npcf:add_npc(ref) - npcf:save(ref.id) - if npc then - npc:update() - end - end -} - -local function get_speed(distance) - local speed = distance * 0.5 - if speed > MAX_SPEED then - speed = MAX_SPEED - end - return speed -end - - -local select_chest = function(self) - -- do nothing if the chest not ready - if not self.metadata.chestpos - or not townchest.chest.list[minetest.pos_to_string(self.metadata.chestpos)] --chest position not valid - or not self.chest - or not self.chest:npc_build_allowed() then --chest buid not ready - - local npcpos = self.object:getpos() - local selectedchest = nil - for key, chest in pairs(townchest.chest.list) do - if (not selectedchest or vector.distance(npcpos, chest.pos) < vector.distance(npcpos, selectedchest.pos)) and chest:npc_build_allowed() then - selectedchest = chest - end - end - if selectedchest then - self.metadata.chestpos = selectedchest.pos - self.chest = selectedchest - dprint("Now I will build for chest",self.chest) - - -- the chest is the new home of npc - if vector.distance(self.origin.pos, selectedchest.pos) > HOME_RANGE then - self.origin.pos = selectedchest.pos - self.origin.yaw = npcf:get_face_direction(npcpos, selectedchest.pos) - end - - else --stay if no chest assigned - self.metadata.chestpos = nil - self.chest = nil - self.chestpos = nil - end +townchest.npc = {} +function townchest.npc.spawn_nearly(pos, owner) + local npcid = tostring(math.random(10000)) + npcf.index[npcid] = owner --owner + local ref = { + id = npcid, + pos = {x=(pos.x+math.random(0,4)-4),y=(pos.y + 0.5),z=(pos.z+math.random(0,4)-4)}, + yaw = math.random(math.pi), + name = "schemlib_builder_npcf:builder", + owner = owner, + } + local npc = npcf:add_npc(ref) + npcf:save(ref.id) + if npc then + npc:update() end end +function townchest.npc.enable_build(plan) + schemlib_builder_npcf.plan_manager:add(minetest.pos_to_string(plan.anchor_pos), plan) +end -local get_if_buildable = function(self, realpos, node_prep) - local pos = self.chest.plan:get_plan_pos(realpos) - local node - if node_prep then - node = node_prep - else - node = self.chest.plan:prepare_node_for_build(pos, realpos) - end +function townchest.npc.disable_build(plan) + schemlib_builder_npcf.plan_manager:set_finished(minetest.pos_to_string(plan.anchor_pos)) +end - if not node then - -- remove something crufty - self.chest.plan:remove_node(pos) - return nil - end - - -- skip the chest position - if realpos.x == self.chest.pos.x and realpos.y == self.chest.pos.y and realpos.z == self.chest.pos.z then --skip chest pos - self.chest.plan:remove_node(pos) - return nil - end - - -- get info about placed node to compare - local orig_node = minetest.get_node(realpos) - if orig_node.name == "ignore" then - minetest.get_voxel_manip():read_from_map(realpos, realpos) - orig_node = minetest.get_node(realpos) - end - - if not orig_node or orig_node.name == "ignore" then --not loaded chunk. can be forced by forceload_block before check if buildable - dprint("ignore node at", minetest.pos_to_string(realpos)) - return nil - end - - -- check if already built - if orig_node.name == node.name or orig_node.name == minetest.registered_nodes[node.name].name then - -- right node is at the place. there are no costs to touch them. Check if a touch needed - if (node.param2 ~= orig_node.param2 and not (node.param2 == nil and orig_node.param2 == 0)) then - --param2 adjustment --- node.matname = townchest.mapping.c_free_item -- adjust params for free - return node - elseif not node.meta then - --same item without metadata. nothing to do - self.chest.plan:remove_node(pos) - return nil - elseif townchest.mapping.is_equal_meta(minetest.get_meta(realpos):to_table(), node.meta) then - --metadata adjustment - self.chest.plan:remove_node(pos) - return nil - elseif node.matname == townchest.mapping.c_free_item then - -- TODO: check if nearly nodes are already built - return node - else - return node +-- hook to trigger chest update each node placement +function townchest.npc.plan_update_hook(plan, status) + if plan.chest then + plan.chest:update_info("build_status") + if status == "finished" then + dprint("----Finished event called in npc hook----") + plan.chest.info.npc_build = false + plan.chest.info.instantbuild = false + townchest.npc.disable_build(plan) end - else - -- no right node at place - return node + plan.chest:update_statistics() end end - -local function prefer_target(npc, t1, t2) - if not t1 then - return t2 - end - - local npcpos = npc.object:getpos() - -- npc is 1.5 blocks over the work - npcpos.y = npcpos.y - 1.5 - - -- variables for preference manipulation - local t1_c = {x=t1.pos.x, y=t1.pos.y, z=t1.pos.z} - local t2_c = {x=t2.pos.x, y=t2.pos.y, z=t2.pos.z} - local prefer = 0 - - --prefer same items in building order - if npc.lastnode then - if npc.lastnode.name == t1.name then - prefer = prefer + 2.5 - end - if npc.lastnode.name == t2.name then - prefer = prefer - 2.5 - end - end - - -- prefer the last target node - if npc.targetnode then - if t1.pos.x == npc.targetnode.pos.x and - t1.pos.y == npc.targetnode.pos.y and - t1.pos.z == npc.targetnode.pos.z then - prefer = prefer + BUILD_DISTANCE - end - if t2.pos.x == npc.targetnode.pos.x and - t2.pos.y == npc.targetnode.pos.y and - t2.pos.z == npc.targetnode.pos.z then - prefer = prefer - BUILD_DISTANCE - end - end - - -- prefer air in general - if t1.name == "air" then - prefer = prefer + 2 - end - if t2.name == "air" then - prefer = prefer - 2 - end - - -- prefer reachable in general - if vector.distance(npcpos, t1.pos) <= BUILD_DISTANCE then - prefer = prefer + 2 - end - if vector.distance(npcpos, t2.pos) <= BUILD_DISTANCE then - prefer = prefer - 2 - end - - -- prefer lower node if not air - if t1.name ~= "air" then - t1_c.y = t1_c.y + 2 - elseif math.abs(npcpos.y - t1.pos.y) <= BUILD_DISTANCE then - -- prefer higher node if air in reachable distance - t1_c.y = t1_c.y - 4 - end - - -- prefer lower node if not air - if t2.name ~= "air" then - t2_c.y = t2_c.y + 2 - elseif math.abs(npcpos.y - t1.pos.y) <= BUILD_DISTANCE then - -- prefer higher node if air in reachable distance - t2_c.y = t2_c.y - 4 - end - - -- avoid build directly under or in the npc - if math.abs(npcpos.x - t1.pos.x) < 0.5 and - math.abs(npcpos.y - t1.pos.y) < 3 and - math.abs(npcpos.z - t1.pos.z) < 0.5 then - prefer = prefer-1.5 - end - if math.abs(npcpos.x - t2.pos.x) < 0.5 and - math.abs(npcpos.y - t1.pos.y) < 3 and - math.abs(npcpos.z - t2.pos.z) < 0.5 then - prefer = prefer+1.5 - end - - -- compare - if vector.distance(npcpos, t1_c) - prefer > vector.distance(npcpos, t2_c) then - return t2 - else - return t1 - end - -end - - -local get_target = function(self) - local npcpos = self.object:getpos() - local plan = self.chest.plan - npcpos.y = npcpos.y - 1.5 -- npc is 1.5 blocks over the work - - local npcpos_round = vector.round(npcpos) - local selectednode - - -- first try: look for nearly buildable nodes - dprint("search for nearly node") - for x=npcpos_round.x-5, npcpos_round.x+5 do - for y=npcpos_round.y-5, npcpos_round.y+5 do - for z=npcpos_round.z-5, npcpos_round.z+5 do - local node = get_if_buildable(self,{x=x,y=y,z=z}) - if node then - node.pos = {x=x,y=y,z=z} - selectednode = prefer_target(self, selectednode, node) - end - end - end - end - if selectednode then - dprint("nearly found: NPC: "..minetest.pos_to_string(npcpos).." Block "..minetest.pos_to_string(selectednode.pos)) - end - - if not selectednode then - dprint("nearly nothing found") - -- get the old target to compare - if self.targetnode and self.targetnode.pos and - (self.targetnode.node_id or self.targetnode.name) then - selectednode = get_if_buildable(self, self.targetnode.pos, self.targetnode) - end - end - - -- second try. Check the current chunk - dprint("search for node in current chunk") - - local chunk_nodes = plan:get_nodes_for_chunk(plan:get_plan_pos(npcpos_round)) - dprint("Chunk loaeded: nodes:", #chunk_nodes) - - for idx, nodeplan in ipairs(chunk_nodes) do - local node = get_if_buildable(self, nodeplan.wpos, nodeplan.node) - if node then - node.pos = nodeplan.wpos - selectednode = prefer_target(self, selectednode, node) - end - end - - if selectednode then - dprint("found in current chunk: NPC: "..minetest.pos_to_string(npcpos).." Block "..minetest.pos_to_string(selectednode.pos)) - end - - if not selectednode then - dprint("get random node") - - local random_pos = plan:get_random_node_pos() - if random_pos then - dprint("---check chunk", minetest.pos_to_string(random_pos)) - local wpos = plan:get_world_pos(random_pos) - local node = get_if_buildable(self, wpos) - if node then - node.pos = wpos - selectednode = prefer_target(self, selectednode, node) - end - - if selectednode then - dprint("random node: Block "..minetest.pos_to_string(random_pos)) - else - dprint("random node not buildable, check the whole chunk", minetest.pos_to_string(random_pos)) - local chunk_nodes = plan:get_nodes_for_chunk(random_pos) - dprint("Chunk loaeded: nodes:", #chunk_nodes) - - for idx, nodeplan in ipairs(chunk_nodes) do - local node = get_if_buildable(self, nodeplan.wpos, nodeplan.node) - if node then - node.pos = nodeplan.wpos - selectednode = prefer_target(self, selectednode, node) - end - end - if selectednode then - dprint("found in current chunk: Block "..minetest.pos_to_string(selectednode.pos)) - end - end - else - dprint("something wrong with random_pos") - end - end - - if selectednode then - assert(selectednode.pos, "BUG: a position should exists") - return selectednode - else - dprint("no next node found", plan.data.nodecount) - if plan.data.nodecount == 0 then - self.chest.info.npc_build = false - end - self.chest:update_statistics() - end -end - -npcf:register_npc("townchest:npcf_builder" ,{ - description = "Townchest Builder NPC", - textures = {"npcf_builder_skin.png"}, - stepheight = 1.1, - inventory_image = "npcf_builder_inv.png", - on_step = function(self) - if self.timer > 1 then - self.timer = 0 - select_chest(self) - self.target_prev = self.targetnode - if self.chest and self.chest.plan and self.chest.plan.data.nodecount > 0 then - self.targetnode = get_target(self) - self.dest_type = "build" - else - if self.dest_type ~= "home_reached" then - self.targetnode = self.origin - self.dest_type = "home" - end - end - --- simple check if target reached - elseif self.targetnode then - local pos = self.object:getpos() - local target_distance = vector.distance(pos, self.targetnode.pos) - if target_distance < 1 then - local yaw = self.object:getyaw() - local speed = 0 - self.object:setvelocity(npcf:get_walk_velocity(speed, self.object:getvelocity().y, yaw)) - end - return - end - - local pos = self.object:getpos() - local yaw = self.object:getyaw() - local state = NPCF_ANIM_STAND - local speed = 0 - local acceleration = {x=0, y=-10, z=0} - if self.targetnode then - local target_distance = vector.distance(pos, self.targetnode.pos) - local last_distance = 0 - if self.var.last_pos then - last_distance = vector.distance(self.var.last_pos, self.targetnode.pos) - end - - yaw = npcf:get_face_direction(pos, self.targetnode.pos) - -- target reached build - if target_distance <= BUILD_DISTANCE and self.dest_type == "build" then - dprint("target reached - build", self.targetnode.name, minetest.pos_to_string(self.targetnode.pos)) - -- do the build ---TODO: move outsite of this function - local soundspec - if minetest.registered_items[self.targetnode.name].sounds then - soundspec = minetest.registered_items[self.targetnode.name].sounds.place - elseif self.targetnode.name == "air" then --TODO: should be determinated on old node, if the material handling is implemented - soundspec = default.node_sound_leaves_defaults({place = {name = "default_place_node", gain = 0.25}}) - end - if soundspec then - soundspec.pos = pos - minetest.sound_play(soundspec.name, soundspec) - end - minetest.env:add_node(self.targetnode.pos, self.targetnode) - if self.targetnode.meta then - minetest.env:get_meta(self.targetnode.pos):from_table(self.targetnode.meta) - end - self.chest.plan:remove_node(self.targetnode) - self.chest:update_statistics() - - local cur_pos = {x=pos.x, y=pos.y - 0.5, z=pos.z} - local cur_node = minetest.registered_items[minetest.get_node(cur_pos).name] - if cur_node.walkable then - pos = {x=pos.x, y=pos.y + 1.5, z=pos.z} - self.object:setpos(pos) - end - - if target_distance > 2 then - speed = 1 - state = NPCF_ANIM_WALK_MINE - else - speed = 0 - state = NPCF_ANIM_MINE - end - - self.timer = 0 - self.lastnode = self.targetnode - self.laststep = "build" - self.targetnode = nil - self.path = nil - -- home reached - elseif target_distance < HOME_RANGE and self.dest_type == "home" then --- self.object:setpos(self.origin.pos) - yaw = self.origin.yaw - speed = 0 - self.dest_type = "home_reached" - self.targetnode = nil - self.path = nil - else - --target not reached -- route - state = NPCF_ANIM_WALK - -- Big jump / teleport upsite - if (self.targetnode.pos.y -(pos.y-1.5)) > BUILD_DISTANCE and - math.abs(self.targetnode.pos.x - pos.x) <= 0.5 and - math.abs(self.targetnode.pos.z - pos.z) <= 0.5 then - acceleration = {x=0, y=0, z=0} - pos = {x=pos.x, y=self.targetnode.pos.y + 1.5, z=pos.z} - self.object:setpos(pos) - target_distance = 0 -- to skip the next part and set speed to 0 - state = NPCF_ANIM_STAND - self.path = nil - dprint("Big jump to"..minetest.pos_to_string(pos)) - end - - if self.timer == 0 or not self.path then - self.path = minetest.find_path(pos, self.targetnode.pos, 10, 1, 5, "A*") - end - - -- teleport in direction in case of stucking - dprint("check for stuck:", last_distance, target_distance, self.laststep) - if not self.path and --no stuck if path known - (last_distance - 0.01) <= target_distance and -- stucking - self.laststep == "walk" and -- second step stuck - self.target_prev and - ( minetest.pos_to_string(self.target_prev.pos) == minetest.pos_to_string(self.targetnode.pos)) then -- destination unchanged - local target_direcion = vector.direction(pos, self.targetnode.pos) - pos = vector.add(pos, vector.multiply(target_direcion, 2)) - if pos.y < self.targetnode.pos.y then - pos = {x=pos.x, y=self.targetnode.pos.y + 1.5, z=pos.z} - end - self.object:setpos(pos) - self.laststep = "teleport" - acceleration = {x=0, y=0, z=0} - target_distance = 0 -- to skip the next part and set speed to 0 - state = NPCF_ANIM_STAND - self.path = nil - dprint("Teleport to"..minetest.pos_to_string(pos)) - end - self.var.last_pos = pos - speed = get_speed(target_distance) - self.laststep = "walk" - end - end - - if self.path then - yaw = npcf:get_face_direction(pos, self.path[1]) - end - self.object:setacceleration(acceleration) - self.object:setvelocity(npcf:get_walk_velocity(speed, self.object:getvelocity().y, yaw)) - self.object:setyaw(yaw) - npcf:set_animation(self, state) - end, -}) - diff --git a/plan.lua b/plan.lua deleted file mode 100644 index 537cf01..0000000 --- a/plan.lua +++ /dev/null @@ -1,246 +0,0 @@ -local dprint = townchest.dprint_off --debug ---local dprint = townchest.dprint--debug - -townchest.plan = {} - -townchest.plan.new = function( chest ) - local self = {} - self.chest = chest - - -- helper: get scm entry for position - function self.get_scm_node(self, pos) - assert(pos.x, "pos without xyz") - if self.data.scm_data_cache[pos.y] == nil then - return nil - end - if self.data.scm_data_cache[pos.y][pos.x] == nil then - return nil - end - if self.data.scm_data_cache[pos.y][pos.x][pos.z] == nil then - return nil - end - return self.data.scm_data_cache[pos.y][pos.x][pos.z] - end - --- "node" = {x=,y=,z=,name_id=,param2=} - function self.add_node(self, node) - -- insert new - if self.data.scm_data_cache[node.y] == nil then - self.data.scm_data_cache[node.y] = {} - end - if self.data.scm_data_cache[node.y][node.x] == nil then - self.data.scm_data_cache[node.y][node.x] = {} - end - self.data.nodecount = self.data.nodecount + 1 - self.data.scm_data_cache[node.y][node.x][node.z] = node - end - - function self.remove_node(self, pos) - -- cleanup raw data - if self.data.scm_data_cache[pos.y] ~= nil then - if self.data.scm_data_cache[pos.y][pos.x] ~= nil then - if self.data.scm_data_cache[pos.y][pos.x][pos.z] ~= nil then - self.data.nodecount = self.data.nodecount - 1 - self.data.scm_data_cache[pos.y][pos.x][pos.z] = nil - end - if next(self.data.scm_data_cache[pos.y][pos.x]) == nil then - self.data.scm_data_cache[pos.y][pos.x] = nil - end - end - if next(self.data.scm_data_cache[pos.y]) == nil then - self.data.scm_data_cache[pos.y] = nil - end - end - - -- remove cached mapping data - if self.data.prepared_cache and self.data.prepared_cache[pos.y] ~= nil then - if self.data.prepared_cache[pos.y][pos.x]then - if self.data.prepared_cache[pos.y][pos.x][pos.z] ~= nil then - self.data.prepared_cache[pos.y][pos.x][pos.z] = nil - end - if next(self.data.prepared_cache[pos.y][pos.x]) == nil then - self.data.prepared_cache[pos.y][pos.x] = nil - end - end - if next(self.data.prepared_cache[pos.y]) == nil then - self.data.prepared_cache[pos.y] = nil - end - end - end - - - - function self.flood_with_air(self) - self.data.ground_y = math.floor(self.data.ground_y) - local add_max = 5 - local additional = 0 - - -- define nodename-ID for air - local air_id = #self.data.nodenames + 1 - self.data.nodenames[ air_id ] = "air" - - dprint("create flatting plan") - for y = self.data.min_pos.y, self.data.max_pos.y + 5 do -- with additional 5 on top - --calculate additional grounding - if y > self.data.ground_y then --only over ground - local high = y-self.data.ground_y - additional = high + 1 - if additional > add_max then --set to max - additional = add_max - end - end - - dprint("flat level:", y) - - for x = self.data.min_pos.x - additional, self.data.max_pos.x + additional do - for z = self.data.min_pos.z - additional, self.data.max_pos.z + additional do - local airnode = {x=x, y=y, z=z, name_id=air_id} - if self:get_scm_node(airnode) == nil then - self:add_node(airnode) - end - end - end - end - dprint("flatting plan done") - end - - - function self.get_world_pos(self,pos) - return { x=pos.x+self.chest.pos.x, - y=pos.y+self.chest.pos.y - self.data.ground_y - 1, - z=pos.z+self.chest.pos.z - } - end - - -- revert get_world_pos - function self.get_plan_pos(self,pos) - return { x=pos.x-self.chest.pos.x, - y=pos.y-self.chest.pos.y + self.data.ground_y + 1, - z=pos.z-self.chest.pos.z - } - end - - --- get nodes for selection which one should be build --- skip parameter is randomized - function self.get_random_node_pos(self) - dprint("get something from list") - - -- get random existing y - local keyset = {} - for k in pairs(self.data.scm_data_cache) do table.insert(keyset, k) end - if #keyset == 0 then --finished - return nil - end - local y = keyset[math.random(#keyset)] - - -- get random existing x - keyset = {} - for k in pairs(self.data.scm_data_cache[y]) do table.insert(keyset, k) end - local x = keyset[math.random(#keyset)] - - -- get random existing z - keyset = {} - for k in pairs(self.data.scm_data_cache[y][x]) do table.insert(keyset, k) end - local z = keyset[math.random(#keyset)] - - if z ~= nil then - return {x=x,y=y,z=z} - end - end - --- to be able working with forceload chunks - function self.get_nodes_for_chunk(self, node) - - -- calculate the begin of the chunk - --local BLOCKSIZE = core.MAP_BLOCKSIZE - local BLOCKSIZE = 16 - local wpos = self:get_world_pos(node) - local minp = {} - minp.x = (math.floor(wpos.x/BLOCKSIZE))*BLOCKSIZE - minp.y = (math.floor(wpos.y/BLOCKSIZE))*BLOCKSIZE - minp.z = (math.floor(wpos.z/BLOCKSIZE))*BLOCKSIZE - local maxp = vector.add(minp, 16) - - dprint("nodes for chunk (real-pos)", minetest.pos_to_string(minp), minetest.pos_to_string(maxp)) - - local minv = self:get_plan_pos(minp) - local maxv = self:get_plan_pos(maxp) - dprint("nodes for chunk (plan-pos)", minetest.pos_to_string(minv), minetest.pos_to_string(maxv)) - - local ret = {} - for y = minv.y, maxv.y do - if self.data.scm_data_cache[y] ~= nil then - for x = minv.x, maxv.x do - if self.data.scm_data_cache[y][x] ~= nil then - for z = minv.z, maxv.z do - if self.data.scm_data_cache[y][x][z] ~= nil then - local pos = {x=x,y=y,z=z} - local wpos = self:get_world_pos(pos) - table.insert(ret, {pos = pos, wpos = wpos, node=self:prepare_node_for_build(pos, wpos)}) - end - end - end - end - end - end - dprint("nodes in chunk to build", #ret) - return ret - end - - -- prepare node for build - function self.prepare_node_for_build(self, pos, wpos) - -- first run, generate mapping data - if self.data.mappednodes == nil then - townchest.mapping.do_mapping(self.data) - end - - -- get from cache - if self.data.prepared_cache ~= nil and - self.data.prepared_cache[pos.y] ~= nil and - self.data.prepared_cache[pos.y][pos.x] ~= nil and - self.data.prepared_cache[pos.y][pos.x][pos.z] ~= nil then - return self.data.prepared_cache[pos.y][pos.x][pos.z] - end - - -- get scm data - local scm_node = self:get_scm_node(pos) - if scm_node == nil then - return nil - end - - --get mapping data - local map = self.data.mappednodes[scm_node.name_id] - if map == nil then - return nil - end - - local node = townchest.mapping.merge_map_entry(map, scm_node) - - if node.custom_function ~= nil then - node.custom_function(node, pos, wpos) - end - - -- maybe node name is changed in custom function. Update the content_id in this case - node.content_id = minetest.get_content_id(node.name) - node.node_def = minetest.registered_nodes[node.name] - - -- store the mapped node info in cache - if self.data.prepared_cache == nil then - self.data.prepared_cache = {} - end - if self.data.prepared_cache[pos.y] == nil then - self.data.prepared_cache[pos.y] = {} - end - if self.data.prepared_cache[pos.y][pos.x] == nil then - self.data.prepared_cache[pos.y][pos.x] = {} - end - self.data.prepared_cache[pos.y][pos.x][pos.z] = node - - return node - end - --------------------- --------------------- - return self -- the plan object -end diff --git a/smartfs-forms.lua b/smartfs-forms.lua index 890c474..3a3c057 100644 --- a/smartfs-forms.lua +++ b/smartfs-forms.lua @@ -24,10 +24,10 @@ local _file_open_dialog = function(state) for name, def in pairs(self._tabs) do if name == tabname then def.button:setBackground("default_gold_block.png") - def.view:setIsHidden(false) + def.view:setVisible(true) else def.button:setBackground(nil) - def.view:setIsHidden(true) + def.view:setVisible(false) end end self.active_name = tabname @@ -49,12 +49,12 @@ local _file_open_dialog = function(state) tab1.button:onClick(function(self) tab_controller:set_active("tab1") end) - tab1.view = state:view(0,1,"tab1_view") - tab1.viewstate = tab1.view:getViewState() + tab1.view = state:container(0,1,"tab1_view") + tab1.viewstate = tab1.view:getContainerState() -- file selection tab view state tab1.viewstate:label(0,0,"header","Please select a building") local listbox = tab1.viewstate:listbox(0,0.5,6,5.5,"fileslist") - for idx, file in ipairs(townchest.files.get()) do + for idx, file in ipairs(townchest.files_get()) do listbox:addItem(file) end tab_controller:tab_add("tab1", tab1) @@ -68,8 +68,8 @@ local _file_open_dialog = function(state) tab2.button:onClick(function(self) tab_controller:set_active("tab2") end) - tab2.view = state:view(0,1,"tab2_view") - tab2.viewstate = tab2.view:getViewState() + tab2.view = state:container(0,1,"tab2_view") + tab2.viewstate = tab2.view:getContainerState() -- Tasks tab view state tab2.viewstate:label(0,0.2,"header","Build simple form") local variant = tab2.viewstate:dropdown(3.5,0.2,4,0.5,"variant", 1) @@ -174,21 +174,25 @@ local _build_status = function(state) inst_tg:onToggle(function(self, state, player) if self:getId() == 2 then chest.info.instantbuild = true + chest.plan:set_status("build") + chest:run_async(chest.instant_build_chunk) else chest.info.instantbuild = false end set_dynamic_values() chest:persist_info() - chest:run_async(chest.instant_build_chunk) end) -- NPC build button local npc_tg = state:toggle(5,3,3,0.5,"npc_tg",{ "Start NPC build", "Stop NPC build"}) npc_tg:onToggle(function(self, state, player) if self:getId() == 2 then - chest.info.npc_build = true --is used by NPC + chest.info.npc_build = true + chest.plan:set_status("build") + townchest.npc.enable_build(chest.plan) else chest.info.npc_build = false + townchest.npc.disable_build(chest.plan) end set_dynamic_values() chest:persist_info() @@ -233,9 +237,9 @@ local _build_status = function(state) end if chest.info.npc_build == true or chest.info.instantbuild == true then - reload_bt:setIsHidden(true) + reload_bt:setVisible(false) else - reload_bt:setIsHidden(false) + reload_bt:setVisible(true) end end diff --git a/smartfs.lua b/smartfs.lua index 38a3867..e57831a 100644 --- a/smartfs.lua +++ b/smartfs.lua @@ -7,6 +7,7 @@ local smartfs = { _fdef = {}, _edef = {}, + _ldef = {}, opened = {}, inv = {} } @@ -64,47 +65,12 @@ function smartfs.dynamic(name,player) smartfs._dynamic_warned = true minetest.log("warning", "SmartFS - (Warning) On the fly forms are being used. May cause bad things to happen") end - - local state = smartfs._makeState_({name=name}, player, nil, false) - state.show = state._show_ + local statelocation = smartfs._ldef.player._make_state_location_(player) + local state = smartfs._makeState_({name=name}, nil, statelocation, player) smartfs.opened[player] = state return state end ------------------------------------------------------- --- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success. ------------------------------------------------------- -function smartfs.add_to_inventory(form, icon, title) - if unified_inventory then - unified_inventory.register_button(form.name, { - type = "image", - image = icon, - }) - unified_inventory.register_page(form.name, { - get_formspec = function(player, formspec) - local name = player:get_player_name() - local opened = smartfs._show_(form, name, nil, true) - return {formspec = opened:_buildFormspec_(false)} - end - }) - return true - elseif inventory_plus then - minetest.register_on_joinplayer(function(player) - inventory_plus.register_button(player, form.name, title) - end) - minetest.register_on_player_receive_fields(function(player, formname, fields) - if formname == "" and fields[form.name] then - local name = player:get_player_name() - local opened = smartfs._show_(form, name, nil, true) - inventory_plus.set_inventory_formspec(player, opened:_buildFormspec_(true)) - end - end) - return true - else - return false - end -end - ------------------------------------------------------ -- Smartfs Interface - Returns the name of an installed and supported inventory mod that will be used above, or nil ------------------------------------------------------ @@ -118,6 +84,26 @@ function smartfs.inventory_mod() end end +------------------------------------------------------ +-- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success. +------------------------------------------------------ +function smartfs.add_to_inventory(form, icon, title) + local ldef + local invmod = smartfs.inventory_mod() + if invmod then + ldef = smartfs._ldef[invmod] + else + return false + end + return ldef.add_to_inventory(form, icon, title) +end + +------------------------------------------------------ +-- Smartfs Interface - Set the form as players inventory +------------------------------------------------------ +function smartfs.set_player_inventory(form) + smartfs._ldef.inventory.set_inventory(form) +end ------------------------------------------------------ -- Smartfs Interface - Allows you to use smartfs.create after the game loads. Not recommended! ------------------------------------------------------ @@ -125,13 +111,157 @@ function smartfs.override_load_checks() smartfs._loaded_override = true end +------------------------------------------------------ +-- Smartfs formspec locations +------------------------------------------------------ +-- Unified inventory plugin +smartfs._ldef.unified_inventory = { + add_to_inventory = function(form, icon, title) + unified_inventory.register_button(form.name, { + type = "image", + image = icon, + }) + unified_inventory.register_page(form.name, { + get_formspec = function(player, formspec) + local name = player:get_player_name() + local statelocation = smartfs._ldef.unified_inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + return {formspec = state:_buildFormspec_(false)} + else + return nil + end + end + }) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + unified_inventory.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state.def.name) + end + } + end +} + +-- Inventory plus plugin +smartfs._ldef.inventory_plus = { + add_to_inventory = function(form, icon, title) + minetest.register_on_joinplayer(function(player) + inventory_plus.register_button(player, form.name, title) + end) + minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "" and fields[form.name] then + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory_plus._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end + end) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + inventory_plus.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state:_buildFormspec_(true)) + end + } + end +} + +-- Show to player +smartfs._ldef.player = { + _make_state_location_ = function(player) + return { + type = "player", + player = player, + _show_ = function(state) + minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true)) + end + } + end +} + +-- Standalone inventory +smartfs._ldef.inventory = { + set_inventory = function(form) + if sfinv and sfinv.enabled then + sfinv.enabled = nil + end + minetest.register_on_joinplayer(function(player) + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state +-- can be back to direct call if the issue is solved https://github.com/minetest/minetest_game/issues/1546 +-- state:show() + minetest.after(1, state.show, state) + end + end) + minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + smartfs.inv[name].obsolete = true + smartfs.inv[name] = nil + end) + end, + _make_state_location_ = function(name) + return { + type = "inventory", + player = name, + _show_ = function(state) + local player = minetest.get_player_by_name(state.location.player) + player:set_inventory_formspec(state:_buildFormspec_(true)) + end + } + end +} + +-- Node metadata +smartfs._ldef.nodemeta = { + _make_state_location_ = function(nodepos) + return { + type = "nodemeta", + pos = nodepos, + _show_ = function(state) + local meta = minetest.get_meta(state.location.pos) + meta:set_string("formspec", state:_buildFormspec_(true)) + meta:set_string("smartfs_name", state.def.name) + end, + } + end +} + +-- Sub-container (internally used) +smartfs._ldef.container = { + _make_state_location_ = function(element) + local self = { + type = "container", + containerElement = element, + parentState = element.root + } + if self.parentState.location.type == "container" then + self.rootState = self.parentState.location.rootState + else + self.rootState = self.parentState + end + return self + end +} + ------------------------------------------------------ -- Minetest Interface - on_receive_fields callback can be used in minetest.register_node for nodemeta forms ------------------------------------------------------ function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, params) local meta = minetest.get_meta(nodepos) local nodeform = meta:get_string("smartfs_name") - if not nodeform then + if not nodeform or nodeform == "" then print("SmartFS - (Warning) smartfs.nodemeta_on_receive_fields for node without smarfs data") return false end @@ -143,7 +273,8 @@ function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, p if not smartfs.opened[opened_id] or -- If opened first time smartfs.opened[opened_id].def.name ~= nodeform or -- Or form is changed smartfs.opened[opened_id].obsolete then - state = smartfs._makeState_(form, nil, params, nil, nodepos) + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + state = smartfs._makeState_(form, params, statelocation) if smartfs.opened[opened_id] then smartfs.opened[opened_id].obsolete = true end @@ -165,9 +296,10 @@ function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, p -- Reset form if all players disconnected if sender and not state.players:get_first() and not state.obsolete then - state._ele = {} - if form.form_setup_callback(state) then - state:_show_() + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local resetstate = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(resetstate) ~= false then + resetstate:show() end smartfs.opened[opened_id] = nil end @@ -181,9 +313,13 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) if smartfs.opened[name] and smartfs.opened[name].location.type == "player" then if smartfs.opened[name].def.name == formname then local state = smartfs.opened[name] - return state:_sfs_on_receive_fields_(name, fields) - else - smartfs.opened[name] = nil + state:_sfs_on_receive_fields_(name, fields) + + -- disconnect player if form closed + if not state.players:get_first() then + smartfs.opened[name].obsolete = true + smartfs.opened[name] = nil + end end elseif smartfs.inv[name] and smartfs.inv[name].location.type == "inventory" then local state = smartfs.inv[name] @@ -202,26 +338,18 @@ end) ------------------------------------------------------ -- Form Interface [linked to form:show()] - Shows the form to a player ------------------------------------------------------ -function smartfs._show_(form, name, params, is_inv) +function smartfs._show_(form, name, params) assert(form) assert(type(name) == "string", "smartfs: name needs to be a string") assert(minetest.get_player_by_name(name), "player does not exist") - - local state = smartfs._makeState_(form, name, params, is_inv) - state.show = state._show_ + local statelocation = smartfs._ldef.player._make_state_location_(name) + local state = smartfs._makeState_(form, params, statelocation, name) if form.form_setup_callback(state) ~= false then - if not is_inv then - if smartfs.opened[name] then -- set maybe previous form to obsolete - smartfs.opened[name].obsolete = true - end - smartfs.opened[name] = state - state:_show_() - else - if smartfs.inv[name] then -- set maybe previous form to obsolete - smartfs.inv[name].obsolete = true - end - smartfs.inv[name] = state + if smartfs.opened[name] then -- set maybe previous form to obsolete + smartfs.opened[name].obsolete = true end + smartfs.opened[name] = state + state:show() end return state end @@ -229,18 +357,18 @@ end ------------------------------------------------------ -- Form Interface [linked to form:attach_to_node()] - Attach a formspec to a node meta ------------------------------------------------------ -function smartfs._attach_to_node_(form, nodepos, placer, params) +function smartfs._attach_to_node_(form, nodepos, params) assert(form) assert(nodepos and nodepos.x) - -- No attached user, no params, no inventory integration: - local state = smartfs._makeState_(form, nil, params, nil, nodepos) - if form.form_setup_callback(state) then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local state = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(state) ~= false then local opened_id = minetest.pos_to_string(nodepos) if smartfs.opened[opened_id] then -- set maybe previous form to obsolete smartfs.opened[opened_id].obsolete = true end - state:_show_() + state:show() end return state end @@ -248,159 +376,113 @@ end ------------------------------------------------------ -- Smartfs Framework - create a form object (state) ------------------------------------------------------ -function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) +function smartfs._makeState_(form, params, location, newplayer) ------------------------------------------------------ -- State - -- Object to manage players ------------------------------------------------------ - local function _make_players_(form, newplayer) + local function _make_players_(newplayer) local self = { _list = {} } - function self.connect(self, player) - if player then - self._list[player] = player - end + self._list[player] = true end - function self.disconnect(self, player) self._list[player] = nil end - function self.get_first(self) return next(self._list) end - - self:connect(newplayer) + if newplayer then + self:connect(newplayer) + end return self end - ------------------------------------------------------ - -- State - location handler - ------------------------------------------------------ - -- create object to handle formspec location - local function _make_location_(form, newplayer, params, is_inv, nodepos) - local self = {} - self.rootState = self --by default. overriden in case of view - if form.root and form.root.location then --the parent "form" is a state - self.type = "view" - self.viewElement = form -- form contains the element trough parent view element or form - self.parentState = form.root - if self.parentState.location.type == "view" then - self.rootState = self.parentState.location.rootState - else - self.rootState = self.parentState - end - elseif nodepos then - self.type = "nodemeta" - self.pos = nodepos - elseif newplayer then - if is_inv then - self.type = "inventory" - else - self.type = "player" - end - self.player = newplayer - end - return self + local compat_is_inv + if location.type == "inventory" then + compat_is_inv = true + else + compat_is_inv = false end ------------------------------------------------------ -- State - create returning state object ------------------------------------------------------ return { - ------------------------------------------------------ - -- State - root window state interface. Not used/supportted in views - ------------------------------------------------------ - players = _make_players_(form, newplayer), - is_inv = is_inv, -- obsolete. Please use location.type="inventory" instead - player = newplayer, -- obsolete. Please use location.player - close = function(self) - self.closed = true - end, - _show_ = function(self) - local res = self:_buildFormspec_(true) - if self.location.type == "inventory" then - if unified_inventory then - unified_inventory.set_inventory_formspec(minetest.get_player_by_name(self.location.player), self.def.name) - elseif inventory_plus then - inventory_plus.set_inventory_formspec(minetest.get_player_by_name(self.location.player), res) - end - elseif self.location.type == "player" then - minetest.show_formspec(self.location.player, form.name, res) - elseif self.location.type == "nodemeta" then - local meta = minetest.get_meta(self.location.pos) - meta:set_string("formspec", res) - meta:set_string("smartfs_name", self.def.name) - end - end, - - ------------------------------------------------------ - -- State - window and view interface - ------------------------------------------------------ - _ele = {}, -- window or view elements list - def = form, -- in case of views there is the parent state - location = _make_location_(form, newplayer, params, is_inv, nodepos), + _ele = {}, + def = form, + players = _make_players_(newplayer), + location = location, + is_inv = compat_is_inv, -- obsolete / compatibility + player = newplayer, -- obsolete / compatibility param = params or {}, get = function(self,name) return self._ele[name] end, + close = function(self) + self.closed = true + end, + getSize = function(self) + return self._size + end, + size = function(self,w,h) + self._size = {w=w,h=h} + end, + setSize = function(self,w,h) + self._size = {w=w,h=h} + end, + getNamespace = function(self) + local ref = self + local namespace = "" + while ref.location.type == "container" do + namespace = ref.location.containerElement.name.."#"..namespace + ref = ref.location.parentState -- step near to the root + end + return namespace + end, _buildFormspec_ = function(self,size) local res = "" if self._size and size then res = "size["..self._size.w..","..self._size.h.."]" end for key,val in pairs(self._ele) do - if not val:getIsHiddenOrCutted() == true then - res = res .. val:build() + if val:getVisible() then + res = res .. val:getBackgroundString() .. val:build() end end return res end, - -- process /apply received field value - _sfs_process_value_ = function(self, field, value) -- process each single received field - local cur_namespace = self:getNamespace() - if cur_namespace == "" or cur_namespace == string.sub(field, 1, string.len(cur_namespace)) then -- Check current namespace - local rel_fieldname = string.sub(field, string.len(cur_namespace)+1) --cut the namespace - if self._ele[rel_fieldname] then -- direct top-level assignment - self._ele[rel_fieldname].data.value = value - else - for elename, eledef in pairs(self._ele) do - if eledef.getViewState then -- element supports sub-states - eledef:getViewState():_sfs_process_value_(field, value) - end - end - end + show = location._show_, + _get_element_recursive_ = function(self, field) + local topfield + for z in field:gmatch("[^#]+") do + topfield = z + break end - end, - -- process action for received field if supported - _sfs_process_action_ = function(self, field, value, player) - local cur_namespace = self:getNamespace() - if cur_namespace == "" or cur_namespace == string.sub(field, 1, string.len(cur_namespace)) then -- Check current namespace - local rel_fieldname = string.sub(field, string.len(cur_namespace)+1) --cut the namespace - if self._ele[rel_fieldname] then -- direct top-level assignment - if self._ele[rel_fieldname].submit then - self._ele[rel_fieldname]:submit(value, player) - end + local element = self._ele[topfield] + if element and field == topfield then + return element + elseif element then + if element._getSubElement_ then + local rel_field = string.sub(field, string.len(topfield)+2) + return element:_getSubElement_(rel_field) else - for elename, eledef in pairs(self._ele) do - if eledef.getViewState then -- element supports sub-states - eledef:getViewState():_sfs_process_action_(field, value, player) - end - end + return element end + else + return nil end end, -- process onInput hook for the state - _sfs_process_oninput_ = function(self, fields, player) --process hooks - -- call onInput hook if enabled + _sfs_process_oninput_ = function(self, fields, player) if self._onInput then self:_onInput(fields, player) end - -- recursive all onInput hooks on visible views + -- recursive all onInput hooks on visible containers for elename, eledef in pairs(self._ele) do - if eledef.getViewState and not eledef:getIsHidden() then - eledef:getViewState():_sfs_process_oninput_(fields, player) + if eledef.getContainerState and eledef:getVisible() then + eledef:getContainerState():_sfs_process_oninput_(fields, player) end end end, @@ -408,24 +490,32 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) _sfs_on_receive_fields_ = function(self, player, fields) -- fields assignment for field, value in pairs(fields) do - self:_sfs_process_value_(field, value) + local element = self:_get_element_recursive_(field) + if element then + element:setValue(value) + end end -- process onInput hooks self:_sfs_process_oninput_(fields, player) - -- do actions for field, value in pairs(fields) do - self:_sfs_process_action_(field, value, player) - end - - if not fields.quit and not self.closed and not self.obsolete then - self:_show_() - else - -- to be closed - self.players:disconnect(player) - if self.location.type == "player" then - smartfs.opened[player] = nil + local element = self:_get_element_recursive_(field) + if element and element.submit then + element:submit(value, player) end + end + -- handle key_enter + if fields.key_enter and fields.key_enter_field then + local element = self:_get_element_recursive_(fields.key_enter_field) + if element and element.submit_key_enter then + element:submit_key_enter(fields[fields.key_enter_field], player) + end + end + -- handle quit/exit + if not fields.quit and not self.closed and not self.obsolete then + self:show() + else + self.players:disconnect(player) if not fields.quit and self.closed and not self.obsolete then --closed by application (without fields.quit). currently not supported, see: https://github.com/minetest/minetest/pull/4675 minetest.show_formspec(player,"","size[5,1]label[0,0;Formspec closing not yet created!]") @@ -471,24 +561,6 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) end return false end, - getSize = function(self) - return self._size - end, - size = function(self,w,h) - self._size = {w=w,h=h} - end, - setSize = function(self,w,h) - self._size = {w=w,h=h} - end, - getNamespace = function(self) - local ref = self - local namespace = "" - while ref.location.type == "view" do - namespace = ref.location.viewElement.name.."#"..namespace - ref = ref.location.parentState -- step near to the root - end - return namespace - end, setparam = function(self,key,value) if not key then return end self.param[key] = value @@ -498,19 +570,12 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) if not key then return end return self.param[key] or default end, - ------------------------------------------------------ - -- Element instance creatior as state method - ------------------------------------------------------ - element = function(self, typen, data) + element = function(self,typen,data) local type = smartfs._edef[typen] assert(type, "Element type "..typen.." does not exist!") assert(not self._ele[data.name], "Element "..data.name.." already exists") data.type = typen - - ------------------------------------------------------ - -- Element instance template / abstract - ------------------------------------------------------ local ele = { name = data.name, root = self, @@ -524,95 +589,25 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) getPosition = function(self) return self.data.pos end, - getAbsolutePosition = function(self) - if not self.root.location.type then - print("SmartFS - (ERROR): self.root.location.type missed:", dump(self)) - break_execution_bug() --stop - end - if self.root.location.type == "view" then --it is a view. Calculate delta - local relapos = self:getPosition() - local viewpos = self.root.location.viewElement:getAbsolutePosition() - local abspos = {} - abspos.x = viewpos.x + relapos.x - abspos.y = viewpos.y + relapos.y - return abspos - else - return self:getPosition() --get current - end - end, setSize = function(self,w,h) self.data.size = {w=w,h=h} end, getSize = function(self) return self.data.size end, - getCuttedSize = function(self) - local allowed_overlap = 0.5 - local elementsize = self:getSize() - if not elementsize then - return nil - end - -- get parent view or window size - local viewsize - if self.root.location and self.root.location.type == "view" then - viewsize = self.root.location.viewElement:getCuttedSize() -- cute a view + setVisible = function(self, visible) + if visible == nil then + self.data.visible = true else - viewsize = self.root:getSize() --root cannot be cuted + self.data.visible = visible end - if not viewsize then --view full-cuted - return nil - end - -- check for overlapping - local pos_in_view = self:getPosition() - local cutedsize = {} - if viewsize.w - pos_in_view.x + allowed_overlap < 0 then - print("SmartFS - (Warning): element "..self.name.." outside of view:"..viewsize.w.." x:"..pos_in_view.x) - return nil - elseif viewsize.w - pos_in_view.x + allowed_overlap < elementsize.w then - print("SmartFS - (Warning): element "..self.name.." cuted. view width:"..viewsize.w.." x:"..pos_in_view.x.." element width:"..elementsize.w) - cutedsize.w = viewsize.w - pos_in_view.x + allowed_overlap - else - cutedsize.w = elementsize.w - end - if viewsize.h - pos_in_view.y + allowed_overlap < 0 then - print("SmartFS - (Warning): element "..self.name.." outside of view:"..viewsize.h.." y:"..pos_in_view.y) - return nil - elseif viewsize.h - pos_in_view.y + allowed_overlap < elementsize.h then - print("SmartFS - (Warning): element "..self.name.." cuted. view hight:"..viewsize.h.." y:"..pos_in_view.y.." element hight:"..elementsize.h) - cutedsize.h = viewsize.h - pos_in_view.y + allowed_overlap - else - cutedsize.h = elementsize.h - end - return cutedsize end, - setIsHidden = function(self, hidden) - self.data.hidden = hidden - end, - getIsHidden = function(self) - return self.data.hidden - end, - getIsHiddenOrCutted = function(self) - if not self:getCuttedSize() then - print("SmartFS - (Warning): element: "..self.name.."is outside of view") - return true - else - return self:getIsHidden() - end + getVisible = function(self) + return self.data.visible end, getAbsName = function(self) return self.root:getNamespace()..self.name end, - getPosString = function(self) - local pos = self:getAbsolutePosition() - return pos.x..","..pos.y - end, - getSizeString = function(self) - local size = self:getCuttedSize() - if not size then - return "" - end - return size.w..","..size.h - end, setBackground = function(self, image) self.data.background = image end, @@ -621,22 +616,25 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) end, getBackgroundString = function(self) if self.data.background then - return "background[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self.data.background.."]" + local size = self:getSize() + if size then + return "background[".. + self.data.pos.x..","..self.data.pos.y..";".. + size.w..","..size.h..";".. + self.data.background.."]" + else + return "" + end else return "" end end, + setValue = function(self, value) + self.data.value = value + end, } - ------------------------------------------------------ - -- Element instance construction - ------------------------------------------------------ - if not ele.data.size then - ele.data.size = {w=0.5,h=0.5} --dummy size for elements without size (label + checkbox) - end + ele.data.visible = true --visible by default for key, val in pairs(type) do ele[key] = val @@ -648,18 +646,6 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) return self._ele[data.name] end, - loadTemplate = function(self, template) - -- template can be a function (usable in smartfs.create()), a form name or object ( a smartfs.create() result) - if type(template) == "function" then -- asume it is a smartfs.create() usable function - return template(self) - elseif type(template) == "string" then -- asume it is a form name - return smartfs.__call(self, template)._reg(self) - elseif type(template) == "table" then --asume it is an other state - if template._reg then - template._reg(self) - end - end - end, ------------------------------------------------------ -- State - Element Constructors @@ -673,11 +659,40 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) closes = exitf or false }) end, + image_button = function(self, x, y, w, h, name, text, image, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + image = image, + closes = exitf or false + }) + end, + item_image_button = function(self, x, y, w, h, name, text, item, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + item = item, + closes = exitf or false + }) + end, label = function(self, x, y, name, text) return self:element("label", { pos = {x=x,y=y}, name = name, - value = text + value = text, + vertical = false + }) + end, + vertlabel = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = true }) end, toggle = function(self, x, y, w, h, name, list) @@ -725,7 +740,26 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) pos = {x=x, y=y}, size = {w=w, h=h}, name = name, - value = img + value = img, + imgtype = "image" + }) + end, + background = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + background = img, + imgtype = "background" + }) + end, + item_image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "item" }) end, checkbox = function(self, x, y, name, label, selected) @@ -760,10 +794,18 @@ function smartfs._makeState_(form, newplayer, params, is_inv, nodepos) name = name }) end, - view = function(self, x, y, name) - return self:element("view", { + container = function(self, x, y, name, relative) + return self:element("container", { pos = {x=x, y=y}, - name = name + name = name, + relative = false + }) + end, + view = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = true }) end, } @@ -781,41 +823,46 @@ smartfs.element("button", { assert(self.data.value, "button needs label") end, build = function(self) - if self.data.img then - return "image_button[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self.data.img..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.value).."]".. - self:getBackgroundString() + local specstring + if self.data.image then + if self.data.closes then + specstring = "image_button_exit[" + else + specstring = "image_button[" + end + elseif self.data.item then + if self.data.closes then + specstring = "item_image_button_exit[" + else + specstring = "item_image_button[" + end else if self.data.closes then - return "button_exit[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.value).."]".. - self:getBackgroundString() + specstring = "button_exit[" else - return "button[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.value).."]".. - self:getBackgroundString() + specstring = "button[" end end + + specstring = specstring .. + self.data.pos.x..","..self.data.pos.y..";".. + self.data.size.w..","..self.data.size.h..";" + if self.data.image then + specstring = specstring..self.data.image..";" + elseif self.data.item then + specstring = specstring..self.data.item..";" + end + specstring = specstring..self:getAbsName()..";".. + minetest.formspec_escape(self.data.value).."]" + if self.data.tooltip then + specstring = specstring.."tooltip["..self:getAbsName()..";"..self.data.tooltip.."]" + end + return specstring end, submit = function(self, field, player) if self._click then self:_click(self.root, player) end - --[[ not needed. there is a quit field received in this case - if self.data.closes then - self.root.location.rootState:close() - end - ]]-- end, onClick = function(self,func) self._click = func @@ -824,19 +871,36 @@ smartfs.element("button", { self._click = func end, setText = function(self,text) - self.data.value = text + self:setValue(text) end, getText = function(self) return self.data.value end, setImage = function(self,image) - self.data.img = image + self.data.image = image + self.data.item = nil end, getImage = function(self) - return self.data.img + return self.data.image + end, + setItem = function(self,item) + self.data.item = item + self.data.image = nil + end, + getItem = function(self) + return self.data.item + end, + setTooltip = function(self,text) + self.data.tooltip = minetest.formspec_escape(text) + end, + getTooltip = function(self) + return self.data.tooltip end, setClose = function(self,bool) self.data.closes = bool + end, + getClose = function(self) + return self.data.closes or false end }) @@ -849,11 +913,14 @@ smartfs.element("toggle", { end, build = function(self) return "button[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.list[self.data.id]).."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.list[self.data.id]).. + "]" end, submit = function(self, field, player) self.data.id = self.data.id + 1 @@ -884,13 +951,20 @@ smartfs.element("label", { assert(self.data.value, "label needs text") end, build = function(self) - return "label[".. - self:getPosString()..";".. - minetest.formspec_escape(self.data.value).."]".. - self:getBackgroundString() + local specstring + if self.data.vertical then + specstring = "vertlabel[" + else + specstring = "label[" + end + return specstring.. + self.data.pos.x..","..self.data.pos.y.. + ";".. + minetest.formspec_escape(self.data.value).. + "]" end, setText = function(self,text) - self.data.value = text + self:setValue(text) end, getText = function(self) return self.data.value @@ -908,31 +982,44 @@ smartfs.element("field", { build = function(self) if self.data.ml then return "textarea[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.label)..";".. - minetest.formspec_escape(self.data.value).."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]" elseif self.data.pwd then return "pwdfield[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.label).."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + "]".. + self:getCloseOnEnterString() else return "field[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.label)..";".. - minetest.formspec_escape(self.data.value).."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]".. + self:getCloseOnEnterString() end end, setText = function(self,text) - self.data.value = text + self:setValue(text) end, getText = function(self) return self.data.value @@ -942,7 +1029,28 @@ smartfs.element("field", { end, isMultiline = function(self,bool) self.data.ml = bool - end + end, + getCloseOnEnterString = function(self) + if self.close_on_enter == nil then + return "" + else + return "field_close_on_enter["..self:getAbsName()..";"..tostring(self.close_on_enter).."]" + end + end, + setCloseOnEnter = function(self, value) + self.close_on_enter = value + end, + getCloseOnEnter = function(self) + return self.close_on_enter + end, + submit_key_enter = function(self, field, player) + if self._key_enter then + self:_key_enter(self.root, player) + end + end, + onKeyEnter = function(self,func) + self._key_enter = func + end, }) smartfs.element("image", { @@ -952,16 +1060,39 @@ smartfs.element("image", { self.data.value = self.data.value or "" end, build = function(self) - return "image[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self.data.value.."]" + if self.data.imgtype == "background" then + return "" -- handled in _buildFormspec_ trough getBackgroundString() + elseif self.data.imgtype == "item" then + return "item_image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + else + return "image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + end end, setImage = function(self,text) - self.data.value = text + if self.data.imgtype == "background" then + self.data.background = text + else + self:setValue(text) + end end, getImage = function(self) - return self.data.value + if self.data.imgtype == "background" then + return self.data.background + else + return self.data.value + end end }) @@ -973,20 +1104,15 @@ smartfs.element("checkbox", { self.data.label = self.data.label or "" end, build = function(self) - if self.data.value then - self.data.value = "true" - else - self.data.value = "false" - end return "checkbox[".. - self:getPosString()..";".. - self:getAbsName()..";".. - minetest.formspec_escape(self.data.label)..";".. - boolToStr(self.data.value) .."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";" .. boolToStr(self.data.value) .."]" end, submit = function(self, field, player) - -- self.data.value already set by value transfer -- call the toggle function if defined if self._tog then self:_tog(self.root, player) @@ -1016,13 +1142,17 @@ smartfs.element("list", { self.data.items = {} end return "textlist[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - table.concat(self.data.items, ",")..";".. - tostring(self.data.selected or "")..";".. - tostring(self.data.transparent or "false").."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(self.data.items, ",").. + ";".. + tostring(self.data.selected or "").. + ";".. + tostring(self.data.transparent or "false").."]" end, submit = function(self, field, player) local _type = string.sub(field,1,3) @@ -1047,7 +1177,7 @@ smartfs.element("list", { self._doubleClick = func end, addItem = function(self, item) - table.insert(self.data.items, item) + table.insert(self.data.items, minetest.formspec_escape(item)) -- return the index of item. It is the last one return #self.data.items end, @@ -1087,12 +1217,16 @@ smartfs.element("dropdown", { end, build = function(self) return "dropdown[".. - self:getPosString()..";".. - self:getSizeString()..";".. - self:getAbsName()..";".. - table.concat(self.data.items, ",")..";".. - tostring(self:getSelected()).."]".. - self:getBackgroundString() + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(self.data.items, ",").. + ";".. + tostring(self:getSelected()).. + "]" end, submit = function(self, field, player) self:getSelected() @@ -1153,12 +1287,16 @@ smartfs.element("inventory", { end, build = function(self) return "list[".. - (self.data.location or "current_player") ..";".. - self.name..";".. --no namespacing - self:getPosString()..";".. - self:getSizeString()..";".. - (self.data.index or "").."]".. - self:getBackgroundString() + (self.data.invlocation or "current_player") .. + ";".. + self.name.. --no namespacing + ";".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + (self.data.index or "") .. + "]" end, -- available inventory locations -- "current_player": Player to whom the menu is shown @@ -1166,20 +1304,20 @@ smartfs.element("inventory", { -- "nodemeta:,,": Any node metadata -- "detached:": A detached inventory -- "context" does not apply to smartfs, since there is no node-metadata as context available - setLocation = function(self,location) - self.data.location = location + setLocation = function(self,invlocation) + self.data.invlocation = invlocation end, getLocation = function(self) - return self.data.location or "current_player" + return self.data.invlocation or "current_player" end, usePosition = function(self, pos) - self.data.location = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z) + self.data.invlocation = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z) end, usePlayer = function(self, name) - self.data.location = "player:" .. name + self.data.invlocation = "player:" .. name end, useDetached = function(self, name) - self.data.location = "detached:" .. name + self.data.invlocation = "detached:" .. name end, setIndex = function(self,index) self.data.index = index @@ -1197,6 +1335,7 @@ smartfs.element("code", { if self._build then self:_build() end + return self.data.code end, submit = function(self, field, player) @@ -1218,28 +1357,38 @@ smartfs.element("code", { end }) -smartfs.element("view", { +smartfs.element("container", { onCreate = function(self) - assert(self.data.pos and self.data.pos.x and self.data.pos.y, "view needs valid pos") - assert(self.name, "view needs name") - self._state = smartfs._makeState_(self, nil, self.root.param) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "container needs valid pos") + assert(self.name, "container needs name") + local statelocation = smartfs._ldef.container._make_state_location_(self) + self._state = smartfs._makeState_(nil, self.root.param, statelocation) end, - -- redefinitions. The size is not handled by data.size but by view-state:size + + -- redefinitions. The size is not handled by data.size but by container-state:size setSize = function(self,w,h) - self:getViewState():setSize(w,h) + self:getContainerState():setSize(w,h) end, getSize = function(self) - return self:getViewState():getSize() + return self:getContainerState():getSize() end, + -- element interface methods build = function(self) - --the background string is "under" the view. Parts of this background can be overriden by elements (with background) from view - return self:getBackgroundString()..self:getViewState():_buildFormspec_(false) + if self.data.relative ~= true then + return "container["..self.data.pos.x..","..self.data.pos.y.."]".. + self:getContainerState():_buildFormspec_(false).. + "container_end[]" + else + return self:getContainerState():_buildFormspec_(false) + end end, - getViewState = function(self) + getContainerState = function(self) return self._state - end - -- submit is handled by framework for elements with getViewState + end, + _getSubElement_ = function(self, field) + return self:getContainerState():_get_element_recursive_(field) + end, }) return smartfs