Get all already implemented working with hsl

additional futures: support for mts files
more intelligent npc
This commit is contained in:
Alexander Weber 2017-01-07 23:27:44 +01:00
parent cb34bad5e3
commit 5aeb2388b0
16 changed files with 951 additions and 1636 deletions

View File

@ -6,16 +6,26 @@ A minetest mod contains a chest with building definitions. The chest can spawn a
## Features
- WorldEdit file target
- A Chest that allows you to choose a building and manage the building options
- The most WorldEdit files are supported. just put the file to the buildings directory
- Building target from file
- A Chest that allow you to choose a building and manage the building options
- The most ".we", ".wem" or ".mts" files are supported. just put the file to the buildings directory
- Flatting and cleanup the building place, remove all cruft-nodes from building inside
- Cleanup unknown nodes
- Ground level detection trough dirt_with_* nodes
- Optimized for realy big buildings. Try the AMMOnym_coloseum.we as a showcase
- Creative build
- Instant build allow you to get the building instantly
- Building target configured
- Some simple tasks implemented:
- Fill with air
- Fill with stone
- Build a box
- Build a plate
- Survival mode
- TODO
- Creative mode
- Instant build allow you to get the result instantly
- Creative build by NPC without providing needed nodes
- Builder-NPC's
@ -32,14 +42,21 @@ A minetest mod contains a chest with building definitions. The chest can spawn a
- a way to change the needed nodes (like in building defined default:wood but I like to use something from moretrees mod)
- the mapping should be able to map the unknown nodes
- Support mts files
## Vision / Ideas / maybe
- other chests that generates a plan. The generated plan can be the daily work of a lumberjack as example
- Architect-NPC instead of the chest to coordinate the build
- Interface (API) to allow other mods create own "Plan coordinator"'s
## Credits
- cornernote - made the towntest mod that was used as template for townchest
- PilzAdam - made the initial NPC entity movement code (towntest)
# Code
- cornernote - towntest mod was used as template for townchest
- rubenwardy - take my smartfs enhancements upstream ready
npcf_ng builder ousted the original towntest builder
- Sokomine - handle_schematics is the base of hsl (=handle_schematics library)
# Buildings
- VanessaE - contributed buildings (towntest)
- kddekadenz - contributed buildings (towntest)
- ACDC - contributed buildings (towntest)
@ -47,6 +64,7 @@ A minetest mod contains a chest with building definitions. The chest can spawn a
- irksomeduck - contributed buildings (towntest)
- AMMOnym_coloseum.we https://forum.minetest.net/viewtopic.php?p=121294#p121294
- PEAK_BremerHaus.we https://forum.minetest.net/viewtopic.php?p=207103#p207103
- All contributions welcome!

234
chest.lua
View File

@ -1,8 +1,7 @@
local dprint = townchest.dprint --debug
local dprint = townchest.dprint_off --debug
local smartfs = townchest.smartfs
local preparing_plan_chunk = 10000
local ASYNC_WAIT=0.2 -- schould be > 0 to restrict performance consumption
--------------------------------------
-- class attributes and methods
--------------------------------------
@ -64,7 +63,7 @@ townchest.chest.new = function()
if formname == "file_open" then
infotext = "please select a building"
elseif formname == "build_status" then
infotext = "Nodes in plan: "..self.plan.building_size
infotext = "Nodes in plan: "..self.plan.data.nodecount
else
infotext = self.infotext or ""
end
@ -94,8 +93,8 @@ 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)
local we = {}
if taskname then
self.info.taskname = taskname
@ -103,8 +102,17 @@ townchest.chest.new = function()
if self.info.taskname == "file" then
-- check if file could be read
we = townchest.files.readfile(self.info.filename)
if not we or #we == 0 then
if not self.info.filename then
-- something wrong, back to file selection
minetest.after(0, self.set_form, self, "file_open")
self.current_stage = "select"
self:persist_info()
return
end
self.plan.data = townchest.files.readfile(self.info.filename)
if not self.plan.data then
self.infotext = "No building found in ".. self.info.filename
self:set_form("status")
self.current_stage = "select"
@ -115,58 +123,64 @@ 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 = {}
if self.info.genblock.variant == 1 then
-- Fill with air
for x = 0, self.info.genblock.x-1 do
for y = 0, self.info.genblock.y-1 do
for z = 0, self.info.genblock.z-1 do
table.insert(we, {x=x,y=y,z=z, name = "air"})
end
end
end
-- 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 = 0, self.info.genblock.x-1 do
for y = 0, self.info.genblock.y-1 do
for z = 0, self.info.genblock.z-1 do
table.insert(we, {x=x,y=y,z=z, name = "default:cobble"})
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})
end
end
end
elseif self.info.genblock.variant == 3 then
-- Build a box
for x = 0, self.info.genblock.x-1 do
for y = 0, self.info.genblock.y-1 do
for z = 0, self.info.genblock.z-1 do
if x == 0 or x == self.info.genblock.x-1 or
y == 0 or y == self.info.genblock.y-1 or
z == 0 or z == self.info.genblock.z-1 then
table.insert(we, {x=x,y=y,z=z, name = "default:cobble"})
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})
end
end
end
end
-- build ground level under chest
self.plan.relative.ground_y = 1
self.plan.data.ground_y = 1
-- Build a plate
elseif self.info.genblock.variant == 4 then
local y = 0
for x = 0, self.info.genblock.x-1 do
for z = 0, self.info.genblock.z-1 do
table.insert(we, {x=x,y=y,z=z, name = "default:cobble"})
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})
end
end
-- build ground level under chest
self.plan.relative.ground_y = 1
self.plan.data.ground_y = 1
end
end
self.rawdata = we
self:run_async(self.prepare_building_plan_chain)
-- TODO: go to customizing screen
self.infotext = "Build preparation"
self:set_form("status")
self:run_async(self.prepare_building_plan)
end
@ -187,94 +201,122 @@ townchest.chest.new = function()
end
self:persist_info()
minetest.after(0.2, async_call, self.pos)
end
--------------------------------------
-- Async task: create building plan from rawdata
--------------------------------------
function self.prepare_building_plan_chain(self)
local chunksize, lastchunk
-- go trough all file entries
if #self.rawdata > preparing_plan_chunk then
chunksize = preparing_plan_chunk
lastchunk = true
else
chunksize = #self.rawdata
end
for i=#self.rawdata, #self.rawdata-chunksize+1, -1 do
-- map to the internal node format
local wenode = self.rawdata[i]
if wenode and wenode.x and wenode.y and wenode.z and wenode.name then
self.plan:adjust_flatting_requrement(wenode)
local node = townchest.nodes.new(self.rawdata[i]):map() --mapped
if node and node.x and node.y and node.z then
self.plan:add_node(node)
end
end
self.rawdata[i] = nil
end
if lastchunk then
dprint("next processing chunk")
self.infotext = "Preparing, nodes left: "..#self.rawdata
self:set_form("status")
return true --repeat async call
else
dprint("reading of building done. Save them to the chest metadata")
self.infotext = "Reading done, preparing"
self:set_form("status")
self:run_async(self.prepare_building_plan_chain_postprocess)
return false
end
minetest.after(ASYNC_WAIT, async_call, self.pos)
end
--------------------------------------
-- Async task: Post-processing of plan preparation
--------------------------------------
function self.prepare_building_plan_chain_postprocess(self)
self.plan:prepare()
function self.prepare_building_plan(self)
self.plan:flood_with_air()
-- self.plan:do_mapping() -- on demand called
self.current_stage = "ready"
self:set_form("build_status")
self:persist_info()
self:run_async(self.instant_build_chain) --just trigger, there is a check if active
self:run_async(self.instant_build_chunk) --just trigger, there is a check if active
end
--------------------------------------
-- Async Task: Do a instant build step
--------------------------------------
function self.instant_build_chain(self)
function self.instant_build_chunk(self)
if not self.info.instantbuild == true then --instantbuild disabled
return
end
dprint("Instant build is running")
dprint("--- Instant build is running")
local startingnode = self.plan:get_nodes(1)
-- go trough all file entries
if startingnode[1] then -- the one node given
dprint("start building chunk for", minetest.pos_to_string(startingnode[1]))
minetest.forceload_block(self.plan:get_world_pos(startingnode[1]))
for idx, node in ipairs(self.plan:get_nodes_from_chunk(startingnode[1])) do
local wpos = self.plan:get_world_pos(node)
if wpos.x ~= self.pos.x or wpos.y ~= self.pos.y or wpos.z ~= self.pos.z then --skip chest pos
--- Place node
local startingpos = self.plan:get_random_node_pos()
if not startingpos 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
local wpos = self.plan:get_world_pos(nodeplan)
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(nodeplan, wpos)
if node then
minetest.env:add_node(wpos, node)
if node.meta then
minetest.env:get_meta(wpos):from_table(node.meta)
end
end
self.plan:set_node_processed(node)
end
minetest.forceload_free_block(self.plan:get_world_pos(startingnode[1]))
self.plan:remove_node(nodeplan)
end
-- chunk done handle next chunk call
dprint("instant nodes left:", self.plan.data.nodecount)
self:update_info("build_status")
if self.plan.building_size > 0 then
if self.plan.data.nodecount > 0 then
--start next plan chain
return true
else
-- finished. disable processing
self.instantbuild = false
self.info.instantbuild = false
return false
end
end
@ -310,8 +352,10 @@ townchest.chest.new = function()
dprint("restoral not necessary, current stage is", self.current_stage)
return
end
if chestinfo.taskname then -- file selected but no plan. Restore the plan
self.current_stage = "restore"
self:persist_info()
self:set_rawdata(chestinfo.taskname)
else
self:set_form("file_open")

View File

@ -1,15 +1,13 @@
local dprint = townchest.dprint_off --debug
-- get worldedit parser load_schematic from worldedit mod
dofile(townchest.modpath.."/".."worldedit-serialization.lua")
files = {}
-----------------------------------------------
-- get files
-- no input parameters
-- returns a table containing buildings
-----------------------------------------------
local __get = function()
function files.get()
local files = {}
if os.getenv('HOME')~=nil then
dprint("use GNU tools to get files")
@ -37,20 +35,24 @@ end
-- filename - the building file to load
-- return - WE-Shema, containing the pos and nodes to build
-----------------------------------------------
local __readfile = function(filename)
local filepath = townchest.modpath.."/buildings/"..filename
local file, err = io.open(filepath, "rb")
if err ~= nil then
dprint("[townchest] error: could not open file \"" .. filepath .. "\"")
function files.readfile(filename)
local file = townchest.hsl.save_restore.file_access(townchest.modpath.."/buildings/"..filename, "r")
if not file then
dprint("[townchest] error: could not open file \"" .. filename .. "\"")
return
end
-- load the building starting from the lowest y
local building_plan = townchest.we_load_schematic(file:read("*a"))
return building_plan
local building_info
-- different file types
if string.find( filename, '.mts', -4 ) then
return townchest.hsl.analyze_mts.analyze_mts_file(file)
end
if string.find( filename, '.we', -3 ) or string.find( filename, '.wem', -4 ) then
return townchest.hsl.analyze_we.analyze_we_file(file)
end
end
townchest.files = {
get = __get,
readfile = __readfile
}
------------------------------------------
-- return the files methods to the caller
return files

View File

@ -23,11 +23,11 @@ local smartfs = townchest.smartfs
dofile(townchest.modpath.."/chest.lua")
-- Reading building files (WorldEdit)
dofile(townchest.modpath.."/files.lua")
townchest.files = dofile(townchest.modpath.."/files.lua")
townchest.hsl = dofile(townchest.modpath.."/libs/hsl/init.lua")
-- Nodes mapping
dofile(townchest.modpath.."/nodes.lua")
dofile(townchest.modpath.."/mapping.lua")
-- building plan
dofile(townchest.modpath.."/plan.lua")

View File

@ -1,217 +0,0 @@
local handle_schematics = {}
-- This code is used to read Minecraft schematic files.
--
-- The .schematic file format is described here:
-- http://minecraft.gamepedia.com/Schematic_file_format?cookieSetup=true
-- It is based on the NBT format, which is described here:
-- http://minecraft.gamepedia.com/NBT_Format?cookieSetup=true
-- position in the decompressed string data_stream
local curr_index = 1;
-- helper arry so that the values do not have to be calculated anew each time
local pot256 = { 1 };
for i=1,8 do
pot256[ #pot256+1 ] = pot256[ #pot256 ] * 256;
end
-- read length bytes from data_stream and turn it into an integer value
local read_signed = function( data_stream, length)
local res = 0;
for i=length,1,-1 do
res = res + (string.byte( data_stream, curr_index )* pot256[ i ]);
-- move one further
curr_index = curr_index+1;
end
return res;
end
-- this table will collect the few tags we're actually intrested in
local mc_schematic_data = {};
-- this will be a recursive function
local read_tag;
-- needs to be defined now because it will contain a recursive function
local read_one_tag;
-- read payload of one tag (= a data element in a NBT data structure)
read_one_tag = function( data_stream, tag, title_tag )
if( tag<= 0 or not(data_stream)) then
return;
elseif( tag==1 ) then -- TAG_BYTE: 1 byte
return read_signed( data_stream, 1 );
elseif( tag==2 ) then -- TAG_SHORT: 2 bytes
return read_signed( data_stream, 2 );
elseif( tag==3 ) then -- TAG_INT: 4 bytes
return read_signed( data_stream, 4 );
elseif( tag==4 ) then -- TAG_LONG: 8 bytes
return read_signed( data_stream, 8 );
elseif( tag==5 ) then -- TAG_FLOAT: 4 bytes
return read_signed( data_stream, 4 ); -- the float values are unused here
elseif( tag==6 ) then -- TAG_DOUBLE: 8 bytes
return read_signed( data_stream, 8 ); -- the float values are unused here
elseif( tag==7 ) then -- TAG_Byte_Array
local size = read_signed( data_stream, 4 ); -- TAG_INT
local res = {};
for i=1,size do
-- a Byte_Array does not contain any sub-tags
res[i] = read_one_tag( data_stream, 1, nil ); -- read TAG_BYTE
end
return res;
elseif( tag==8 ) then -- TAG_String
local size = read_signed( data_stream, 2);
local res = string.sub( data_stream, curr_index, curr_index+size-1 );
-- move on in the data stream
curr_index = curr_index + size;
return res;
elseif( tag==9 ) then -- TAG_List
-- these exact values are not particulary intresting
local tagtyp = read_signed( data_stream, 1 ); -- TAG_BYTE
local size = read_signed( data_stream, 4 ); -- TAG_INT
local res = {};
for i=1,size do
-- we need to pass title_tag on to the "child"
res[i] = read_one_tag( data_stream, tagtyp, title_tag );
end
return res;
elseif( tag==10 ) then -- TAG_Compound
return read_tag( data_stream, title_tag );
elseif( tag==11 ) then -- TAG_Int_Array
local size = read_signed( data_stream, 4 ); -- TAG_INT
local res = {};
for i=1,size do
-- a Int_Array does not contain any sub-tags
res[i] = read_one_tag( data_stream, 3, nil ); -- TAG_INT
end
return res;
end
end
-- read tag type, tag name and call read_one_tag to get the payload;
read_tag = function( data_stream, title_tag )
local schematic_data = {};
while( data_stream ) do
local tag = string.byte( data_stream, curr_index);
-- move on in the data stream
curr_index = curr_index + 1;
if( not( tag ) or tag <= 0 ) then
return;
end
local tag_name_length = string.byte( data_stream, curr_index ) * 256 + string.byte(data_stream, curr_index + 1);
-- move 2 further
curr_index = curr_index + 2;
local tag_name = string.sub( data_stream, curr_index, curr_index+tag_name_length-1 );
-- move on...
curr_index = curr_index + tag_name_length;
--print('[analyze_mc_schematic_file] Found: Tag '..tostring( tag )..' <'..tostring( tag_name )..'>');
local res = read_one_tag( data_stream, tag, tag_name );
-- Entities and TileEntities are ignored
if( title_tag == 'Schematic'
and( tag_name == 'Width'
or tag_name == 'Height'
or tag_name == 'Length'
or tag_name == 'Materials' -- "Classic" or "Alpha" (=Survival)
or tag_name == 'Blocks'
or tag_name == 'Data'
)) then
mc_schematic_data[ tag_name ] = res;
end
end
return;
end
handle_schematics.analyze_mc_schematic_file = function( path )
-- these files are usually compressed; there is no point to start if the
-- decompress function is missing
if( minetest.decompress == nil) then
return nil;
end
local file, err = save_restore.file_access(path..'.schematic', "rb")
if (file == nil) then
-- print('[analyze_mc_schematic_file] ERROR: NO such file: '..tostring( path..'.schematic'));
return nil
end
local compressed_data = file:read( "*all" );
--local data_string = minetest.decompress(compressed_data, "deflate" );
local data_string = compressed_data; -- TODO
print('FILE SIZE: '..tostring( string.len( data_string ))); -- TODO
file.close(file)
-- we use this (to this file) global variable to store gathered information;
-- doing so inside the recursive functions proved problematic
mc_schematic_data = {};
-- this index will iterate through the schematic data
curr_index = 1;
-- actually analyze the data
read_tag( data_string, nil );
if( not( mc_schematic_data.Width )
or not( mc_schematic_data.Height )
or not( mc_schematic_data.Length )
or not( mc_schematic_data.Blocks )
or not( mc_schematic_data.Data )) then
print('[analyze_mc_schematic_file] ERROR: Failed to analyze '..tostring( path..'.schematic'));
return nil;
end
local translation_function = handle_schematics.findMC2MTConversion;
if( minetest.get_modpath('mccompat')) then
translation_function = mccompat.findMC2MTConversion;
end
local max_msg = 40; -- just for error handling
local size = {x=mc_schematic_data.Width, y=mc_schematic_data.Height, z=mc_schematic_data.Length};
local scm = {};
local nodenames = {};
local nodenames_id = {};
for y=1,size.y do
scm[y] = {};
for x=1,size.x do
scm[y][x] = {};
for z =1,size.z do
local new_node = translation_function(
-- (Y×length + Z)×width + X.
mc_schematic_data.Blocks[ ((y-1)*size.z + (z-1) )*size.x + (size.x-x) +1],
mc_schematic_data.Data[ ((y-1)*size.z + (z-1) )*size.x + (size.x-x) +1] );
-- some MC nodes store the information about a node in TWO block and data fields (doors, large flowers, ...)
if( new_node[3] and new_node[3]~=0 ) then
new_node = translation_function(
-- (Y×length + Z)×width + X.
mc_schematic_data.Blocks[ ((y-1)*size.z + (z-1) )*size.x + (size.x-x) +1],
mc_schematic_data.Data[ ((y-1)*size.z + (z-1) )*size.x + (size.x-x) +1],
mc_schematic_data.Blocks[ ((y-1+new_node[3])*size.z + (z-1) )*size.x + (size.x-x) +1],
mc_schematic_data.Data[ ((y-1+new_node[3])*size.z + (z-1) )*size.x + (size.x-x) +1] );
end
if( not( nodenames_id[ new_node[1]] )) then
nodenames_id[ new_node[1] ] = #nodenames + 1;
nodenames[ nodenames_id[ new_node[1] ]] = new_node[1];
end
-- print a few warning messages in case something goes wrong - but do not exaggerate
if( not( new_node[2] and max_msg>0)) then
-- print('[handle_schematics:schematic] MISSING param2: '..minetest.serialize( new_node ));
new_node[2]=0;
max_msg=max_msg-1;
end
-- save some space by not saving air
if( new_node[1] ~= 'air' ) then
scm[y][x][z] = { nodenames_id[ new_node[1]], new_node[2]};
end
end
end
end
return { size = { x=size.x, y=size.y, z=size.z}, nodenames = nodenames, on_constr = {}, after_place_node = {}, rotated=90, burried=0, scm_data_cache = scm, metadata = {}};
end
return handle_schematics

View File

@ -37,20 +37,10 @@ local handle_schematics = {}
-- gets the size of a structure file
-- nodenames: contains all the node names that are used in the schematic
-- on_constr: lists all the node names for which on_construct has to be called after placement of the schematic
handle_schematics.analyze_mts_file = function( path )
handle_schematics.analyze_mts_file = function(file)
local size = { x = 0, y = 0, z = 0, version = 0 }
local version = 0;
local file, err = save_restore.file_access(path..'.mts', "rb")
if (file == nil) then
return nil
end
--print('[handle_schematics] Analyzing .mts file '..tostring( path..'.mts' ));
--if( not( string.byte )) then
-- print( '[handle_schematics] Error: string.byte undefined.');
-- return nil;
--end
-- thanks to sfan5 for this advanced code that reads the size from schematic files
local read_s16 = function(fi)
return string.byte(fi:read(1)) * 256 + string.byte(fi:read(1))
@ -72,48 +62,28 @@ handle_schematics.analyze_mts_file = function( path )
-- read the slice probability for each y value that was introduced in version 3
if( size.version >= 3 ) then
-- the probability is not very intresting for buildings so we just skip it
file:read( size.y );
file:read( size.y )
end
-- this list is not yet used for anything
local nodenames = {};
-- this list is needed for calling on_construct after place_schematic
local on_constr = {};
-- nodes that require after_place_node to be called
local after_place_node = {};
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 );
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 );
local name_length = read_s16( file )
-- the text of the next name
local name_text = file:read( name_length );
table.insert( nodenames, name_text );
-- in order to get this information, the node has to be defined and loaded
if( minetest.registered_nodes[ name_text ] and minetest.registered_nodes[ name_text ].on_construct) then
table.insert( on_constr, name_text );
end
-- some nodes need after_place_node to be called for initialization
if( minetest.registered_nodes[ name_text ] and minetest.registered_nodes[ name_text ].after_place_node) then
table.insert( after_place_node, name_text );
end
end
local rotated = 0;
local burried = 0;
local parts = path:split('_');
if( parts and #parts > 2 ) then
if( parts[#parts]=="0" or parts[#parts]=="90" or parts[#parts]=="180" or parts[#parts]=="270" ) then
rotated = tonumber( parts[#parts] );
burried = tonumber( parts[ #parts-1 ] );
if( not( burried ) or burried>20 or burried<0) then
burried = 0;
end
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
@ -121,141 +91,80 @@ handle_schematics.analyze_mts_file = function( path )
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
-- return { size = { x=size.x, y=size.y, z=size.z}, nodenames = nodenames, on_constr = on_constr, after_place_node = after_place_node, rotated=rotated, burried=burried, scm_data_cache = nil };
end
local compressed_data = file:read( "*all" );
local data_string = minetest.decompress(compressed_data, "deflate" );
file.close(file)
local ids = {};
local needs_on_constr = {};
local is_air = 0;
-- translate nodenames to ids
for i,v in ipairs( nodenames ) do
ids[ i ] = minetest.get_content_id( v );
needs_on_constr[ i ] = false;
if( minetest.registered_nodes[ v ] and minetest.registered_nodes[ v ].on_construct ) then
needs_on_constr[ i ] = true;
end
if( v == 'air' ) then
is_air = i;
end
end
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 = 0
local groundnode_count = 0
for z = 1, size.z do
for y = 1, size.y do
for x = 1, size.x do
if( not( scm[y] )) then
scm[y] = {};
end
if( not( scm[y][x] )) then
scm[y][x] = {};
end
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;
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
if( id ~= is_air ) then
scm[y][x][z] = {id, p2};
end
end
end
end
-- 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
return { size = { x=size.x, y=size.y, z=size.z}, nodenames = nodenames, on_constr = on_constr, after_place_node = after_place_node, rotated=rotated, burried=burried, scm_data_cache = scm };
end
handle_schematics.store_mts_file = function( path, data )
data.nodenames[ #data.nodenames+1 ] = 'air';
local file, err = save_restore.file_access(path..'.mts', "wb")
if (file == nil) then
return nil
end
local write_s16 = function( fi, a )
fi:write( string.char( math.floor( a/256) ));
fi:write( string.char( a%256 ));
end
data.size.version = 3; -- we only support version 3 of the .mts file format
file:write( "MTSM" );
write_s16( file, data.size.version );
write_s16( file, data.size.x );
write_s16( file, data.size.y );
write_s16( file, data.size.z );
-- set the slice probability for each y value that was introduced in version 3
if( data.size.version >= 3 ) then
-- the probability is not very intresting for buildings so we just skip it
for i=1,data.size.y do
file:write( string.char(255) );
-- 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
-- set how many diffrent nodenames (node_name_count) are present in the file
write_s16( file, #data.nodenames );
for i = 1, #data.nodenames do
-- the length of the next name
write_s16( file, string.len( data.nodenames[ i ] ));
file:write( data.nodenames[ i ] );
end
-- this string will later be compressed
local node_data = "";
-- actual node data
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 and type( a ) == 'table') then
node_data = node_data..string.char( math.floor( a[1]/256) )..string.char( a[1]%256-1);
else
node_data = node_data..string.char( 0 )..string.char( #data.nodenames-1 );
end
end
end
end
-- probability of occurance
for z = 1, data.size.z do
for y = 1, data.size.y do
for x = 1, data.size.x do
node_data = node_data..string.char( 255 );
end
end
end
-- param2
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 and type( a) == 'table' ) then
node_data = node_data..string.char( a[2] );
else
node_data = node_data..string.char( 0 );
end
end
end
end
local compressed_data = minetest.compress( node_data, "deflate" );
file:write( compressed_data );
file.close(file);
print('SAVING '..path..'.mts (converted from .we).');
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

View File

@ -1,54 +1,73 @@
local handle_schematics = {}
handle_schematics.analyze_we_file = function(scm, we_origin)
local c_ignore = minetest.get_content_id("ignore")
-- receive parameter modpath
local modpath = ...
-- this table will contain the nodes read
local nodes = {}
-- deserialize worldedit savefiles
local worldedit_file = dofile(modpath.."worldedit_file.lua")
-- check if it is a worldedit file
-- (no idea why reading that is done in such a complicated way; a simple deserialize and iteration over all nodes ought to do as well)
local f, err = save_restore.file_access( scm..".we", "r")
if not f then
f, err = save_restore.file_access( scm..".wem", "r")
if not f then
-- error("Could not open schematic '" .. scm .. ".we': " .. err)
return nil;
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 = 0
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
end
local value = f:read("*a")
f:close()
-- 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
local nodes = worldedit_file.load_schematic(value, we_origin)
-- 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
-- create a list of nodenames
local nodenames = {};
local nodenames_id = {};
for i,ent in ipairs( nodes ) do
if( ent and ent.name and not( nodenames_id[ ent.name ])) then
nodenames_id[ ent.name ] = #nodenames + 1;
nodenames[ nodenames_id[ ent.name ] ] = ent.name;
end
end
scm = {}
local maxx, maxy, maxz = -1, -1, -1
local all_meta = {};
for i = 1, #nodes do
local ent = nodes[i]
ent.x = ent.x + 1
ent.y = ent.y + 1
ent.z = ent.z + 1
if ent.x > maxx then
maxx = ent.x
end
if ent.y > maxy then
maxy = ent.y
end
if ent.z > maxz then
maxz = ent.z
end
-- build to scm data tree
if scm[ent.y] == nil then
scm[ent.y] = {}
end
@ -58,47 +77,36 @@ handle_schematics.analyze_we_file = function(scm, we_origin)
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;
local has_meta = false
for _,v in pairs( ent.meta.fields ) do
has_meta = true;
has_meta = true
break
end
for _,v in pairs(ent.meta.inventory) do
has_meta = true;
has_meta = true
break
end
if( has_meta == true ) then
all_meta[ #all_meta+1 ] = {
x=ent.x,
y=ent.y,
z=ent.z,
fields = ent.meta.fields,
inventory = ent.meta.inventory};
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}
scm[ent.y][ent.x][ent.z] = { nodenames_id[ ent.name ], ent.param2 };
nodecount = nodecount + 1
end
for y = 1, maxy do
if scm[y] == nil then
scm[y] = {}
end
for x = 1, maxx do
if scm[y][x] == nil then
scm[y][x] = {}
end
end
end
local size = {};
size.y = math.max(maxy,0);
size.x = math.max(maxx,0);
size.z = math.max(maxz,0);
return { size = { x=size.x, y=size.y, z=size.z}, nodenames = nodenames, on_constr = {}, after_place_node = {}, rotated=0, burried=0, scm_data_cache = scm, metadata = all_meta };
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

View File

@ -3,25 +3,17 @@
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/'
-- deserialize worldedit savefiles
hsl.worldedit_file = dofile(modpath.."worldedit_file.lua")
-- reads and analyzes .mts files (minetest schematics)
hsl.analyze_mts = dofile(modpath.."/analyze_mts_file.lua")
-- reads and analyzes worldedit files
hsl.analyze_we = dofile(modpath.."/analyze_we_file.lua")
-- reads and analyzes Minecraft schematic files
hsl.translate_mc = dofile(modpath.."/translate_nodenames_for_mc_schematic.lua")
hsl.analyze_mc = dofile(modpath.."/analyze_mc_schematic_file.lua")
hsl.analyze_we = assert(loadfile(modpath.."/analyze_we_file.lua"))(modpath)
-- handles rotation and mirroring
hsl.rotate = dofile(modpath.."/rotate.lua")
@ -33,5 +25,4 @@ hsl.misc = dofile(modpath.."/handle_schematics_misc.lua")
hsl.save_restore = dofile(modpath.."/save_restore.lua");
hsl.meta = dofile(modpath.."/handle_schematics_meta.lua");
return hsl

View File

@ -1,405 +0,0 @@
local handle_schematics = {}
-- based on:
-- # Minecraft to Minetest WE schematic MCEdit filter
-- # by sfan5
-- #blockdata -1 means ignore
local P2_IGNORE = -1;
-- #blockdata -2 means copy without change
local P2_COPY = -2;
-- #blockdata -3 means copy and convert the mc facedir value to mt facedir
local P2_CONVERT= -3;
-- #blockdata -4 is for stairs to support upside down ones
local P2_STAIR = -4;
-- #blockdata selects one of the listed subtypes
local P2_SELECT = -5;
-- #Reference MC: http://media-mcw.cursecdn.com/8/8c/DataValuesBeta.png
-- #Reference MT:
-- # https://github.com/minetest/common/blob/master/mods/default/init.lua
-- # https://github.com/minetest/common/blob/master/mods/wool/init.lua
-- # https://github.com/minetest/common/blob/master/mods/stairs/init.lua
local conversionTable = {
-- #blockid blockdata minetest-nodename
-- [0] = {P2_IGNORE, "air"},
[1] = {P2_IGNORE, "default:stone"},
-- 0: stone; 1: granite; 2: polished granite;
-- 3: diorite; 4: polished diorite; 5: andesite;
-- 6: polished andesite
[2] = {P2_IGNORE, "default:dirt_with_grass"},
[3] = {P2_IGNORE, "default:dirt"},
-- 0: dirt; 1: coarse dirt; 2: podzol
[4] = {P2_IGNORE, "default:cobble"},
[5] = {P2_SELECT, { [0]="default:wood",
[1]="moretrees:spruce_planks",
[2]="moretrees:birch_planks",
[3]="default:junglewood",
[4]="moretrees:acacia_planks",
[5]="moretrees:oak_planks"}},
[6] = {P2_SELECT, { [0]="default:wood",
[1]="moretrees:spruce_sapling",
[2]="moretrees:birch_sapling",
[3]="default:junglesapling",
[4]="moretrees:acacia_sapling",
[5]="moretrees:oak_sapling"}},
[7] = {P2_IGNORE, "minecraft:bedrock"}, --# FIXME Bedrock
[8] = {P2_IGNORE, "default:water_flowing"},
[9] = {P2_IGNORE, "default:water_source"},
[10] = {P2_IGNORE, "default:lava_flowing"},
[11] = {P2_IGNORE, "default:lava_source"},
[12] = {P2_SELECT, { [0]="default:sand",
[1]="default:desert_sand"}},
[13] = {P2_IGNORE, "default:gravel"},
[14] = {P2_IGNORE, "default:stone_with_gold"},
[15] = {P2_IGNORE, "default:stone_with_iron"},
[16] = {P2_IGNORE, "default:stone_with_coal"},
-- TODO: the trees have facedir
[17] = {P2_SELECT, { [0]="default:tree",
[1]="moretrees:spruce_trunk",
[2]="moretrees:birch_trunk",
[3]="default:jungletree",
[4]="moretrees:acacia_trunk",
[5]="moretrees:oak_trunk"}},
[18] = {P2_SELECT, { [0]="default:leaves",
[1]="moretrees:spruce_leaves",
[2]="moretrees:birch_leaves",
[3]="default:jungleleaves",
[4]="default:leaves",
[5]="moretrees:spruce_leaves",
[6]="moretrees:birch_leaves",
[7]="default:jungleleaves",
[8]="default:leaves",
[9]="moretrees:spruce_leaves",
[10]="moretrees:birch_leaves",
[11]="default:jungleleaves",
[12]="default:leaves",
[13]="moretrees:spruce_leaves",
[14]="moretrees:birch_leaves",
[15]="default:jungleleaves"}},
[19] = {P2_CONVERT,"minecraft:sponge"},
[20] = {P2_IGNORE, "default:glass"},
[21] = {P2_IGNORE, "default:stone_with_copper"}, -- Lapis Lazuli Ore
[22] = {P2_IGNORE, "default:copperblock"}, -- Lapis Lazuli Block
[23] = {P2_CONVERT,"minecraft:dispenser"},
[24] = {P2_SELECT, { [0]="default:sandstone",
[1]="default:sandstonebrick",
[2]="default:sandstone"}},
[25] = {P2_CONVERT,"minecraft:nodeblock"},
[26] = {P2_CONVERT,"beds:bed"}, -- TODO: might require special handling?
[27] = {P2_CONVERT,"minecraft:golden_rail"},
[28] = {P2_CONVERT,"minecraft:detector_rail"},
-- 29: sticky piston
[30] = {P2_CONVERT,"minecraft:web"},
[31] = {P2_SELECT, { [0]="default:dry_shrub",
[1]="default:grass_4",
[2]="ferns:fern_02"}},
[32] = {P2_IGNORE, "default:dry_shrub"},
-- 34: piston head
[35] = {P2_SELECT, { [0]="wool:white",
[1]="wool:orange",
[2]="wool:magenta",
[3]="wool:light_blue",
[4]="wool:yellow",
[5]="wool:green",
[6]="wool:pink",
[7]="wool:dark_grey",
[8]="wool:grey",
[9]="wool:cyan",
[10]="wool:violet",
[11]="wool:blue",
[12]="wool:brown",
[13]="wool:dark_green",
[14]="wool:red",
[15]="wool:black"}},
-- 36: piston extension
[37] = {P2_IGNORE, "flowers:dandelion_yellow"},
[38] = {P2_SELECT, { [0]="flowers:rose",
[1]="flowers:geranium",
[2]="flowers:viola",
[3]="flowers:dandelion_white",
[4]="tulips:red",
[5]="flowers:tulip",
[6]="tulips:white",
[7]="tulips:pink",
[8]="tulips:black"}},
[41] = {P2_IGNORE, "default:goldblock"},
[42] = {P2_IGNORE, "default:steelblock"},
-- double stone slabs...full blocks?
[43] = {P2_SELECT, { [0]="default:stone",
[1]="default:sandstonebrick",
[2]="default:wood",
[3]="default:cobble",
[4]="default:brick",
[5]="default:stonebrick",
[6]="nether:brick",
[7]="quartz:quartz",
[8]="moreblocks:split_stone_tile",
[9]="default:sandstone"}},
[44] = {P2_SELECT, { [0]="stairs:slab_stone",
[1]="stairs:slab_sandstone",
[2]="stairs:slab_wood",
[3]="stairs:slab_cobble",
[4]="stairs:slab_brick",
[5]="stairs:slab_stonebrick",
[6]="stairs:slab_nether",
[7]="stairs:slab_quartz",
[8]="stairs:slab_stoneupside_down",
[9]="stairs:slab_sandstoneupside_down",
[10]="stairs:slab_woodupside_down",
[11]="stairs:slab_cobbleupside_down",
[12]="stairs:slab_brickupside_down",
[13]="stairs:slab_stonebrickupside_down",
[14]="stairs:slab_netzerupside_down",
[15]="stairs:slab_quartzupside_down"}},
[45] = {P2_IGNORE, "default:brick"},
[46] = {P2_CONVERT,"tnt:tnt"},
[47] = {P2_IGNORE, "default:bookshelf"},
[48] = {P2_IGNORE, "default:mossycobble"},
[49] = {P2_IGNORE, "default:obsidian"},
[50] = {P2_CONVERT,"default:torch"},
[51] = {P2_IGNORE, "fire:basic_flame"},
[52] = {P2_CONVERT,"minecraft:mob_spawner"},
[53] = {P2_STAIR, "stairs:stair_wood"},
[54] = {P2_IGNORE, "default:chest"},
[56] = {P2_IGNORE, "default:stone_with_diamond"},
[57] = {P2_IGNORE, "default:diamondblock"},
[58] = {P2_CONVERT,"minecraft:crafting_table"},
[59] = {P2_IGNORE, "farming:wheat_8"},
[60] = {P2_IGNORE, "farming:soil_wet"},
[61] = {P2_IGNORE, "default:furnace"},
[62] = {P2_IGNORE, "default:furnace_active"},
[63] = {P2_IGNORE, "default:sign_wall"},
[64] = {P2_IGNORE, "doors:door_wood_t_1"},
[65] = {P2_IGNORE, "default:ladder"},
[66] = {P2_IGNORE, "default:rail"},
[67] = {P2_STAIR, "stairs:stair_cobble"},
[68] = {P2_CONVERT,"default:sign_wall"},
[71] = {P2_IGNORE, "doors:door_steel_t_1"},
[78] = {P2_IGNORE, "default:snow"},
[79] = {P2_IGNORE, "default:ice"},
[80] = {P2_IGNORE, "default:snowblock"},
[81] = {P2_IGNORE, "default:cactus"},
[82] = {P2_IGNORE, "default:clay"},
[83] = {P2_IGNORE, "default:papyrus"},
[84] = {P2_CONVERT,"minecraft:jukebox"},
[85] = {P2_IGNORE, "default:fence_wood"},
[86] = {P2_CONVERT,"farming:pumpkin"},
[91] = {P2_CONVERT,"farming:pumpkin_face_light"},
[92] = {P2_CONVERT,"minecraft:cake"},
[95] = {P2_IGNORE, "minecraft:stained_glass"}, -- TODO
[96] = {P2_CONVERT,"doors:trapdoor"},
[97] = {P2_IGNORE, "minecraft:monster_egg"},
[98] = {P2_IGNORE, "default:stonebrick"},
[108]= {P2_STAIR, "stairs:stair_brick"},
[109]= {P2_CONVERT,"stairs:stair_stonebrick"},
-- TODO: double ... wood slab...
[125]= {P2_SELECT, { [0]="default:wood",
[1]="moretrees:spruce_planks",
[2]="moretrees:birch_planks",
[3]="default:junglewood",
[4]="moretrees:acacia_planks",
[5]="moretrees:oak_planks"}},
[125]= {P2_IGNORE, "default:wood"},
[126]= {P2_SELECT, { [0]="stairs:slab_wood",
[1]="stairs:slab_spruce_planks",
[2]="stairs:slab_birch_planks",
[3]="stairs:slab_junglewood",
[4]="stairs:slab_acacia_planks",
[5]="stairs:slab_oak_planks",
[8]="stairs:slab_woodupside_down",
[9]="stairs:slab_spruce_planksupside_down",
[10]="stairs:slab_birch_planksupside_down",
[11]="stairs:slab_junglewoodupside_down",
[12]="stairs:slab_acacia_planksupside_down",
[13]="stairs:slab_oak_planksupside_down"}},
[126]= {P2_IGNORE, "stairs:slab_wood"},
[128]= {P2_STAIR, "stairs:stair_sandstone"},
[129]= {P2_IGNORE, "default:stone_with_mese"},
[133]= {P2_IGNORE, "default:mese"},
[134]= {P2_STAIR, "stairs:stair_wood"},
[135]= {P2_STAIR, "stairs:stair_wood"},
[136]= {P2_STAIR, "stairs:stair_junglewood"},
-- #Mesecons section
-- # Reference: https://github.com/Jeija/minetest-mod-mesecons/blob/master/mesecons_alias/init.lua
[25] = {P2_IGNORE, "mesecons_noteblock:noteblock"},
[29] = {P2_CONVERT,"mesecons_pistons:piston_sticky_off"},
[33] = {P2_CONVERT,"mesecons_pistons:piston_normal_off"},
[55] = {P2_IGNORE, "mesecons:wire_00000000_off"},
[69] = {P2_CONVERT,"mesecons_walllever:wall_lever_off"},
[70] = {P2_IGNORE, "mesecons_pressureplates:pressure_plate_stone_off"},
[72] = {P2_IGNORE, "mesecons_pressureplates:pressure_plate_wood_off"},
[73] = {P2_IGNORE, "default:stone_with_mese"},
[74] = {P2_IGNORE, "default:stone_with_mese"},
[75] = {P2_CONVERT,"mesecons_torch:torch_off"},
[76] = {P2_CONVERT,"mesecons_torch:torch_on"},
[77] = {P2_CONVERT,"mesecons_button:button_off"},
[93] = {P2_CONVERT,"mesecons_delayer:delayer_off_1"},
[94] = {P2_CONVERT,"mesecons_delayer:delayer_on_1"},
-- see mod https://github.com/doyousketch2/stained_glass
[95] = {P2_SELECT, { [0]="default:glass", -- TODO
[1]="stained_glass:orange__",
[2]="stained_glass:magenta__",
[3]="stained_glass:skyblue__",
[4]="stained_glass:yellow__",
[5]="stained_glass:lime__",
[6]="stained_glass:redviolet__",
[7]="stained_glass:dark_grey__", -- TODO
[8]="stained_glass:grey__", -- TODO
[9]="stained_glass:cyan__",
[10]="stained_glass:violet__",
[11]="stained_glass:blue__",
[12]="stained_glass:orange_dark_",
[13]="stained_glass:green__",
[14]="stained_glass:red__",
[15]="stained_glass:black__"}}, -- TODO
[101]= {P2_CONVERT,"xpanes:bar"},
[102]= {P2_CONVERT,"xpanes:pane"},
[103]= {P2_IGNORE, "farming:melon"},
[104]= {P2_IGNORE, "minecraft:pumpkin_stem"},
[105]= {P2_IGNORE, "minecraft:melon_stem"},
[106]= {P2_CONVERT,"vines:vine"},
[107]= {P2_CONVERT,"minecraft:fence_gate"},
[108]= {P2_STAIR, "stairs:stair_brick"},
[109]= {P2_STAIR, "stairs:stair_stonebrick"},
[110]= {P2_CONVERT,"minecraft:mycelium"},
[111]= {P2_CONVERT,"flowers:waterlily"},
[112]= {P2_CONVERT,"minecraft:nether_brick"},
[113]= {P2_CONVERT,"minecraft:nether_brick_fence"},
[114]= {P2_CONVERT,"minecraft:nether_brick_stairs"},
[115]= {P2_CONVERT,"minecraft:nether_wart"},
[116]= {P2_CONVERT,"minecraft:enchanting_table"},
[117]= {P2_CONVERT,"minecraft:brewing_stand"},
[118]= {P2_CONVERT,"minecraft:cauldron"},
[119]= {P2_CONVERT,"minecraft:end_portal"},
[120]= {P2_CONVERT,"minecraft:end_portal_frame"},
[121]= {P2_CONVERT,"minecraft:end_stone"},
[122]= {P2_CONVERT,"minecraft:dragon_egg"},
[123]= {P2_IGNORE, "mesecons_lightstone_red_off"},
[124]= {P2_IGNORE, "mesecons_lightstone_red_on"},
[125]= {P2_CONVERT,"minecraft:double_wooden_slab"},
[126]= {P2_CONVERT,"stairs:slab_wood"},
[127]= {P2_CONVERT,"farming_plus:cocoa"},
[137]= {P2_IGNORE, "mesecons_commandblock:commandblock_off"},
[151]= {P2_IGNORE, "mesecons_solarpanel:solar_panel_off"},
[152]= {P2_IGNORE, "default:mese"},
-- see mod https://github.com/tenplus1/bakedclay
[159] = {P2_SELECT, { [0]="bakedclay:white",
[1]="bakedclay:orange",
[2]="bakedclay:magenta",
[3]="bakedclay:light_blue", -- TODO
[4]="bakedclay:yellow",
[5]="bakedclay:green",
[6]="bakedclay:pink",
[7]="bakedclay:dark_grey",
[8]="bakedclay:grey",
[9]="bakedclay:cyan",
[10]="bakedclay:violet",
[11]="bakedclay:blue",
[12]="bakedclay:brown",
[13]="bakedclay:dark_green",
[14]="bakedclay:red",
[15]="bakedclay:black"}},
-- see mod mccarpet https://forum.minetest.net/viewtopic.php?t=7419
[171] = {P2_SELECT, { [0]="mccarpet:white",
[1]="mccarpet:orange",
[2]="mccarpet:magenta",
[3]="mccarpet:light_blue", -- TODO
[4]="mccarpet:yellow",
[5]="mccarpet:green",
[6]="mccarpet:pink",
[7]="mccarpet:dark_grey",
[8]="mccarpet:grey",
[9]="mccarpet:cyan",
[10]="mccarpet:violet",
[11]="mccarpet:blue",
[12]="mccarpet:brown",
[13]="mccarpet:dark_green",
[14]="mccarpet:red",
[15]="mccarpet:black"}},
[181] = {P2_SELECT, { [0]="default:desert_stonebrick",
[1]="default:desertstone"}},
-- #Nether section
-- # Reference: https://github.com/PilzAdam/nether/blob/master/init.lua
[43] = {P2_IGNORE, "nether:brick"},
[87] = {P2_IGNORE, "nether:rack"},
[88] = {P2_IGNORE, "nether:sand"},
[89] = {P2_IGNORE, "nether:glowstone"},
[90] = {P2_CONVERT,"nether:portal"},
-- #Riesenpilz Section
-- # Reference: https://github.com/HybridDog/riesenpilz/blob/master/init.lua
[39] = {P2_IGNORE, "riesenpilz:brown"},
[40] = {P2_IGNORE, "riesenpilz:red"},
[99] = {P2_CONVERT,"riesenpilz:head_brown"},
[100]= {P2_CONVERT,"riesenpilz:head_brown"},
}
local mc2mtFacedir = function(blockdata)
-- #Minetest
-- # x+ = 2
-- # x- = 3
-- # z+ = 1
-- # z- = 0
-- #Minecraft
-- # x+ = 3
-- # x- = 1
-- # z+ = 0
-- # z- = 2
local tbl = {
[3]= 2,
[1]= 1,
[0]= 3,
[2]= 0,
}
if( tbl[ blockdata ] ) then
return tbl[ blockdata ];
-- this happens with i.e. wallmounted torches...
else
return blockdata;
end
end
local mc2mtstairs = function( name, blockdata)
if blockdata >= 4 then
return {name.. "upside_down", mc2mtFacedir(blockdata - 4)}
else
return {name, mc2mtFacedir(blockdata)}
end
end
-- returns {translated_node_name, translated_param2}
handle_schematics.findMC2MTConversion = function(blockid, blockdata)
if (blockid == 0 ) then
return {"air",0};
-- fallback
elseif( not( conversionTable[ blockid ])) then
return { "minecraft:"..tostring( blockid )..'_'..tostring( blockdata ), 0};
end
local conv = conversionTable[ blockid ];
if( conv[1] == P2_IGNORE ) then
return { conv[2], 0};
elseif( conv[1] == P2_COPY ) then
return { conv[2], blockdata};
elseif( conv[1] == P2_CONVERT) then
return { conv[2], mc2mtFacedir(blockdata)};
elseif( conv[1] == P2_STAIR ) then
return mc2mtstairs(conv[2], blockdata);
elseif( conv[1] == P2_SELECT
and conv[2][ blockdata ] ) then
return { conv[2][ blockdata ], 0};
elseif( conv[1] == P2_SELECT
and not(conv[2][ blockdata ] )) then
return { conv[2][0], 0};
else
return { conv[2], 0 };
end
return {air, 0};
end
return handle_schematics

View File

@ -8,7 +8,8 @@
-- * 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 = {}
local worldedit_file = {} -- add the namespace
--- Schematic serialization and deserialiation.
-- @module worldedit.serialization
@ -90,18 +91,15 @@ function worldedit_file.load_schematic(value, we_origin)
entry[2] = nil
end
end
elseif version == 3 or version=="3" then -- List format
if( not( we_origin ) or #we_origin <3) then
we_origin = { 0, 0, 0 };
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 = we_origin[1] + tonumber(x),
y = we_origin[2] + tonumber(y),
z = we_origin[3] + tonumber(z),
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,

248
mapping.lua Normal file
View File

@ -0,0 +1,248 @@
--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" }
-----------------------------------------------
-- copy table of mapping entry
-----------------------------------------------
function mapping.merge_map_entry(entry1, entry2)
if entry2 then
return {name = entry1.name or entry2.name, --not a typo: used to merge fallback to mapped data. The mapped data is preferred
node_def = entry1.node_def or entry2.node_def,
content_id = entry1.content_id or entry2.content_id,
param2 = entry2.param2 or entry1.param2,
meta = entry2.meta or entry1.meta,
custom_function = entry2.custom_function or entry1.custom_function,
cost_item = entry2.cost_item or entry1.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
dprint("map", name, "to", mr.name, mr.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)
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

271
nodes.lua
View File

@ -1,271 +0,0 @@
local dprint = townchest.dprint_off --debug
local _c_free_item = "default:cloud"
-- Fallback nodes replacement of unknown nodes
-- Maybe it is beter to use aliases for unknown notes. But anyway
-- TODO: should be editable in game trough a nice gui, to customize the building before build
local __map_unknown = function(self)
local map = townchest.nodes.unknown_nodes_data[self.name]
if not map or map.name == self.name then -- no fallback mapping. don't use the node
dprint("mapping failed:", self.name, dump(map))
print("unknown node in building", self.name)
return nil
end
dprint("mapped", self.name, "to", self.name)
local mappednode = townchest.nodes.new(self)
mappednode.name = map.name -- must be there!
if map.meta then
if not mappednode.meta then
mappednode.meta = {}
end
for k, v in pairs(map.meta) do
mappednode.meta[k] = v
end
end
if map.param1 then
if type(map.param1) == "function" then
dprint("map param1 by function")
mappednode.param1 = map.param1(node)
else
mappednode.param1 = map.param1
dprint("map param1 by value")
end
end
if map.param2 then
if type(map.param2) == "function" then
dprint("map param2 by function")
mappednode.param2 = map.param2(map)
else
dprint("map param2 by value")
mappednode.param2 = map.param2
end
end
return mappednode
end
-- Nodes replacement to customizie buildings
-- TODO: should be editable in game trough a nice gui, to customize the building before build
local __customize = function(self)
local map = townchest.nodes.customize_data[self.name]
if not map then -- no mapping. return unchanged
return self
end
-- dprint("map", self.name, "to", map.name, map.matname)
local mappednode = townchest.nodes.new(self)
if map.name then
mappednode.name = map.name
end
if map.matname then
mappednode.matname = map.matname
end
if map.meta then
if not mappednode.meta then
mappednode.meta = {}
end
for k, v in pairs(map.meta) do
mappednode.meta[k] = v
end
end
return mappednode
end
-----------------------------------------------
-- towntest_chest.mapping.mapnode Take filters and actions on nodes before building. Currently the payment item determination and check for registred node only
-- node - Node (from file) to check if buildable and payable
-- return - node with enhanced informations
-----------------------------------------------
local __map = function(self)
local node_chk = minetest.registered_nodes[self.name]
if not node_chk then
local fallbacknode = self:map_unknown()
if fallbacknode then
return fallbacknode:map()
end
else
-- known node Map them?
local customizednode = self:customize()
if customizednode.name == "" then --disabled by mapping
return nil
end
if not customizednode.matname then --no matname override customizied.
--Check for price or if it is free
local recipe = minetest.get_craft_recipe(node_chk.name)
if (node_chk.groups.not_in_creative_inventory and --not in creative
not (node_chk.groups.not_in_creative_inventory == 0) and
(not recipe or not recipe.items)) --and not craftable
or
(not node_chk.description or node_chk.description == "") then -- no description
if node_chk.drop and node_chk.drop ~= "" then
-- use possible drop as payment
if type(node_chk.drop) == "table" then -- drop table
customizednode.matname = node_chk.drop[1] -- use the first one
else
customizednode.matname = node_chk.drop
end
else --something not supported, but known
customizednode.matname = _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
customizednode.matname = customizednode.name
end
end
return customizednode
end
end
-----------------------------------------------
-- is_equal_meta - compare meta information of 2 nodes
-- name - Node name to check and map
-- return - item name used as payment
-----------------------------------------------
local __is_equal_meta = function(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 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
-- door compatibility. Seems the old doors was facedir and now the wallmounted values should be used
local __param2_wallmounted_to_facedir = function(self)
if self.param2 == 0 then -- +y?
return 0
elseif self.param2 == 1 then -- -y?
return 1
elseif self.param2 == 2 then --unsure
return 3
elseif self.param2 == 3 then --unsure
return 1
elseif self.param2 == 4 then --unsure
return 2
elseif self.param2 == 5 then --unsure
return 0
end
end
local __id = function(this)
return minetest.pos_to_string(this)
end
local 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"}}},param2 = __param2_wallmounted_to_facedir} --closed
u["doors:door_wood_b_o"] = {name = "doors:door_wood_b", {["meta"] = {["fields"] = {["state"] = "1"}}},param2 = __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"}}},param2 = __param2_wallmounted_to_facedir} --closed
u["doors:door_glass_b_o"] = {name = "doors:door_glass_b", {["meta"] = {["fields"] = {["state"] = "1"}}},param2 = __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"}}},param2 = __param2_wallmounted_to_facedir} --closed
u["doors:door_steel_b_o"] = {name = "doors:door_steel_b", {["meta"] = {["fields"] = {["state"] = "1"}}},param2 = __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 = {}
-- "name" and "matname" are optional.
-- if name is missed it will not be changed
-- if matname 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", matname = "default:wood" }
c["beds:bed_top"] = { matname = _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"] = { matname = "bucket:bucket_lava" }
c["default:river_water_source"] = { matname = "bucket:bucket_river_water" }
c["default:water_source"] = { matname = "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"] = { matname = "default:dirt" }
c["default:dirt_with_grass"] = { matname = "default:dirt" }
c["default:dirt_with_snow"] = { matname = "default:dirt" }
townchest.nodes = {
-- We need a free item that always available to get visible working on them
c_free_item = _c_free_item,
unknown_nodes_data = u,
customize_data = c,
is_equal_meta = __is_equal_meta
}
townchest.nodes.new = function(nodelike)
local this = {}
if nodelike then
this = nodelike --!by reference It will remain the same node, but just
end
this.id = __id
this.map_unknown = __map_unknown
this.customize = __customize
this.map = __map
this.param2_wallmounted_to_facedir = __param2_wallmounted_to_facedir
return this
end

View File

@ -1,4 +1,5 @@
local dprint = townchest.dprint --debug
local dprint = townchest.dprint_off --debug
local dprint = townchest.dprint
local MAX_SPEED = 5
local BUILD_DISTANCE = 3
@ -59,50 +60,55 @@ local select_chest = function(self)
end
local get_if_buildable = function(self, realpos)
local get_if_buildable = function(self, realpos, node_prep)
local pos = self.chest.plan:get_plan_pos(realpos)
-- dprint("in plan", minetest.pos_to_string(pos))
local node = self.chest.plan.building_full[minetest.pos_to_string(pos)]
if not node then
return nil
local node
if node_prep then
node = node_prep
else
node = self.chest.plan:prepare_node_for_build(pos, realpos)
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:set_node_processed(node)
if not node then
-- remove something crufty
self.chest.plan:remove_node(pos)
return nil
end
-- check if already build (skip the most air)
local success = minetest.forceload_block(realpos) --keep the target node loaded
if not success then
dprint("error forceloading:", minetest.pos_to_string(realpos))
-- 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)
minetest.forceload_free_block(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("check ignored")
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.nodes.c_free_item -- adjust params for free
-- 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:set_node_processed(node)
self.chest.plan:remove_node(pos)
return nil
elseif townchest.nodes.is_equal_meta(minetest.get_meta(realpos):to_table(), node.meta) then
elseif townchest.mapping.is_equal_meta(minetest.get_meta(realpos):to_table(), node.meta) then
--metadata adjustment
self.chest.plan:set_node_processed(node)
self.chest.plan:remove_node(pos)
return nil
elseif node.matname == townchest.nodes.c_free_item then
elseif node.matname == townchest.mapping.c_free_item then
-- TODO: check if nearly nodes are already built
return node
else
@ -141,14 +147,14 @@ local function prefer_target(npc, t1, t2)
-- prefer the last target node
if npc.targetnode then
if t1.x == npc.targetnode.pos.x and
t1.y == npc.targetnode.pos.y and
t1.z == npc.targetnode.pos.z 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.x == npc.targetnode.pos.x and
t2.y == npc.targetnode.pos.y and
t2.z == npc.targetnode.pos.z then
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
@ -161,9 +167,17 @@ local function prefer_target(npc, t1, t2)
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 + 1
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
@ -171,7 +185,7 @@ local function prefer_target(npc, t1, t2)
-- prefer lower node if not air
if t2.name ~= "air" then
t2_c.y = t2_c.y + 1
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
@ -179,12 +193,12 @@ local function prefer_target(npc, t1, t2)
-- 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) < 2 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) < 2 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
@ -203,74 +217,97 @@ 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=math.floor(npcpos.x)-5, math.floor(npcpos.x)+5 do
for y=math.floor(npcpos.y)-5, math.floor(npcpos.y)+5 do
for z=math.floor(npcpos.z)-5, math.floor(npcpos.z)+5 do
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 = plan:get_world_pos(node)
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
-- get the old target to compare
if self.targetnode and self.targetnode.pos then
selectednode = get_if_buildable(self, self.targetnode.pos)
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")
for idx, nodeplan in ipairs(plan:get_nodes_from_chunk(plan:get_plan_pos(npcpos))) do
local node = get_if_buildable(self, plan:get_world_pos(nodeplan))
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 wpos = plan:get_world_pos(nodeplan)
local node = get_if_buildable(self, wpos, nodeplan.node)
if node then
node.pos = plan:get_world_pos(node)
node.pos = 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
--get anything - with forceloading, so the NPC can go away
dprint("get node with random jump")
local jump = plan.building_size
if jump > 1000 then
jump = 1000
end
if jump > 1 then
jump = math.floor(math.random(jump))
else
jump = 0
end
local startingnode = plan:get_nodes(1,jump)
if startingnode[1] then -- the one node given
dprint("---check chunk", minetest.pos_to_string(startingnode[1]))
for idx, nodeplan in ipairs(plan:get_nodes_from_chunk(startingnode[1])) do
local node_wp = plan:get_world_pos(nodeplan)
local node = get_if_buildable(self, node_wp)
if node then
node.pos = node_wp
selectednode = prefer_target(self, selectednode, node)
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(plan:get_plan_pos(random_pos))
dprint("Chunk loaeded: nodes:", #chunk_nodes)
for idx, nodeplan in ipairs(chunk_nodes) do
local node = get_if_buildable(self, plan:get_world_pos(nodeplan), nodeplan.node)
if node then
node.pos = plan:get_world_pos(node)
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 startningnode")
dprint("something wrong with random_pos")
end
end
if selectednode then
selectednode.pos = plan:get_world_pos(selectednode)
assert(selectednode.pos, "BUG: a position should exists")
return selectednode
else
dprint("no next node found", plan.building_size)
if plan.building_size == 0 then
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()
@ -287,7 +324,7 @@ npcf:register_npc("townchest:npcf_builder" ,{
self.timer = 0
select_chest(self)
self.target_prev = self.targetnode
if self.chest and self.chest.plan and self.chest.plan.building_size > 0 then
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
@ -328,7 +365,8 @@ npcf:register_npc("townchest:npcf_builder" ,{
yaw = npcf:get_face_direction(pos, self.targetnode.pos)
-- target reached build
if target_distance <= BUILD_DISTANCE and self.dest_type == "build" then
-- do the build
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
@ -343,7 +381,7 @@ npcf:register_npc("townchest:npcf_builder" ,{
if self.targetnode.meta then
minetest.env:get_meta(self.targetnode.pos):from_table(self.targetnode.meta)
end
self.chest.plan:set_node_processed(self.targetnode)
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}
@ -373,8 +411,10 @@ npcf:register_npc("townchest:npcf_builder" ,{
self.dest_type = "home_reached"
self.targetnode = nil
else
--target not reached
--target not reached -- route
state = NPCF_ANIM_WALK
self.path = minetest.find_path(pos, self.targetnode.pos, 10, 1, 5, "A*")
-- 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
@ -388,14 +428,19 @@ npcf:register_npc("townchest:npcf_builder" ,{
end
-- teleport in direction in case of stucking
if (last_distance - 0.01) <= target_distance and self.laststep == "walk" and
(self.target_prev == self.targetnode) then
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
@ -408,6 +453,10 @@ npcf:register_npc("townchest:npcf_builder" ,{
else
dprint("no target")
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)

396
plan.lua
View File

@ -1,176 +1,238 @@
local dprint = townchest.dprint_off --debug
local __add_node = function(this,node)
-- add to the full list
this.building_full[node:id()] = node --collect references for direct access
this.building_size = this.building_size + 1
end
local __adjust_flatting_requrement = function(this,node)
-- create relative sizing information
if not this.relative.min_x or this.relative.min_x > node.x then
this.relative.min_x = node.x
end
if not this.relative.max_x or this.relative.max_x < node.x then
this.relative.max_x = node.x
end
if not this.relative.min_y or this.relative.min_y > node.y then
this.relative.min_y = node.y
end
if not this.relative.max_y or this.relative.max_y < node.y then
this.relative.max_y = node.y
end
if not this.relative.min_z or this.relative.min_z > node.z then
this.relative.min_z = node.z
end
if not this.relative.max_z or this.relative.max_z < node.z then
this.relative.max_z = node.z
end
-- create ground level information
if string.sub(node.name, 1, 18) == "default:dirt_with_" or
node.name == "farming:soil_wet" then
if not this.relative.groundnode_count then
this.relative.groundnode_count = 1
this.relative.ground_y = node.y + 1
else
this.relative.groundnode_count = this.relative.groundnode_count + 1
this.relative.ground_y = this.relative.ground_y + (node.y + 1 - this.relative.ground_y) / this.relative.groundnode_count
dprint("ground calc:", node.name, this.relative.groundnode_count, node.y, this.relative.ground_y)
end
end
end
local __prepare = function(this)
-- round ground level to full block
this.relative.ground_y = math.floor(this.relative.ground_y)
local additional = 0
local add_max = 5
dprint("create flatting plan")
for y = this.relative.min_y, this.relative.max_y + 5 do -- with additional 5 on top
--calculate additional grounding
if y >= this.relative.ground_y then --only over ground
local high = y-this.relative.ground_y
additional = high+1
if additional > add_max then --set to max
additional = add_max
end
dprint("additional flat", y, additional)
end
for x = this.relative.min_x - additional, this.relative.max_x + additional do
for z = this.relative.min_z - additional, this.relative.max_z + additional do
if not this.building_full[minetest.pos_to_string({x=x, y=y, z=z})] then -- not in plan - flat them
local node = townchest.nodes.new({ x=x, y=y, z=z, name="air", matname = townchest.nodes.c_free_item })
this:add_node(node)
end
end
end
end
dprint("flatting plan done")
end
local __get_world_pos = function(this,pos)
local fpos = { x=pos.x+this.chest.pos.x,
y=pos.y+this.chest.pos.y - this.relative.ground_y,
z=pos.z+this.chest.pos.z
}
-- dprint("world_pos y:"..pos.y.."+"..this.chest.pos.y.."-"..this.relative.ground_y)
return fpos
end
-- revert get_world_pos
local __get_plan_pos = function(this,pos)
local fpos = { x=pos.x-this.chest.pos.x,
y=pos.y-this.chest.pos.y + this.relative.ground_y,
z=pos.z-this.chest.pos.z
}
-- dprint("plan_pos y:"..pos.y.."-"..this.chest.pos.y.."+"..this.relative.ground_y)
return fpos
end
local __get_nodes = function(this, count, skip)
local ret = {}
local counter = 0
if not skip then
skip = 0
end
for key, node in pairs(this.building_full) do
counter = counter + 1
if counter > skip then
table.insert(ret, node)
end
if counter >= count + skip then
break
end
end
return ret
end
-- to be able working with forceload chunks
local __get_nodes_from_chunk = function(this, node)
-- calculate the begin of the chunk
--local BLOCKSIZE = core.MAP_BLOCKSIZE
local BLOCKSIZE = 16
local wpos = this:get_world_pos(node)
wpos.x = (math.floor(wpos.x/BLOCKSIZE))*BLOCKSIZE
wpos.y = (math.floor(wpos.y/BLOCKSIZE))*BLOCKSIZE
wpos.z = (math.floor(wpos.z/BLOCKSIZE))*BLOCKSIZE
dprint("nodes for chunk (wpos)", wpos.x, wpos.y, wpos.z)
local vpos = this:get_plan_pos(wpos)
dprint("nodes for chunk (vpos)", vpos.x, vpos.y, vpos.z)
local ret = {}
for x = vpos.x, vpos.x + BLOCKSIZE do
for y = vpos.y, vpos.y + BLOCKSIZE do
for z = vpos.z, vpos.z + BLOCKSIZE do
--local node = this.building_full[node:id()]
local node = this.building_full[minetest.pos_to_string({x=x,y=y,z=z})]
if node then
table.insert(ret, node)
end
end
end
end
return ret
end
local __set_node_processed = function(this, node)
this.building_full[node:id()] = nil
this.building_size = this.building_size - 1
end
--local dprint = townchest.dprint--debug
townchest.plan = {}
townchest.plan.new = function( chest )
local this = {}
this.relative = {} --relative infos
this.relative.ground_y = 0
local self = {}
self.chest = chest
this.chest = chest
-- helper: get scm entry for position
function self.get_scm_node(self, pos)
assert(pos.x, "pos without xyz")
if not self.data.scm_data_cache[pos.y] then
return nil
end
if not self.data.scm_data_cache[pos.y][pos.x] then
return nil
end
if not self.data.scm_data_cache[pos.y][pos.x][pos.z] then
return nil
end
return self.data.scm_data_cache[pos.y][pos.x][pos.z]
end
-- full plan - key-indexed - equvalent to the we-file
this.building_full = {}
this.building_size = 0
-- "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
this.add_node = __add_node
this.adjust_flatting_requrement = __adjust_flatting_requrement
this.prepare = __prepare
this.get_world_pos = __get_world_pos
this.get_plan_pos = __get_plan_pos
this.get_nodes = __get_nodes
this.set_node_processed = __set_node_processed
this.get_nodes_from_chunk = __get_nodes_from_chunk
return this
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 not self:get_scm_node(airnode) 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 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)
wpos.x = (math.floor(wpos.x/BLOCKSIZE))*BLOCKSIZE
wpos.y = (math.floor(wpos.y/BLOCKSIZE))*BLOCKSIZE
wpos.z = (math.floor(wpos.z/BLOCKSIZE))*BLOCKSIZE
dprint("nodes for chunk (wpos)", wpos.x, wpos.y, wpos.z)
local vpos = self:get_plan_pos(wpos)
dprint("nodes for chunk (vpos)", vpos.x, vpos.y, vpos.z)
local ret = {}
for y = vpos.y, vpos.y + BLOCKSIZE do
if self.data.scm_data_cache[y] then
for x = vpos.x, vpos.x + BLOCKSIZE do
if self.data.scm_data_cache[y][x] then
for z = vpos.z, vpos.z + BLOCKSIZE do
if self.data.scm_data_cache[y][x][z] then
table.insert(ret, {x=x,y=y,z=z, node=self:prepare_node_for_build({x=x,y=y,z=z}, wpos)})
end
end
end
end
end
end
return ret
end
function self.remove_node(self, pos)
-- cleanup raw data
if self.data.scm_data_cache[pos.y] then
if self.data.scm_data_cache[pos.y][pos.x]then
if self.data.scm_data_cache[pos.y][pos.x][pos.z] then
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] then
if self.data.prepared_cache[pos.y][pos.x]then
if self.data.prepared_cache[pos.y][pos.x][pos.z] 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
-- prepare node for build
function self.prepare_node_for_build(self, pos, wpos)
-- first run, generate mapping data
if not self.data.mappednodes then
townchest.mapping.do_mapping(self.data)
end
-- get from cache
if self.data.prepared_cache and
self.data.prepared_cache[pos.y] and
self.data.prepared_cache[pos.y][pos.x] and
self.data.prepared_cache[pos.y][pos.x][pos.z] 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 not scm_node then
return nil
end
--get mapping data
local map = self.data.mappednodes[scm_node.name_id]
if not map then
return nil
end
local node = townchest.mapping.merge_map_entry(map, scm_node)
if node.custom_function 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

View File

@ -155,7 +155,6 @@ local _build_status = function(state)
print("BUG: no plan in build_status dialog!")
return false -- no update
end
local relative = chest.plan.relative
-- create screen
state:size(10,5)
@ -180,7 +179,7 @@ local _build_status = function(state)
end
set_dynamic_values()
chest:persist_info()
chest:run_async(chest.instant_build_chain)
chest:run_async(chest.instant_build_chunk)
end)
-- NPC build button
@ -212,12 +211,14 @@ local _build_status = function(state)
elseif chest.info.taskname == "generate" then
l1:setText("Simple task: "..chest.info.genblock.variant_name)
end
l2:setText("Size: "..(relative.max_x-relative.min_x).." x "..(relative.max_z-relative.min_z))
l3:setText("Building high: "..(relative.max_y-relative.min_y).." Ground high: "..(relative.ground_y-relative.min_y))
local size = vector.add(vector.subtract(chest.plan.data.max_pos, chest.plan.data.min_pos),1)
l2:setText("Size: "..size.x.." x "..size.z)
l3:setText("Building high: "..size.y.." Ground high: "..(chest.plan.data.ground_y-chest.plan.data.min_pos.y))
--update data on demand without rebuild the state
set_dynamic_values = function()
l4:setText("Nodes to do: "..chest.plan.building_size)
l4:setText("Nodes to do: "..chest.plan.data.nodecount)
if chest.info.npc_build == true then
npc_tg:setId(2)

View File

@ -1,122 +0,0 @@
--- Schematic serialization and deserialiation.
-- @module worldedit.serialization
--worldedit.LATEST_SERIALIZATION_VERSION = 5
--local LATEST_SERIALIZATION_HEADER = worldedit.LATEST_SERIALIZATION_VERSION .. ":"
--[[
Serialization version history:
1: Original format. Serialized Lua table with a weird linked format...
2: Position and node seperated into sub-tables in fields `1` and `2`.
3: List of nodes, one per line, with fields seperated by spaces.
Format: <X> <Y> <Z> <Name> <Param1> <Param2>
4: Serialized Lua table containing a list of nodes with `x`, `y`, `z`,
`name`, `param1`, `param2`, and `meta` fields.
5: Added header and made `param1`, `param2`, and `meta` fields optional.
Header format: <Version>,<ExtraHeaderField1>,...:<Content>
--]]
--- Reads the header of serialized data.
-- @param value Serialized WorldEdit data.
-- @return The version as a positive natural number, or 0 for unknown versions.
-- @return Extra header fields as a list of strings, or nil if not supported.
-- @return Content (data after header).
function townchest.we_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 townchest.we_load_schematic(value)
local version, header, content = townchest.we_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