From aedba5310089d26097bee827b3f093cc584f76a3 Mon Sep 17 00:00:00 2001 From: Sokomine Date: Fri, 1 May 2015 18:04:03 +0200 Subject: [PATCH] extracted the handling of schematics from mg_villages and created an extra mod for it --- README.md | 4 + analyze_mts_file.lua | 273 +++++++++ analyze_we_file.lua | 83 +++ build_chest.lua | 824 +++++++++++++++++++++++++++ build_chest_add_schems_from_file.lua | 55 ++ build_chest_handle_replacements.lua | 219 +++++++ build_chest_preview_image.lua | 245 ++++++++ depends.txt | 21 + handle_schematics_misc.lua | 107 ++++ init.lua | 51 ++ place_buildings.lua | 810 ++++++++++++++++++++++++++ replacements_farming.lua | 167 ++++++ replacements_get_table.lua | 21 + replacements_realtest.lua | 82 +++ replacements_roof.lua | 110 ++++ replacements_wood.lua | 233 ++++++++ rotate.lua | 114 ++++ village_traders.lua | 146 +++++ worldedit_file.lua | 138 +++++ 19 files changed, 3703 insertions(+) create mode 100644 README.md create mode 100644 analyze_mts_file.lua create mode 100644 analyze_we_file.lua create mode 100644 build_chest.lua create mode 100644 build_chest_add_schems_from_file.lua create mode 100644 build_chest_handle_replacements.lua create mode 100644 build_chest_preview_image.lua create mode 100644 depends.txt create mode 100644 handle_schematics_misc.lua create mode 100644 init.lua create mode 100644 place_buildings.lua create mode 100644 replacements_farming.lua create mode 100644 replacements_get_table.lua create mode 100644 replacements_realtest.lua create mode 100644 replacements_roof.lua create mode 100644 replacements_wood.lua create mode 100644 rotate.lua create mode 100644 village_traders.lua create mode 100644 worldedit_file.lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..481b3e6 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ + +This mod is not finished yet. +Type "/giveme handle_schematics:build" to get a build chest. + diff --git a/analyze_mts_file.lua b/analyze_mts_file.lua new file mode 100644 index 0000000..45dc756 --- /dev/null +++ b/analyze_mts_file.lua @@ -0,0 +1,273 @@ + +--[[ taken from src/mg_schematic.cpp: + Minetest Schematic File Format + + All values are stored in big-endian byte order. + [u32] signature: 'MTSM' + [u16] version: 3 + [u16] size X + [u16] size Y + [u16] size Z + For each Y: + [u8] slice probability value + [Name-ID table] Name ID Mapping Table + [u16] name-id count + For each name-id mapping: + [u16] name length + [u8[] ] name + ZLib deflated { + For each node in schematic: (for z, y, x) + [u16] content + For each node in schematic: + [u8] probability of occurance (param1) + For each node in schematic: + [u8] param2 + } + + Version changes: + 1 - Initial version + 2 - Fixed messy never/always place; 0 probability is now never, 0xFF is always + 3 - Added y-slice probabilities; this allows for variable height structures +--]] + +--handle_schematics = {} + +-- taken from https://github.com/MirceaKitsune/minetest_mods_structures/blob/master/structures_io.lua (Taokis Sructures I/O mod) +-- gets the size of a structure file +-- nodenames: contains all the node names that are used in the schematic +-- 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 ) + local size = { x = 0, y = 0, z = 0, version = 0 } + local version = 0; + + local file = io.open(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)) + end + + local function get_schematic_size(f) + -- make sure those are the first 4 characters, otherwise this might be a corrupt file + if f:read(4) ~= "MTSM" then + return nil + end + -- advance 2 more characters + local version = read_s16(f); --f:read(2) + -- the next characters here are our size, read them + return read_s16(f), read_s16(f), read_s16(f), version + end + + size.x, size.y, size.z, size.version = get_schematic_size(file) + + -- read the slice probability for each y value that was introduced in version 3 + if( size.version >= 3 ) then + -- the probability is not very intresting for buildings so we just skip it + file:read( size.y ); + end + + + -- this list is not yet used for anything + local nodenames = {}; + -- 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 = {}; + + -- after that: read_s16 (2 bytes) to find out how many diffrent nodenames (node_name_count) are present in the file + local node_name_count = read_s16( file ); + + for i = 1, node_name_count do + + -- the length of the next name + local name_length = read_s16( file ); + -- the text of the next name + local name_text = file:read( name_length ); + + 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 + end + end + + -- decompression was recently added; if it is not yet present, we need to use normal place_schematic + if( minetest.decompress == nil) then + file.close(file); + return nil; -- normal place_schematic is no longer supported as minetest.decompress is now part of the release version of minetest +-- 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 = {}; + 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; + + if( id ~= is_air ) then + scm[y][x][z] = {id, p2}; -- TODO: handle possible meta values contained in another file + end + end + end + 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 = io.open(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) ); + 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).'); +end + + +-- read .mts and .we files +handle_schematics.analyze_file = function( file_name, origin_offset, store_as_mts ) + local res = handle_schematics.analyze_mts_file( file_name ); + -- alternatively, read the mts file + if( not( res )) then + res = handle_schematics.analyze_we_file( file_name, origin_offset ); + -- convert to .mts for later usage + if( res and store_as_mts ) then + handle_schematics.store_mts_file( store_as_mts, res ); + end + end + return res; +end diff --git a/analyze_we_file.lua b/analyze_we_file.lua new file mode 100644 index 0000000..3e7da65 --- /dev/null +++ b/analyze_we_file.lua @@ -0,0 +1,83 @@ +handle_schematics.analyze_we_file = function(scm, we_origin) + local c_ignore = minetest.get_content_id("ignore") + + -- this table will contain the nodes read + local nodes = {} + + -- 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 = io.open( scm..".we", "r") + if not f then + f, err = io.open( scm..".wem", "r") + if not f then + error("Could not open schematic '" .. scm .. ".we': " .. err) + return nil; + end + end + + local value = f:read("*a") + f:close() + + local nodes = worldedit_file.load_schematic(value, we_origin) + + -- 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 + 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 + if scm[ent.y] == nil then + scm[ent.y] = {} + end + if scm[ent.y][ent.x] == nil then + scm[ent.y][ent.x] = {} + end + if ent.param2 == nil then + ent.param2 = 0 + end + if ent.meta == nil then + ent.meta = {fields={}, inventory={}} + end + + scm[ent.y][ent.x][ent.z] = { nodenames_id[ ent.name ], ent.param2 }; --TODO ent.meta + + 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 }; +end diff --git a/build_chest.lua b/build_chest.lua new file mode 100644 index 0000000..743a390 --- /dev/null +++ b/build_chest.lua @@ -0,0 +1,824 @@ +----------------------------------------------------------------------------------------------------------------- +-- interface for manual placement of houses +----------------------------------------------------------------------------------------------------------------- + + +-- functions specific to the build_chest are now stored in this table +build_chest = {}; + +-- scaffolding that will be placed instead of other nodes in order to show +-- how large the building will be +build_chest.SUPPORT = 'build_chest:support'; + + +-- contains information about all the buildings +build_chest.building = {}; + +-- returns the id under which the building is stored +build_chest.add_building = function( file_name, data ) + if( not( file_name ) or not( data )) then + return; + end + build_chest.building[ file_name ] = data; +end + +-- that many options can be shown simultaneously on one menu page +build_chest.MAX_OPTIONS = 24; -- 3 columns with 8 entries each + + +build_chest.menu = {}; +build_chest.menu.main = {}; + +-- create a tree structure for the menu +build_chest.add_entry = function( path ) + if( not( path ) or #path<1 ) then + return; + end + + local sub_menu = build_chest.menu; + for i,v in ipairs( path ) do + if( not( sub_menu[ v ] )) then + sub_menu[ v ] = {}; + end + sub_menu = sub_menu[ v ]; + end +end + +-- add a menu entry that will always be available +build_chest.add_entry( {'save a building'} ); + +-- needed for saving buildings +build_chest.end_pos_list = {}; + +--dofile( minetest.get_modpath( minetest.get_current_modname()).."/build_chest_handle_replacements.lua"); +--dofile( minetest.get_modpath( minetest.get_current_modname()).."/build_chest_preview_image.lua"); +--dofile( minetest.get_modpath( minetest.get_current_modname()).."/build_chest_add_schems.lua"); + + + + +build_chest.read_building = function( building_name ) + -- read data + local res = handle_schematics.analyze_file( building_name, nil, nil ); + if( not( res )) then + return; + end + build_chest.building[ building_name ].size = res.size; + build_chest.building[ building_name ].nodenames = res.nodenames; + build_chest.building[ building_name ].rotated = res.rotated; + build_chest.building[ building_name ].burried = res.burried; + -- scm_data_cache is not stored as that would take up too much storage space + --build_chest.building[ building_name ].scm_data_cache = res.scm_data_cache; + + -- create a statistic about how often each node occours + build_chest.building[ building_name ].statistic = handle_schematics.count_nodes( res ); + + build_chest.building[ building_name ].preview = build_chest.preview_image_create_views( res, + build_chest.building[ building_name ].orients ); + return res; +end + + + +build_chest.get_start_pos = function( pos ) + -- rotate the building so that it faces the player + local node = minetest.get_node( pos ); + local meta = minetest.get_meta( pos ); + + local building_name = meta:get_string( 'building_name' ); + if( not( building_name )) then + return "No building_name provided."; + end + if( not( build_chest.building[ building_name ] )) then + return "No data found for this building."; + end + + if( not( build_chest.building[ building_name ].size )) then + if( not( build_chest.read_building( building_name ))) then + return "Unable to read data file of this building."; + end + end + local selected_building = build_chest.building[ building_name ]; + + local mirror = 0; -- place_schematic does not support mirroring + + local start_pos = {x=pos.x, y=pos.y, z=pos.z}; + -- yoff(set) from mg_villages (manually given) + if( selected_building.yoff ) then + start_pos.y = start_pos.y + selected_building.yoff -1; + end + + -- make sure the building always extends forward and to the right of the player + local param2_rotated = handle_schematics.translate_param2_to_rotation( node.param2, mirror, start_pos, + selected_building.size, selected_building.rotated, selected_building.burried, selected_building.orients, + selected_building.yoff ); + + -- save the data for later removal/improvement of the building in the chest + meta:set_string( 'start_pos', minetest.serialize( param2_rotated.start_pos )); + meta:set_string( 'end_pos', minetest.serialize( param2_rotated.end_pos )); + meta:set_string( 'rotate', tostring(param2_rotated.rotate )); + meta:set_int( 'mirror', mirror ); + -- no replacements yet + meta:set_string( 'replacements', minetest.serialize( {} )); + return start_pos; +end + + + + + +build_chest.update_formspec = function( pos, page, player, fields ) + + -- information about the village the build chest may belong to and about the owner + local meta = minetest.get_meta( pos ); + local village_name = meta:get_string( 'village' ); + local village_pos = minetest.deserialize( meta:get_string( 'village_pos' )); + local owner_name = meta:get_string( 'owner' ); + local building_name = meta:get_string('building_name' ); + + -- distance from village center + local distance = math.floor( math.sqrt( (village_pos.x - pos.x ) * (village_pos.x - pos.x ) + + (village_pos.y - pos.y ) * (village_pos.x - pos.y ) + + (village_pos.z - pos.z ) * (village_pos.x - pos.z ) )); + + + if( page == 'please_remove' ) then + if( build_chest.stages_formspec_page_please_remove ) then + return build_chest.stages_formspec_page_please_remove( building_name, owner_name, village_name, village_pos, distance ); + end + elseif( page == 'finished' ) then + if( build_chest.stages_formspec_page_finished ) then + return build_chest.stages_formspec_page_finished( building_name, owner_name, village_name, village_pos, distance ); + end + elseif( page ~= 'main' ) then + -- if in doubt, return the old formspec + return meta:get_string('formspec'); + end + + + -- create the header + local formspec = "size[13,10]".. + "label[3.3,0.0;Building box]".. + "label[0.3,0.4;Located at:]" .."label[3.3,0.4;"..(minetest.pos_to_string( pos ) or '?')..", which is "..tostring( distance ).." m away]" + .."label[7.3,0.4;from the village center]".. + "label[0.3,0.8;Part of village:]" .."label[3.3,0.8;"..(village_name or "?").."]" + .."label[7.3,0.8;located at "..(minetest.pos_to_string( village_pos ) or '?').."]".. + "label[0.3,1.2;Owned by:]" .."label[3.3,1.2;"..(owner_name or "?").."]".. + "label[3.3,1.6;Click on a menu entry to select it:]"; + + + if( building_name and building_name ~= '' and build_chest.building[ building_name ] and build_chest.building[ building_name ].size) then + local size = build_chest.building[ building_name ].size; + formspec = formspec.. + -- show which building has been selected + "label[0.3,9.5;Selected building:]".. + "label[2.3,9.5;"..minetest.formspec_escape(building_name).."]".. + -- size of the building + "label[0.3,9.8;Size ( wide x length x height ):]".. + "label[4.3,9.8;"..tostring( size.x )..' x '..tostring( size.z )..' x '..tostring( size.y ).."]"; + end + + local current_path = minetest.deserialize( meta:get_string( 'current_path' ) or 'return {}' ); + if( #current_path > 0 ) then + formspec = formspec.."button[9.9,0.4;2,0.5;back;Back]"; + end + + + -- offer a menu to set the positions for saving a building + if( #current_path > 0 and current_path[1]=='save a building' ) then + local saved_as_filename = meta:get_string('saved_as_filename'); + if( saved_as_filename and saved_as_filename ~= "" ) then + local p1str = meta:get_string('p1'); + local p2str = meta:get_string('p2'); + + return formspec.. + "label[2.0,3;This area has been saved to the file]".. + "label[2.5,3.3;"..minetest.formspec_escape( saved_as_filename ).."]".. + "label[2.0,3.6;The area extends from]".. + "label[2.5,3.9;"..minetest.formspec_escape( p1str ).."]".. + "label[2.0,4.2;to the point]".. + "label[2.5,4.5;"..minetest.formspec_escape( p2str ).."]".. + "button[5,8.0;3,0.5;back;Back]"; + end + + local end_pos_mark = build_chest.end_pos_list[ player:get_player_name() ]; + if( end_pos_mark + and end_pos_mark.x==pos.x + and end_pos_mark.y==pos.y + and end_pos_mark.z==pos.z ) then + + return formspec.. + "label[2,3.0;This chest marks the end position of your building. Please put another]".. + "label[2,3.3;build chest in front of your building and save it with that chest.]".. + "button[5,8.0;3,0.5;back;Back]"; + end + + if( end_pos_mark and end_pos_mark.start_pos ) then + + if( end_pos_mark.start_pos.x == pos.x + and end_pos_mark.start_pos.y == pos.y + and end_pos_mark.start_pos.z == pos.z ) then + local p2 = {x=end_pos_mark.x, y=end_pos_mark.y, z=end_pos_mark.z}; + local p1 = {x=end_pos_mark.start_pos.x, y=end_pos_mark.start_pos.y, z=end_pos_mark.start_pos.z}; + local height = math.abs( p1.y - p2.y )+1; + local width = 0; + local length = 0; + if( end_pos_mark.param2==0 or end_pos_mark.param2==2 ) then + -- adjust p1 and p2 so that only the area we really care about is marked + if( p1.z > p2.z ) then + p1.z = p1.z-1; + p2.z = p2.z+1; + else + p1.z = p1.z+1; + p2.z = p2.z-1; + end + width = math.abs( p1.x - p2.x )+1; + length = math.abs( p1.z - p2.z )+1; + else + if( p1.x > p2.x ) then + p1.x = p1.x-1; + p2.x = p2.x+1; + else + p1.x = p1.x+1; + p2.x = p2.x-1; + end + length = math.abs( p1.x - p2.x )+1; + width = math.abs( p1.z - p2.z )+1; + end + return formspec.. + -- p1 and p2 are passed on as inputs in order to avoid any unwanted future interferences + -- with any other build chests + "field[40,40;0.1,0.1;save_as_p1;;"..minetest.pos_to_string(p1).."]".. + "field[40,40;0.1,0.1;save_as_p2;;"..minetest.pos_to_string(p2).."]".. + + "label[2,2.4;How high is your building? This does *not* include the height offset below. The]".. + "label[2,2.7;default value is calculated from the height difference between start and end position.]".. + "label[2,3.15;Total height of your building:]".. + "field[6,3.5;1,0.5;save_as_height;;"..tostring(height).."]".. + + -- note: in mg_villages, yoff has to be 0 in order to include the ground floor as well; + -- "1" means the building without floor; here, "1" means a floating building + "label[2,3.8;The hight offset sets how deep your building will be burried in the ground. Examples:]".. + "label[2.5,4.1;A value of -4 will include a cellar which extends 4 nodes below this build chest.]".. + "label[2.5,4.4;A value of -1 will include the floor below the chest, but no cellar.]".. + "label[2.5,4.7;A positive value will make your building float in the air.]".. + "label[2,5.15;Add height offset:]".. + "field[6,5.5;1,0.5;save_as_yoff;;0]".. + + "label[2,5.8;Without the changes entered in the input form above, your building will extend from]".. + "label[2.5,6.1;"..minetest.formspec_escape( + minetest.pos_to_string( p1 ).." to ".. + minetest.pos_to_string( p2 ).." and span a volume of ".. + -- x and z are swpapped here if rotated by 90 or 270 degree + tostring(width )..' (width) x '.. + tostring(length)..' (depth) x '.. + tostring(height)..' (height)').."]".. + + "label[2,6.7;Please enter a descriptive filename. Allowed charcters: ".. + minetest.formspec_escape("a-z, A-Z, 0-9, -, _, .").."]".. + "label[2,7.15;Save schematic as:]".. + "field[6,7.5;4,0.5;save_as_filename;;]".. + + "button[2,8.0;3,0.5;abort_set_start_pos;Abort]".. + "button[6,8.0;3,0.5;save_as;Save building now]"; + else + return formspec.. + "label[3,3;You have selected another build chest as start position.]".. + "button[5,8.0;3,0.5;back;Back]".. + "button[5,5.0;3,0.5;abort_set_start_pos;Reset start position]"; + end + end + + if( fields.error_msg ) then + return formspec.. + "label[4,4.5;Error while trying to set the start position:]".. + "textarea[4,5;6,2;error_msg;;".. + minetest.formspec_escape( fields.error_msg ).."]".. + "button[5,8.0;3,0.5;back;Back]"; + end + + return formspec.. + "label[2.5,2.2;First, let us assume that you are facing the front of this build chest.]".. + + "label[2,3.1;Are you looking at the BACKSIDE of your building, and does said backside stretch]".. + "label[2,3.4;to the right and in front of you? Then click on the button below:]".. + "button[4,4;5,0.5;set_end_pos;Set this position as new end position]".. + + "label[2,5.2;Have you set the end position with another build chest using the method above]".. + "label[2,5.5;in the meantime? And are you now looking at the FRONT of your building, which]".. + "label[2,5.8;streches in front of you and to the right? Then click on Proceed:]".. + "button[5,6.4;3,0.5;set_start_pos;Proceed with saving]".. + + "label[4,7.4;If this confuses you, you can also abort the process.]".. + "button[5,8.0;3,0.5;back;Abort]"; + end + + + -- the building has been placed; offer to restore a backup + local backup_file = meta:get_string('backup'); + if( backup_file and backup_file ~= "" ) then + return formspec.."button[3,3;3,0.5;restore_backup;Restore original landscape]"; + end + + -- offer diffrent replacement groups + if( fields.set_wood and fields.set_wood ~= "" ) then + return formspec.. + "label[1,2.2;Select replacement for "..tostring( fields.set_wood )..".]".. + "label[1,2.5;Trees, saplings and other blocks will be replaced accordingly as well.]".. + -- invisible field that encodes the value given here + "field[-20,-20;0.1,0.1;set_wood;;"..minetest.formspec_escape( fields.set_wood ).."]".. + build_chest.replacements_get_group_list_formspec( pos, 'wood', 'wood_selection' ); + end + + if( fields.set_farming and fields.set_farming ~= "" ) then + return formspec.. + "label[1,2.5;Select the fruit the farm is going to grow:]".. + -- invisible field that encodes the value given here + "field[-20,-20;0.1,0.1;set_farming;;"..minetest.formspec_escape( fields.set_farming ).."]".. + build_chest.replacements_get_group_list_formspec( pos, 'farming', 'farming_selection' ); + end + + if( fields.set_roof and fields.set_roof ~= "" ) then + return formspec.. + "label[1,2.5;Select a roof type for the house:]".. + -- invisible field that encodes the value given here + "field[-20,-20;0.1,0.1;set_roof;;"..minetest.formspec_escape( fields.set_roof ).."]".. + build_chest.replacements_get_group_list_formspec( pos, 'roof', 'roof_selection' ); + end + + if( fields.preview and building_name ) then + return formspec..build_chest.preview_image_formspec( building_name, + minetest.deserialize( meta:get_string( 'replacements' )), fields.preview); + end + + + -- show list of all node names used + local start_pos = meta:get_string('start_pos'); + if( building_name and building_name ~= '' and start_pos and start_pos ~= '' and meta:get_string('replacements')) then + return formspec..build_chest.replacements_get_list_formspec( pos ); + end + + -- find out where we currently are in the menu tree + local menu = build_chest.menu; + for i,v in ipairs( current_path ) do + if( menu and menu[ v ] ) then + menu = menu[ v ]; + end + end + + -- all submenu points at this menu position are options that need to be shown + local options = {}; + for k,v in pairs( menu ) do + table.insert( options, k ); + end + + -- handle if there are multiple files under the same menu point + if( #options == 0 and build_chest.building[ current_path[#current_path]] ) then + options = {current_path[#current_path]}; + end + + -- we have found an end-node - a particular building + if( #options == 1 and options[1] and build_chest.building[ options[1]] ) then + -- a building has been selected + meta:set_string( 'building_name', options[1] ); + local start_pos = build_chest.get_start_pos( pos ); + if( type(start_pos)=='table' and start_pos and start_pos.x and build_chest.building[ options[1]].size) then +-- TODO: also show size and such + -- do replacements for realtest where necessary (this needs to be done only once) + local replacements = {}; + replacements_group['realtest'].replace( replacements ); + meta:set_string( 'replacements', minetest.serialize( replacements )); + + return formspec..build_chest.replacements_get_list_formspec( pos ); + elseif( type(start_pos)=='string' ) then + return formspec.."label[3,3;Error reading building data:]".. + "label[3.5,3.5;"..start_pos.."]"; + else + return formspec.."label[3,3;Error reading building data.]"; + end + end + table.sort( options ); + + local page_nr = meta:get_int( 'page_nr' ); + -- if the options do not fit on a single page, split them up + if( #options > build_chest.MAX_OPTIONS ) then + if( not( page_nr )) then + page_nr = 0; + end + local new_options = {}; + local new_index = build_chest.MAX_OPTIONS*page_nr; + for i=1,build_chest.MAX_OPTIONS do + if( options[ new_index+i ] ) then + new_options[ i ] = options[ new_index+i ]; + end + end + + -- we need to add prev/next buttons to the formspec + formspec = formspec.."label[7.5,1.5;"..minetest.formspec_escape( + "Showing "..tostring( new_index+1 ).. + '-'..tostring( math.min( new_index+build_chest.MAX_OPTIONS, #options)).. + '/'..tostring( #options )).."]"; + if( page_nr > 0 ) then + formspec = formspec.."button[9.5,1.5;1,0.5;prev;prev]"; + end + if( build_chest.MAX_OPTIONS*(page_nr+1) < #options ) then + formspec = formspec.."button[11,1.5;1,0.5;next;next]"; + end + options = new_options; + end + + + -- found an end node of the menu graph +-- elseif( build_chest.stages_formspec_page_first_stage ) then +-- return build_chest.stages_formspec_page_first_stage( v.menu_path[( #current_path )], player, pos, meta, ); +-- end + + -- show the menu with the next options + local i = 0; + local x = 0; + local y = 0; + if( #options < 9 ) then + x = x + 4; + end + -- order alphabeticly + table.sort( options, function(a,b) return a < b end ); + + for index,k in ipairs( options ) do + + i = i+1; + + -- new column + if( y==8 ) then + x = x+4; + y = 0; + end + + formspec = formspec .."button["..(x)..","..(y+2.5)..";4,0.5;selection;"..k.."]" + y = y+1; + --x = x+4; + end + + return formspec; +end + + + +build_chest.on_receive_fields = function(pos, formname, fields, player) + + local meta = minetest.get_meta(pos); + + local owner = meta:get_string('owner'); + local pname = player:get_player_name(); + + -- make sure not everyone can mess up the build chest + if( owner and owner ~= '' and owner ~= pname + and minetest.is_protected( pos, pname )) then + minetest.chat_send_player( pname, + "Sorry. This build chest belongs to "..tostring( owner ).." and only ".. + "accepts input from its owner or other players who can build here."); + return; + end + +-- general menu handling + -- back button selected + if( fields.back ) then + + local current_path = minetest.deserialize( meta:get_string( 'current_path' ) or 'return {}' ); + + table.remove( current_path ); -- revert latest selection + meta:set_string( 'current_path', minetest.serialize( current_path )); + meta:set_string( 'building_name', ''); + meta:set_int( 'replace_row', 0 ); + meta:set_int( 'page_nr', 0 ); + meta:set_string( 'saved_as_filename', nil); + + -- menu entry selected + elseif( fields.selection ) then + + local current_path = minetest.deserialize( meta:get_string( 'current_path' ) or 'return {}' ); + table.insert( current_path, fields.selection ); + meta:set_string( 'current_path', minetest.serialize( current_path )); + + -- if there are more menu items than can be shown on one page: show previous page + elseif( fields.prev ) then + local page_nr = meta:get_int( 'page_nr' ); + if( not( page_nr )) then + page_nr = 0; + end + page_nr = math.max( page_nr - 1 ); + meta:set_int( 'page_nr', page_nr ); + + -- if there are more menu items than can be shown on one page: show next page + elseif( fields.next ) then + local page_nr = meta:get_int( 'page_nr' ); + if( not( page_nr )) then + page_nr = 0; + end + meta:set_int( 'page_nr', page_nr+1 ); + +-- specific to the build chest + -- the player has choosen a material from the list; ask for a replacement + elseif( fields.build_chest_replacements ) then + local event = minetest.explode_table_event( fields.build_chest_replacements ); + local building_name = meta:get_string('building_name'); + if( event and event.row and event.row > 0 + and building_name + and build_chest.building[ building_name ] ) then + + meta:set_int('replace_row', event.row ); + end + + -- the player has asked for a particular replacement + elseif( fields.store_replacement + and fields.replace_row_with and fields.replace_row_with ~= "" + and fields.replace_row_material and fields.replace_row_material ~= "") then + + build_chest.replacements_apply( pos, meta, fields.replace_row_material, fields.replace_row_with ); + + + elseif( fields.wood_selection ) then + build_chest.replacements_apply_for_group( pos, meta, 'wood', fields.wood_selection, fields.set_wood ); + fields.set_wood = nil; + + elseif( fields.farming_selection ) then + build_chest.replacements_apply_for_group( pos, meta, 'farming', fields.farming_selection, fields.set_farming ); + fields.set_farming = nil; + + elseif( fields.roof_selection ) then + build_chest.replacements_apply_for_group( pos, meta, 'roof', fields.roof_selection, fields.set_roof ); + fields.set_roof = nil; + + + elseif( fields.proceed_with_scaffolding ) then + local building_name = meta:get_string('building_name'); + local start_pos = minetest.deserialize( meta:get_string('start_pos')); + local end_pos = minetest.deserialize( meta:get_string('end_pos')); + local filename = meta:get_string('backup' ); + if( not( filename ) or filename == "" ) then + -- /backup_PLAYERNAME_x_y_z_burried_rotation.mts + filename = minetest.get_worldpath()..'/backup_'.. + meta:get_string('owner')..'_'.. + tostring( start_pos.x )..':'..tostring( start_pos.y )..':'..tostring( start_pos.z )..'_'.. + '0_0.mts'; + +-- TODO: handle metadata + -- store a backup of the original landscape + minetest.create_schematic( start_pos, end_pos, nil, filename, nil); + meta:set_string('backup', filename ); + + minetest.chat_send_player( pname, 'CREATING backup schematic for this place in '..tostring( filename )..'.'); + end + +-- TODO: use scaffolding here (exchange some replacements) + local replacement_list = minetest.deserialize( meta:get_string( 'replacements' )); + local rotate = meta:get_string('rotate'); + local mirror = meta:get_string('mirror'); + local axis = build_chest.building[ building_name ].axis; + local no_plotmarker = 1; + -- actually place the building + --minetest.place_schematic( start_pos, building_name..'.mts', rotate, replacement_list, true ); +mirror = nil; + fields.error_msg = handle_schematics.place_building_from_file( start_pos, end_pos, building_name, replacement_list, rotate, axis, mirror, no_plotmarker ); + if( fields.error_msg ) then + fields.error_msg = 'Error: '..tostring( fields.error_msg ); + end + + -- restore the original landscape + elseif( fields.restore_backup ) then + local start_pos = minetest.deserialize( meta:get_string('start_pos')); + local end_pos = minetest.deserialize( meta:get_string('end_pos')); + local backup_file = meta:get_string( 'backup' ); + if( start_pos and end_pos and start_pos.x and end_pos.x and backup_file and backup_file ~= "") then + minetest.place_schematic( start_pos, backup_file, "0", {}, true ); + meta:set_string('backup', nil ); + end + + + -- store a new end position + elseif( fields.set_end_pos ) then + local node = minetest.get_node( pos ); + if( node and node.param2 ) then + build_chest.end_pos_list[ pname ] = {x=pos.x, y=pos.y, z=pos.z, param2=node.param2 }; + end + + + elseif( fields.set_start_pos ) then + local error_msg = ""; + local end_pos = build_chest.end_pos_list[ pname ]; + if( not( end_pos )) then + error_msg = "Please mark the end position of your building first!"; + else + local node = minetest.get_node( pos ); + if( not( node ) or not( node.param2 )) then + error_msg = "A strange error happened."; + elseif( (node.param2 == 0 and end_pos.param2 ~= 2) + or (node.param2 == 1 and end_pos.param2 ~= 3) + or (node.param2 == 2 and end_pos.param2 ~= 0) + or (node.param2 == 3 and end_pos.param2 ~= 1)) then + error_msg = "One build chest needs to point to the front of your building, and ".. + "the other one to the backside. This does not seem to be the case."; + + elseif( (node.param2 == 2 and ( pos.x < end_pos.x or pos.z < end_pos.z )) -- x and z need to get larger + or (node.param2 == 3 and ( pos.x < end_pos.x or pos.z > end_pos.z )) -- x gets larger, z gets smaller + or (node.param2 == 0 and ( pos.x > end_pos.x or pos.z > end_pos.z )) -- x and z need to get smaller + or (node.param2 == 1 and ( pos.x > end_pos.x or pos.z < end_pos.z )) -- x gets smaller, z gets larger + ) then + error_msg = "The end position does not fit to the orientation of this build chest."; + + -- the chest takes up one node as well + elseif( math.abs(pos.x-end_pos.x)<1) then + error_msg = "Start- and end position share the same x value."; + + elseif( math.abs(pos.z-end_pos.z)<1) then + error_msg = "Start- and end position share the same z value."; + + -- all ok; we may proceed + else + error_msg = ""; + build_chest.end_pos_list[ pname ].start_pos = {x=pos.x, y=pos.y, z=pos.z, param2=node.param2 }; + end + fields.error_msg = error_msg; + end + + -- in case the player selected the wrong chest for the save dialog + elseif( fields.abort_set_start_pos ) then + local end_pos = build_chest.end_pos_list[ pname ]; + if( end_pos ) then + build_chest.end_pos_list[ pname ].start_pos = nil; + end + + + elseif( fields.save_as ) then + if( fields.save_as_p1 and fields.save_as_p2 and fields.save_as_filename ) then + -- restore p1 and p2, the positions of the area that is to be saved + local p1 = minetest.string_to_pos( fields.save_as_p1 ); + local p2 = minetest.string_to_pos( fields.save_as_p2 ); + + -- take height changes into account + if( fields.save_as_height ) then + local new_height = tonumber( fields.save_as_height ); + -- the new height is measured from the start position as well + if( new_height and new_height ~= (math.abs(p1.y-p2.y)+1)) then + p2.y = p1.y+new_height; + end + end + + local burried = 0; + if( fields.save_as_yoff ) then + burried = tonumber( fields.save_as_yoff ); + if( not( burried )) then + burried = 0; + end + -- the yoffset is applied to the start position + p1.y = p1.y + burried; + -- TODO: real negative values are not supported by analyze_mts_file + if( burried ~= 0 ) then + burried = -1*burried; + end + end + + -- create an automatic filename if none is provided + local filename = fields.save_as_filename; + -- TODO: check the input if it contains only allowed chars (a-z, A-Z, 0-9, -, _, .) + if( not( filename )) then + filename = pname..'_'..tostring( p1 )..'_'..tostring(p2); + end + + -- param2 needs to be translated init initial rotation as well + local node = minetest.get_node( pos ); + if( node.param2 == 0 ) then + filename = filename..'_'..burried..'_90'; + elseif( node.param2 == 3 ) then + filename = filename..'_'..burried..'_180'; + elseif( node.param2 == 1 ) then + filename = filename..'_'..burried..'_0'; + elseif( node.param2 == 2 ) then + filename = filename..'_'..burried..'_270'; + end + -- TODO: what if there is no schems folder in that directory? + -- TODO: forbid overwriting existing files? + local worldpath = minetest.get_worldpath(); + local filename_complete = worldpath..'/schems/'..filename..'.mts'; + -- really save it with probability_list and slice_prob_list both as nil + minetest.create_schematic( p1, p2, nil, filename_complete, nil); + + -- store that we have saved this area + meta:set_string('saved_as_filename', filename); + meta:set_string('p1', minetest.pos_to_string( p1 )); + meta:set_string('p2', minetest.pos_to_string( p2 )); + -- forget the end position + build_chest.end_pos_list[ pname ] = nil; + + -- add this chest to the menu + local worldnameparts = string.split( worldpath, '/worlds/' ); + if( not( worldnameparts ) or #worldnameparts < 1 ) then + worldnameparts = {'unkown world'}; + end + build_chest.add_entry( {'main','worlds', worldnameparts[ #worldnameparts], 'schems', filename, worldpath..'/schems/'..filename}); + build_chest.add_building( worldpath..'/schems/'..filename, {scm=filename, typ='nn'}); + + minetest.chat_send_player( pname, + 'Created schematic \''..tostring( filename )..'\'. Saved area from '.. + minetest.pos_to_string( p1 )..' to '.. + minetest.pos_to_string( p2 )); + end + end + -- the final build stage may offer further replacements + if( build_chest.stages_on_receive_fields ) then + build_chest.stages_on_receive_fields(pos, formname, fields, player, meta); + end + + meta:set_string( 'formspec', build_chest.update_formspec( pos, 'main', player, fields )); +end + + + +minetest.register_node("handle_schematics:build", { --TODO + description = "Building-Spawner", + tiles = {"default_chest_side.png", "default_chest_top.png", "default_chest_side.png", + "default_chest_side.png", "default_chest_side.png", "default_chest_front.png"}, +-- drawtype = 'signlike', +-- paramtype = "light", +-- paramtype2 = "wallmounted", +-- sunlight_propagates = true, +-- walkable = false, +-- selection_box = { +-- type = "wallmounted", +-- }, + + paramtype2 = "facedir", + groups = {snappy=2,choppy=2,oddly_breakable_by_hand=2}, + legacy_facedir_simple = true, + after_place_node = function(pos, placer, itemstack) + + -- TODO: check if placement is allowed + + local meta = minetest.get_meta( pos ); + meta:set_string( 'current_path', minetest.serialize( {} )); + meta:set_string( 'village', 'BEISPIELSTADT' ); --TODO + meta:set_string( 'village_pos', minetest.serialize( {x=1,y=2,z=3} )); -- TODO + meta:set_string( 'owner', placer:get_player_name()); + + meta:set_string('formspec', build_chest.update_formspec( pos, 'main', placer, {} )); + end, + on_receive_fields = function( pos, formname, fields, player ) + return build_chest.on_receive_fields(pos, formname, fields, player); + end, + -- taken from towntest + allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) + if from_list=="needed" or to_list=="needed" then return 0 end + return count + end, + allow_metadata_inventory_put = function(pos, listname, index, stack, player) + if listname=="needed" then return 0 end + return stack:get_count() + end, + allow_metadata_inventory_take = function(pos, listname, index, stack, player) + if listname=="needed" then return 0 end +-- if listname=="lumberjack" then return 0 end + return stack:get_count() + end, + + can_dig = function(pos,player) + local meta = minetest.get_meta( pos ); + local inv = meta:get_inventory(); + local owner_name = meta:get_string( 'owner' ); + local building_name = meta:get_string( 'building_name' ); + local name = player:get_player_name(); + + if( not( meta ) or not( owner_name )) then + return true; + end + if( owner_name ~= name ) then + minetest.chat_send_player(name, "This building chest belongs to "..tostring( owner_name )..". You can't take it."); + return false; + end + if( building_name ~= nil and building_name ~= "" ) then + minetest.chat_send_player(name, "This building chest has been assigned to a building project. You can't take it away now."); + return false; + end + return true; + end, + + -- have all materials been supplied and the remaining parts removed? + on_metadata_inventory_take = function(pos, listname, index, stack, player) + local meta = minetest.get_meta( pos ); + local inv = meta:get_inventory(); + local stage = meta:get_int( 'building_stage' ); + + if( inv:is_empty( 'needed' ) and inv:is_empty( 'main' )) then + if( stage==nil or stage < 6 ) then + build_chest.update_needed_list( pos, stage+1 ); -- request the material for the very first building step + else + meta:set_string( 'formspec', build_chest.update_formspec( pos, 'finished', player, {} )); + end + end + end, + + on_metadata_inventory_put = function(pos, listname, index, stack, player) + return build_chest.on_metadata_inventory_put( pos, listname, index, stack, player ); + end, + +}) + + diff --git a/build_chest_add_schems_from_file.lua b/build_chest_add_schems_from_file.lua new file mode 100644 index 0000000..ba8ab6b --- /dev/null +++ b/build_chest_add_schems_from_file.lua @@ -0,0 +1,55 @@ + + +build_chest.add_files_to_menu = function( path, add_path ) + local file,error = io.open( path, "rb") + if (file == nil) then + return; + end + + local text = file:read("*a"); + file:close(); + + for schem_file_name in string.gmatch(text, "([^\r\n]*)[\r\n]*") do + if( schem_file_name and schem_file_name ~= "" ) then + local help = string.split( schem_file_name, '/', true, -1, false); + + local i = #help; + local found = 1; + -- search from the end of the file name for the first occourance of "mods" or "worlds" + -- as that will be the path where we will put it into the menu + while (i>1 and found==1) do + if( help[i]=='mods' or help[i]=='worlds' ) then + found = i; + end + i = i-1; + end + + local name = help[#help]; + local length1 = string.len( name ); + local length2 = string.len( schem_file_name ); + -- remove the file name extension + if( string.sub( name, -4 )=='.mts' ) then + name = string.sub( name, 1, length1-4 ); + schem_file_name = string.sub( schem_file_name, 1, length2-4 ); + elseif( string.sub( name, -3 )=='.we' ) then + name = string.sub( name, 1, length1-3 ); + schem_file_name = string.sub( schem_file_name, 1, length2-3 ); + end + help[#help] = name; + + -- build the new menu path + local menu_path = {'main'}; + for j=(i+1),#help do + table.insert( menu_path, help[j] ); + end + schem_file_name = add_path..schem_file_name; + table.insert( menu_path, schem_file_name ); + + build_chest.add_entry( menu_path ); + build_chest.add_building( schem_file_name, {scm=help[#help], typ='nn'}); + + end + end +end + +build_chest.add_files_to_menu( minetest.get_modpath( minetest.get_current_modname()).."/list_of_schematics.txt", ""); diff --git a/build_chest_handle_replacements.lua b/build_chest_handle_replacements.lua new file mode 100644 index 0000000..a10bb65 --- /dev/null +++ b/build_chest_handle_replacements.lua @@ -0,0 +1,219 @@ +------------------------------------------------------------- +--- contains the handling of replacements for the build chest +------------------------------------------------------------- + +-- internal function +build_chest.replacements_get_extra_buttons = function( group, name, types_found_list, button_name, extra_buttons ) + -- find out if there are any nodes that may need a group replacement + local found_type = ""; + for k,w in ipairs( replacements_group[ group ].all ) do + -- we have found the full block of that group type + if( name == w ) then + found_type = w; + -- no primary node found; there may still be subordinate types + else + for nr,t in ipairs( replacements_group[ group ].data[ w ] ) do + if( name==t and not( types_found_list[ w ])) then + found_type = w; + end + end + end + end + if( found_type ~= "" and not( types_found_list[ found_type ])) then + extra_buttons.offset = extra_buttons.offset + 1; + extra_buttons.text = extra_buttons.text.."button[9.9,".. + tostring( (extra_buttons.offset*0.9)+2.8 )..";3.0,0.5;".. + tostring( button_name )..";".. + minetest.formspec_escape( found_type ).."]"; + -- remember that we found and offered this type already; avoid duplicates + types_found_list[ found_type ] = 1; + end + return extra_buttons; +end + + + +build_chest.replacements_get_list_formspec = function( pos, selected_row ) + if( not( pos )) then + return ""; + end + local meta = minetest.env:get_meta( pos ); + local replacements = minetest.deserialize( meta:get_string( 'replacements' )); + local building_name = meta:get_string( 'building_name' ); + if( not( building_name ) or not( build_chest.building[ building_name ])) then + return ""; + end + local replace_row = meta:get_int('replace_row'); + + local formspec = "tableoptions[" .. + "color=#ff8000;" .. + "background=#0368;" .. + "border=true;" .. + --"highlight=#00008040;" .. + "highlight=#aaaaaaaa;" .. + "highlight_text=#7fffff]" .. + "tablecolumns[" .. + "color;" .. + "text,width=1,align=right;" .. + "color;" .. + "text,width=5;" .. + "color;" .. + "text,width=1;" .. + "color;" .. + "text,width=5]" .. +-- "tabheader[".. +-- "1,1;columns;amount,original material,,target material;1;true;true]".. + "table[".. + "0.5,2.7;9.4,6.8;build_chest_replacements;"; + + local j=1; + local may_proceed = true; + local replace_row_material = nil; + local replace_row_with = ""; + -- make sure the statistic has been created + if( not( build_chest.building[ building_name ].statistic )) then + if( not( build_chest.read_building( building_name ))) then + return "label[2,2;Error: Unable to read building file.]"; + end + end + + -- used for setting wood type or plant(farming) type etc. + local extra_buttons = { text = "", offset = 0}; + -- there may be wood types that only occour as stairs and/or slabs etc., without full blocks + local types_found_list_wood = {}; + local types_found_list_farming = {}; + local types_found_list_roof = {}; + + local not_the_first_entry = false; + for i,v in ipairs( build_chest.building[ building_name ].statistic ) do + local name = build_chest.building[ building_name ].nodenames[ v[1]]; + -- nodes that are to be ignored do not need to be replaced + if( name ~= 'air' and name ~= 'ignore' and name ~= 'mg:ignore' and v[2] and v[2]>0) then + local anz = v[2]; + -- find out if this node name gets replaced + local repl = name; + for j,r in ipairs( replacements ) do + if( r and r[1]==name ) then + repl = r[2]; + end + end + + -- avoid empty lines at the end + if( not_the_first_entry ) then + formspec = formspec..','; + end + + formspec = formspec..'#fff,'..tostring( anz )..','; + if( name == repl and repl and minetest.registered_nodes[ repl ]) then + formspec = formspec.."#0ff,,#fff,,"; + else + if( name and minetest.registered_nodes[ name ] ) then + formspec = formspec.."#0f0,"; -- green + else + formspec = formspec.."#ff0,"; -- yellow + end + formspec = formspec..name..',#fff,'..minetest.formspec_escape('-->')..','; + end + + if( repl and (minetest.registered_nodes[ repl ] or repl=='air') ) then + formspec = formspec.."#0f0,"..repl; -- green + else + formspec = formspec.."#ff0,?"; -- yellow + may_proceed = false; -- we need a replacement for this material + end + + if( j == replace_row ) then + replace_row_material = name; + if( repl ~= name ) then + replace_row_with = repl; + end + end + + extra_buttons = build_chest.replacements_get_extra_buttons( 'wood', name, types_found_list_wood, 'set_wood', extra_buttons ); + extra_buttons = build_chest.replacements_get_extra_buttons( 'farming', name, types_found_list_farming, 'set_farming', extra_buttons ); + extra_buttons = build_chest.replacements_get_extra_buttons( 'roof', name, types_found_list_farming, 'set_roof', extra_buttons ); + + j=j+1; + + not_the_first_entry = true; + end + end + formspec = formspec.."]"; + -- add the proceed-button as soon as all unkown materials have been replaced + if( may_proceed ) then + formspec = formspec.."button[9.9,9.0;2.0,0.5;proceed_with_scaffolding;Proceed]"; + end + formspec = formspec.."button[9.9,1.0;2.0,0.5;preview;Preview]"; + if( extra_buttons.text and extra_buttons.text ~= "" ) then + formspec = formspec..extra_buttons.text.. + "label[9.9,2.8;Replace by type:]"; + end + if( replace_row_material ) then + formspec = formspec.. + "label[0.5,2.1;Replace ".. + minetest.formspec_escape( replace_row_material ).."]".. + "label[6.5,2.1;with:]".. + "field[7.5,2.4;4,0.5;replace_row_with;;".. + minetest.formspec_escape( replace_row_with ).."]".. + "field[-10,-10;0.1,0.1;replace_row_material;;".. + minetest.formspec_escape( replace_row_material ).."]".. + "button[11.1,2.1;1,0.5;store_replacement;Store]"; + end + return formspec; +end + + +build_chest.replacements_apply = function( pos, meta, old_material, new_material ) + -- a new value has been entered - we do not need to remember the row any longer + meta:set_int('replace_row', 0 ); + local found = false; + -- only accept replacements which can actually be placed + if( new_material=='air' or minetest.registered_nodes[ new_material ] ) then + local replacements_orig = minetest.deserialize( meta:get_string( 'replacements' )); + for i,v in ipairs(replacements_orig) do + if( v and v[1]==old_material ) then + v[2] = new_material; + found = true; + end + end + if( not( found )) then + table.insert( replacements_orig, { old_material, new_material }); + end + -- store the new set of replacements + meta:set_string( 'replacements', minetest.serialize( replacements_orig )); + end +end + + +build_chest.replacements_get_group_list_formspec = function( pos, group, button_name ) + local formspec = ""; + for i,v in ipairs( replacements_group[ group ].found ) do + formspec = formspec.."item_image_button["..tostring(((i-1)%8)+1)..",".. + tostring(3+math.floor((i-1)/8))..";1,1;".. + tostring( v )..";"..tostring( button_name )..";"..tostring(i).."]"; + end + return formspec; +end + + +build_chest.replacements_apply_for_group = function( pos, meta, group, selected, old_material ) + local nr = tonumber( selected ); + if( not(nr) or nr <= 0 or nr > #replacements_group[ group ].found ) then + return; + end + + local new_material = replacements_group[ group ].found[ nr ]; + if( old_material and old_material == new_material ) then + return; + end + + local replacements = minetest.deserialize( meta:get_string( 'replacements' )); + if( not( replacements )) then + replacements = {}; + end + replacements_group[ group ].replace_material( replacements, old_material, new_material ); + + -- store the new set of replacements + meta:set_string( 'replacements', minetest.serialize( replacements )); +end + diff --git a/build_chest_preview_image.lua b/build_chest_preview_image.lua new file mode 100644 index 0000000..213a949 --- /dev/null +++ b/build_chest_preview_image.lua @@ -0,0 +1,245 @@ + +build_chest.preview_image_draw_tile = function( content_id, image, x, z, dx, dz, tile_nr ) + if( not( image )) then + local node_name = minetest.get_name_from_content_id( content_id ); + if( not( node_name )) then + return ''; + end + local node_def = minetest.registered_nodes[ node_name ]; + if( not( node_def )) then + return ''; + end + local tiles = node_def.tiles; + local tile = nil; + if( tiles ~= nil ) then + if( not(tile_nr) or tile_nr > #tiles or tile_nr < 1 ) then + tile_nr = 1; + end + tile = tiles[tile_nr]; + end + if type(tile)=="table" then + tile=tile["name"] + end + image = tile; + if( not( image )) then + image = "unknown_object.png"; + end + end + return "image["..tostring(x)..",".. tostring(z) ..";"..dx..','..dz..";" .. image .."]"; +end + + + +-- creates a 2d preview image (or rather, the data structure for it) of the building +-- internal function +build_chest.preview_image_create_one_view = function( data, side ) + local params = {1, data.size.x, 1, 1, data.size.z, 1, 0, 0}; + if( side==1 ) then + params = {1, data.size.x, 1, 1, data.size.z, 1, 0, 0}; + elseif( side==2 ) then + params = {1, data.size.z, 1, 1, data.size.x, 1, 1, 1}; + elseif( side==3 ) then + params = {1, data.size.x, 1, data.size.z, 0, -1, 0, 1}; + elseif( side==4 ) then + params = {1, data.size.z, 1, data.size.x, 0, -1, 1, 0}; + end + + -- do not create preview images for buildings that are too big + if( params[2] * params[4] > 2500 ) then + return nil; + end + local preview = {}; + for y = 1, data.size.y do + preview[ y ] = {}; + for x = params[1], params[2], params[3] do + local found = nil; + local z = params[4]; + local target_x = x; + if( params[8]==1 ) then + target_x = math.max( params[1],params[2] )- x; + end + while( not( found ) and z~= params[5]) do + local node = -1; + if( params[7]==0 ) then + node = data.scm_data_cache[y][x][z]; + else + node = data.scm_data_cache[y][z][x]; + end + if( node and node[1] + and data.nodenames[ node[1] ] + and data.nodenames[ node[1] ] ~= 'air' + and data.nodenames[ node[1] ] ~= 'ignore' + and data.nodenames[ node[1] ] ~= 'mg:ignore' + and data.nodenames[ node[1] ] ~= 'default:torch' ) then + -- a preview node is only set if there's no air there + preview[y][target_x] = node[1]; + found = 1; + end + z = z+params[6]; + end + if( not( found )) then + preview[y][target_x] = -1; + end + end + end + return preview; +end + +-- internal function +build_chest.preview_image_create_view_from_top = function( data ) + -- no view from top if the image is too big + if( data.size.z * data.size.y > 2500 ) then + return nil; + end + + local preview = {}; + for z = 1, data.size.z do + preview[ z ] = {}; + for x = 1, data.size.x do + local found = nil; + local y = data.size.y; + while( not( found ) and y > 1) do + local node = data.scm_data_cache[y][x][z]; + if( node and node[1] + and data.nodenames[ node[1] ] + and data.nodenames[ node[1] ] ~= 'air' + and data.nodenames[ node[1] ] ~= 'ignore' + and data.nodenames[ node[1] ] ~= 'mg:ignore' + and data.nodenames[ node[1] ] ~= 'default:torch' ) then + -- a preview node is only set if there's no air there + preview[z][x] = node[1]; + found = 1; + end + y = y-1; + end + if( not( found )) then + preview[z][x] = -1; + end + end + end + return preview; +end + + +-- function called by the build chest to display one view +build_chest.preview_image_formspec = function( building_name, replacements, side_name ) + if( not( building_name ) + or not( build_chest.building[ building_name ] ) + or not( build_chest.building[ building_name ].preview )) then + return ""; + end + + local side_names = {"front","right","back","left","top"}; + local side = 1; + for i,v in ipairs( side_names ) do + if( side_name and side_name==v ) then + side = i; + end + end + + local formspec = ""; + for i=1,5 do + if( i ~= side ) then + formspec = formspec.."button["..tostring(3.3+1.2*(i-1)).. + ",2.2;1,0.5;preview;"..side_names[i].."]"; + else + formspec = formspec.."label["..tostring(3.3+1.2*(i-1))..",2.2;"..side_names[i].."]"; + end + end + + local data = build_chest.building[ building_name ]; + + -- the draw_tile function is based on content_id + local content_ids = {}; + for i,v in ipairs( data.nodenames ) do + local found = false; + for j,w in ipairs( replacements ) do + if( w and w[1] and w[1]==v) then + found = true; + if( minetest.registered_nodes[ w[2]] ) then + content_ids[ i ] = minetest.get_content_id( w[2] ); + end + end + end + if( not( found )) then + if( minetest.registered_nodes[ v ]) then + content_ids[ i ] = minetest.get_content_id( v ); + elseif( v ~= 'air' ) then + content_ids[ i ] = -1; + end + end + end + + local scale = 0.5; + + local tile_nr = 3; -- view from the side + if( side ~= 5 ) then + local scale_y = 6.0/data.size.y; + local scale_z = 10.0/data.size.z; + if( scale_y > scale_z) then + scale = scale_z; + else + scale = scale_y; + end + else + local scale_x = 10.0/data.size.x; -- only relevant for view from top + local scale_z = 6.0/data.size.z; + if( scale_x > scale_z) then + scale = scale_z; + else + scale = scale_x; + end + tile_nr = 1; -- view from top + end + + if( not( side )) then + side = 1; + end + local preview = data.preview[ side ]; + if( not( preview )) then + formspec = formspec.."label[3,3;Sorry, this schematic is too big for a preview image.]"; + return formspec; + end + for y,y_values in ipairs( preview ) do + for l,v in ipairs( y_values ) do + -- air, ignore and mg:ignore are not stored + if( v and content_ids[ v ]==-1 ) then + formspec = formspec..build_chest.preview_image_draw_tile( nil, "unknown_node.png", (l*scale), 9-(y*scale), scale*1.3, scale*1.2, tile_nr); + elseif( v and v>0 and content_ids[v]) then + formspec = formspec..build_chest.preview_image_draw_tile( content_ids[ v ], nil, (l*scale), 9-(y*scale), scale*1.3, scale*1.2, tile_nr); + end + end + end + return formspec; +end + + +-- create all five preview images +build_chest.preview_image_create_views = function( res, orients ) + + -- create a 2d overview image (or rather, the data structure for it) + local preview = { + build_chest.preview_image_create_one_view( res, 2 ), + build_chest.preview_image_create_one_view( res, 1 ), + build_chest.preview_image_create_one_view( res, 4 ), + build_chest.preview_image_create_one_view( res, 3 )}; + + -- the building might be stored in rotated form + if( orients and #orients and orients[1] ) then + if( orients[1]==1 ) then + preview = {preview[2],preview[3],preview[4],preview[1]}; + elseif( orients[1]==2 ) then + preview = {preview[3],preview[4],preview[1],preview[2]}; + elseif( orients[1]==3 ) then + preview = {preview[4],preview[1],preview[2],preview[3]}; + end + end + -- ...and add a preview image from top + preview[5] = build_chest.preview_image_create_view_from_top( res ); + return preview; +end + + + + +-- this function makes sure that the building will always extend to the right and in front of the build chest diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..9078e38 --- /dev/null +++ b/depends.txt @@ -0,0 +1,21 @@ +default? +doors? +farming? +wool? +stairs? +cottages? +moretrees? +trees? +forest? +dryplants? +cavestuff? +snow? +moresnow? +darkage? +ethereal? +deco? +metals? +grounds? +moreblocks? +bell? +mobf_trader? diff --git a/handle_schematics_misc.lua b/handle_schematics_misc.lua new file mode 100644 index 0000000..64360a9 --- /dev/null +++ b/handle_schematics_misc.lua @@ -0,0 +1,107 @@ + +-- helper function; sorts by the second element of the table +local function handle_schematics_comp(a,b) + if (a[2] > b[2]) then + return true; + end +end + +-- create a statistic about how frequent each node name occoured +handle_schematics.count_nodes = function( data ) + local statistic = {}; + -- make sure all node names are counted (air may sometimes be included without occouring) + for id=1, #data.nodenames do + statistic[ id ] = { id, 0}; + end + + for z = 1, data.size.z do + for y = 1, data.size.y do + for x = 1, data.size.x do + + local a = data.scm_data_cache[y][x][z]; + if( a ) then + local id = 0; + if( type( a )=='table' ) then + id = a[1]; + else + id = a; + end + if( statistic[ id ] and statistic[ id ][ 2 ] ) then + statistic[ id ] = { id, statistic[ id ][ 2 ]+1 }; + end + end + end + end + end + table.sort( statistic, handle_schematics_comp ); + return statistic; +end + + +-- this function makes sure that the building will always extend to the right and in front of the build chest +handle_schematics.translate_param2_to_rotation = function( param2, mirror, start_pos, orig_max, rotated, burried, orients, yoff ) + + -- mg_villages stores available rotations of buildings in orients={0,1,2,3] format + if( orients and #orients and orients[1]~=0) then + -- reset rotated - else we'd apply it twice + rotated = 0; + if( orients[1]==1 ) then + rotated = rotated + 90; + elseif( orients[1]==2 ) then + rotated = rotated + 180; + elseif( orients[1]==3 ) then + rotated = rotated + 270; + end + if( rotated >= 360 ) then + rotated = rotated % 360; + end + end + + local max = {x=orig_max.x, y=orig_max.y, z=orig_max.z}; + -- if the schematic has been saved in a rotated way, swapping x and z may be necessary + if( rotated==90 or rotated==270) then + max.x = orig_max.z; + max.z = orig_max.x; + end + + -- the building may have a cellar or something alike + if( burried and burried ~= 0 and yoff == nil ) then + start_pos.y = start_pos.y - burried; + end + + -- make sure the building always extends forward and to the right of the player + local rotate = 0; + if( param2 == 0 ) then rotate = 270; if( mirror==1 ) then start_pos.x = start_pos.x - max.x + max.z; end -- z gets larger + elseif( param2 == 1 ) then rotate = 0; start_pos.z = start_pos.z - max.z; -- x gets larger + elseif( param2 == 2 ) then rotate = 90; start_pos.z = start_pos.z - max.x; + if( mirror==0 ) then start_pos.x = start_pos.x - max.z; -- z gets smaller + else start_pos.x = start_pos.x - max.x; end + elseif( param2 == 3 ) then rotate = 180; start_pos.x = start_pos.x - max.x; -- x gets smaller + end + + if( param2 == 1 or param2 == 0) then + start_pos.z = start_pos.z + 1; + elseif( param2 == 1 or param2 == 2 ) then + start_pos.x = start_pos.x + 1; + end + if( param2 == 1 ) then + start_pos.x = start_pos.x + 1; + end + + rotate = rotate + rotated; + -- make sure the rotation does not reach or exceed 360 degree + if( rotate >= 360 ) then + rotate = rotate - 360; + end + -- rotate dimensions when needed + if( param2==0 or param2==2) then + local tmp = max.x; + max.x = max.z; + max.z = tmp; + end + + return { rotate=rotate, start_pos = {x=start_pos.x, y=start_pos.y, z=start_pos.z}, + end_pos = {x=(start_pos.x+max.x-1), y=(start_pos.y+max.y-1), z=(start_pos.z+max.z-1) }, + max = {x=max.x, y=max.y, z=max.z}}; +end + diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..91d4527 --- /dev/null +++ b/init.lua @@ -0,0 +1,51 @@ + + +handle_schematics.modpath = minetest.get_modpath( "handle_schematics"); + +-- adds worldedit_file.* namespace +-- deserialize worldedit savefiles +dofile(handle_schematics.modpath.."/worldedit_file.lua") + +-- uses handle_schematics.* namespace +handle_schematics = {} +-- reads and analyzes .mts files (minetest schematics) +dofile(handle_schematics.modpath.."/analyze_mts_file.lua") +-- reads and analyzes worldedit files +dofile(handle_schematics.modpath.."/analyze_we_file.lua") +-- handles rotation and mirroring +dofile(handle_schematics.modpath.."/rotate.lua") +-- count nodes, take param2 into account for rotation etc. +dofile(handle_schematics.modpath.."/handle_schematics_misc.lua") + +-- uses replacements_group.* namespace +-- these functions are responsible for the optional dependencies; they check +-- which nodes are available and may be offered as possible replacements +replacements_group = {}; +dofile(handle_schematics.modpath.."/replacements_wood.lua") +dofile(handle_schematics.modpath.."/replacements_realtest.lua") +dofile(handle_schematics.modpath.."/replacements_farming.lua") +dofile(handle_schematics.modpath.."/replacements_roof.lua") + +-- transforms the replacement list into a table; +-- also creates a replacement if needed and replaces default:torch +dofile(handle_schematics.modpath.."/replacements_get_table.lua") + +-- uses build_chest.* namespace +-- a chest for spawning buildings manually +dofile(handle_schematics.modpath.."/build_chest.lua") +-- makes the replacements from replacements_group.* available to the build chest +dofile(handle_schematics.modpath.."/build_chest_handle_replacements.lua"); +-- creates 2d previews of the schematic from left/right/back/front/top +dofile(handle_schematics.modpath.."/build_chest_preview_image.lua"); +-- reads a file and adds the files listed there as menu entries +dofile(handle_schematics.modpath.."/build_chest_add_schems_from_file.lua"); + +-- chooses traders and spawn positions for buildings +dofile(handle_schematics.modpath.."/village_traders.lua") + +-- the main functionality of the mod; +-- provides the function handle_schematics.place_building_from_file +-- (and also place_buildings for mg_villages) +dofile(handle_schematics.modpath.."/place_buildings.lua") + +-- dofile(handle_schematics.modpath.."/fill_chest.lua") diff --git a/place_buildings.lua b/place_buildings.lua new file mode 100644 index 0000000..f285d1b --- /dev/null +++ b/place_buildings.lua @@ -0,0 +1,810 @@ +-- TODO: this function also occours in replacements.lua +handle_schematics.get_content_id_replaced = function( node_name, replacements ) + if( not( node_name ) or not( replacements ) or not(replacements.table )) then + return minetest.get_content_id( 'ignore' ); + end + if( replacements.table[ node_name ]) then + return minetest.get_content_id( replacements.table[ node_name ] ); + else + return minetest.get_content_id( node_name ); + end +end + +-- either uses get_node_or_nil(..) or the data from voxelmanip +-- the function might as well be local (only used by *.mg_drop_moresnow) +handle_schematics.get_node_somehow = function( x, y, z, a, data, param2_data ) + if( a and data and param2_data ) then + return { content = data[a:index(x, y, z)], param2 = param2_data[a:index(x, y, z)] }; + end + -- no voxelmanip; get the node the normal way + local node = minetest.get_node_or_nil( {x=x, y=y, z=z} ); + if( not( node ) ) then + return { content = moresnow.c_ignore, param2 = 0 }; + end + return { content = minetest.get_content_id( node.name ), param2 = node.param2, name = node.name }; +end + + +-- "drop" moresnow snow on diffrent shapes; works for voxelmanip and node-based setting +handle_schematics.mg_drop_moresnow = function( x, z, y_top, y_bottom, a, data, param2_data) + + -- this only works if moresnow is installed + if( not( handle_schematics.moresnow_installed )) then + return; + end + + local y = y_top; + local node_above = handle_schematics.get_node_somehow( x, y+1, z, a, data, param2_data ); + local node_below = nil; + while( y >= y_bottom ) do + + node_below = handle_schematics.get_node_somehow( x, y, z, a, data, param2_data ); + if( node_above.content == moresnow.c_air + and node_below.content + and node_below.content ~= moresnow.c_ignore + and node_below.content ~= moresnow.c_air ) then + + -- turn water into ice, but don't drop snow on it + if( node_below.content == minetest.get_content_id("default:water_source") + or node_below.content == minetest.get_content_id("default:river_water_source")) then + return { height = y, suggested = {new_id = minetest.get_content_id('default:ice'), param2 = 0 }}; + end + + -- if the node below drops snow when digged (i.e. is either snow or a moresnow node), we're finished + local get_drop = minetest.get_name_from_content_id( node_below.content ); + if( get_drop ) then + get_drop = minetest.registered_nodes[ get_drop ]; + if( get_drop and get_drop.drop and type( get_drop.drop )=='string' and get_drop.drop == 'default:snow') then + return; + end + end + if( not(node_below.content) + or node_below.content == moresnow.c_snow ) then + return; + end + + local suggested = moresnow.suggest_snow_type( node_below.content, node_below.param2 ); + + -- c_snow_top and c_snow_fence can only exist when the node 2 below is a solid one + if( suggested.new_id == moresnow.c_snow_top + or suggested.new_id == moresnow.c_snow_fence) then + local node_below2 = handle_schematics.get_node_somehow( x, y-1, z, a, data, param2_data); + if( node_below2.content ~= moresnow.c_ignore + and node_below2.content ~= moresnow.c_air ) then + local suggested2 = moresnow.suggest_snow_type( node_below2.content, node_below2.param2 ); + + if( suggested2.new_id == moresnow.c_snow ) then + return { height = y+1, suggested = suggested }; + end + end + -- it is possible that this is not the right shape; if so, the snow will continue to fall down + elseif( suggested.new_id ~= moresnow.c_ignore ) then + + return { height = y+1, suggested = suggested }; + end + -- TODO return; -- abort; there is no fitting moresnow shape for the node below + end + y = y-1; + node_above = node_below; + end +end + + +-- helper function for generate_building +-- places a marker that allows players to buy plots with houses on them (in order to modify the buildings) +local function generate_building_plotmarker( pos, minp, maxp, data, param2_data, a, cid, building_nr_in_bpos, village_id, filename) + -- position the plot marker so that players can later buy this plot + building in order to modify it + -- pos.o contains the original orientation (determined by the road and the side the building is + local p = {x=pos.x, y=pos.y+1, z=pos.z}; + if( pos.o == 0 ) then + p.x = p.x - 1; + p.z = p.z + pos.bsizez - 1; + elseif( pos.o == 2 ) then + p.x = p.x + pos.bsizex; + elseif( pos.o == 1 ) then + p.z = p.z + pos.bsizez; + p.x = p.x + pos.bsizex - 1; + elseif( pos.o == 3 ) then + p.z = p.z - 1; + end + -- actually position the marker + if( p.x >= minp.x and p.x <= maxp.x and p.z >= minp.z and p.z <= maxp.z and p.y >= minp.y and p.y <= maxp.y) then + if( data[ a:index(p.x, p.y, p.z)] == cid.c_snow and p.y1, 5->2, 3->3, 4->0 } + new_nodes[ i ].change_param2[2] = 1; + new_nodes[ i ].change_param2[5] = 2; + new_nodes[ i ].change_param2[3] = 3; + new_nodes[ i ].change_param2[4] = 0; + new_nodes[ i ].paramtype2 = 'facedir'; + -- ..except if they are stairs or ladders + elseif( string.sub( node_name, 1, 7 ) == 'stairs:' or string.sub( node_name, 1, 6 ) == 'doors:') then + new_nodes[ i ].paramtype2 = 'facedir'; + -- normal nodes + elseif( regnode and regnode.paramtype2 and (regnode.paramtype2=='facedir' or regnode.paramtype2=='wallmounted')) then + new_nodes[ i ].paramtype2 = regnode.paramtype2; + end + + -- we tried our best, but the replacement node is not defined + elseif( new_node_name ~= 'mg:ignore' ) then + local msg = 'ERROR: Did not find a suitable replacement for '..tostring( node_name )..' (suggested but inexistant: '.. + tostring( new_node_name )..'). Building: '..tostring( binfo_scm )..'.'; + if( mg_villages and mg_villages.print ) then + mg_villages.print( mg_villages.DEBUG_LEVEL_WARNING, msg ); + else + print( msg ); + end + msg = nil; + new_nodes[ i ].ignore = 1; -- keep the old content + else -- handle mg:ignore + new_nodes[ i ].ignore = 1; + end + + + end + return new_nodes; +end + + +local function generate_building(pos, minp, maxp, data, param2_data, a, extranodes, replacements, cid, extra_calls, building_nr_in_bpos, village_id, binfo_extra, road_node) + + local binfo = binfo_extra; + if( not( binfo ) and mg_villages) then + binfo = mg_villages.BUILDINGS[pos.btype] + end + local scm + + -- the building got removed from mg_villages.BUILDINGS in the meantime + if( not( binfo )) then + return; + end + + -- schematics of .mts type are not handled here; they need to be placed using place_schematics + if( binfo.is_mts and binfo.is_mts == 1 ) then + return; + end + + + -- roads are very simple structures that are not stored as schematics + if( pos.btype == 'road' ) then + handle_schematics.place_road( minp, maxp, data, param2_data, a, road_node, pos, cid.c_air ); + return; + end + + + if( not( pos.no_plotmarker )) then + generate_building_plotmarker( pos, minp, maxp, data, param2_data, a, cid, building_nr_in_bpos, village_id, binfo.scm ); + end + + -- skip building if it is not located at least partly in the area that is currently beeing generated + if( pos.x > maxp.x or pos.x + pos.bsizex < minp.x + or pos.z > maxp.z or pos.z + pos.bsizez < minp.z ) then + return; + end + + + if( pos.btype and + (( binfo.sizex ~= pos.bsizex and binfo.sizex ~= pos.bsizez ) + or ( binfo.sizez ~= pos.bsizex and binfo.sizez ~= pos.bsizez ) + or not( binfo.scm_data_cache ))) then + if( mg_villages and mg_villages.print ) then + mg_villages.print( mg_villages.DEBUG_LEVEL_WARNING, + 'ERROR: This village was created using diffrent buildings than those known know. Cannot place unknown building.'); + else + print( 'ERROR: Size information about this building differs. Cannot place building.'); + end + return; + end + + if( binfo.scm_data_cache )then + scm = binfo.scm_data_cache; + else + scm = binfo.scm + end + + -- the fruit is set per building, not per village as the other replacements + if( binfo.farming_plus and binfo.farming_plus == 1 and pos.fruit and mg_villages) then + mg_villages.get_fruit_replacements( replacements, pos.fruit); + end + + local traders = {}; + if( handle_schematics.choose_traders ) then + local village_type = ""; + if( village_id and mg_villages and mg_villages.all_villages and mg_villages.all_villages[ village_id ] ) then + village_type = mg_villages.all_villages[ village_id ].village_type; + end + local building_type = ""; + if( binfo.typ ) then + building_type = binfo.typ; + end + if( not( pos.traders )) then + traders = handle_schematics.choose_traders( village_type, building_type, replacements ) + end + end + + local c_ignore = minetest.get_content_id("ignore") + local c_air = minetest.get_content_id("air") + local c_snow = minetest.get_content_id( "default:snow"); + local c_dirt = minetest.get_content_id( "default:dirt" ); + local c_dirt_with_grass = minetest.get_content_id( "default:dirt_with_grass" ); + local c_dirt_with_snow = minetest.get_content_id( "default:dirt_with_snow" ); + + local scm_x = 0; + local scm_z = 0; + local step_x = 1; + local step_z = 1; + local scm_z_start = 0; + + if( pos.brotate == 2 ) then + scm_x = pos.bsizex+1; + step_x = -1; + end + if( pos.brotate == 1 ) then + scm_z = pos.bsizez+1; + step_z = -1; + scm_z_start = scm_z; + end + + local mirror_x = false; + local mirror_z = false; + if( pos.mirror ) then + if( binfo.axis and binfo.axis == 1 ) then + mirror_x = true; + mirror_z = false; + else + mirror_x = false; + mirror_z = true; + end + end + + -- translate all nodenames and apply the replacements + local new_nodes = generate_building_translate_nodenames( binfo.nodenames, replacements, cid, binfo.scm, mirror_x, mirror_z ); + + for x = 0, pos.bsizex-1 do + scm_x = scm_x + step_x; + scm_z = scm_z_start; + for z = 0, pos.bsizez-1 do + scm_z = scm_z + step_z; + + local xoff = scm_x; + local zoff = scm_z; + if( pos.brotate == 2 ) then + if( mirror_x ) then + xoff = pos.bsizex - scm_x + 1; + end + if( mirror_z ) then + zoff = scm_z; + else + zoff = pos.bsizez - scm_z + 1; + end + elseif( pos.brotate == 1 ) then + if( mirror_x ) then + xoff = pos.bsizez - scm_z + 1; + else + xoff = scm_z; + end + if( mirror_z ) then + zoff = pos.bsizex - scm_x + 1; + else + zoff = scm_x; + end + elseif( pos.brotate == 3 ) then + if( mirror_x ) then + xoff = pos.bsizez - scm_z + 1; + else + xoff = scm_z; + end + if( mirror_z ) then + zoff = scm_x; + else + zoff = pos.bsizex - scm_x + 1; + end + elseif( pos.brotate == 0 ) then + if( mirror_x ) then + xoff = pos.bsizex - scm_x + 1; + end + if( mirror_z ) then + zoff = pos.bsizez - scm_z + 1; + end + end + + local has_snow = false; + local ground_type = c_dirt_with_grass; + for y = 0, binfo.ysize-1 do + local ax = pos.x+x; + local ay = pos.y+y+binfo.yoff; + local az = pos.z+z; + if (ax >= minp.x and ax <= maxp.x) and (ay >= minp.y and ay <= maxp.y) and (az >= minp.z and az <= maxp.z) then + + local new_content = c_air; + local t = scm[y+1][xoff][zoff]; + + local node_content = data[a:index(ax, ay, az)]; + if( binfo.yoff+y == 0 ) then + -- no snow on the gravel roads + if( node_content == c_dirt_with_snow or data[a:index(ax, ay+1, az)]==c_snow) then + has_snow = true; + end + + ground_type = node_content; + end + + if( not( t )) then + if( node_content ~= cid.c_plotmarker and (not(moresnow) or node_content ~= moresnow.c_snow_top )) then + data[ a:index(ax, ay, az)] = cid.c_air; + end + else + local n = new_nodes[ t[1] ]; -- t[1]: id of the old node + if( not( n.ignore )) then + new_content = n.new_content; + else + new_content = node_content; + end + + -- replace all dirt and dirt with grass at that x,z coordinate with the stored ground grass node; + if( n.is_grass ) then + new_content = ground_type; + end + + if( n.on_construct ) then + if( not( extra_calls.on_constr[ new_content ] )) then + extra_calls.on_constr[ new_content ] = { {x=ax, y=ay, z=az}}; + else + table.insert( extra_calls.on_constr[ new_content ], {x=ax, y=ay, z=az}); + end + end + + -- do not overwrite plotmarkers + if( new_content ~= cid.c_air or node_content ~= cid.c_plotmarker ) then + data[ a:index(ax, ay, az)] = new_content; + end + + -- store that a tree is to be grown there + if( n.is_tree ) then + table.insert( extra_calls.trees, {x=ax, y=ay, z=az, typ=new_content, snow=has_snow}); + + -- we're dealing with a chest that might need filling + elseif( n.is_chestlike ) then + table.insert( extra_calls.chests, {x=ax, y=ay, z=az, typ=new_content, bpos_i=building_nr_in_bpos}); + + -- the sign may require some text to be written on it + elseif( n.is_sign ) then + table.insert( extra_calls.signs, {x=ax, y=ay, z=az, typ=new_content, bpos_i=building_nr_in_bpos}); + end + + -- handle rotation + if( n.paramtype2 ) then + local param2 = t[2]; + if( n.change_param2 and n.change_param2[ t[2] ]) then + param2 = n.change_param2[ param2 ]; + end + + local np2 = 0; + if( mirror_x ) then + np2 = handle_schematics.rotation_table[ n.paramtype2 ][ param2+1 ][ pos.brotate+1 ][ 2 ]; + elseif( mirror_z ) then + np2 = handle_schematics.rotation_table[ n.paramtype2 ][ param2+1 ][ pos.brotate+1 ][ 3 ]; + else + np2 = handle_schematics.rotation_table[ n.paramtype2 ][ param2+1 ][ pos.brotate+1 ][ 1 ]; + end + +--[[ + local param2list = handle_schematics.get_param2_rotated( n.paramtype2, param2); + local np2 = param2list[ pos.brotate + 1]; + -- mirror + if( mirror_x ) then + if( #param2list==5) then + np2 = handle_schematics.mirror_facedir[ ((pos.brotate+1)%2)+1 ][ np2+1 ]; + elseif( #param2list<5 + and ((pos.brotate%2==1 and (np2==4 or np2==5)) + or (pos.brotate%2==0 and (np2==2 or np2==3)))) then + np2 = param2list[ (pos.brotate + 2)%4 +1]; + end + + elseif( mirror_z ) then + if( #param2list==5) then + np2 = handle_schematics.mirror_facedir[ (pos.brotate %2)+1 ][ np2+1 ]; + elseif( #param2list<5 + and ((pos.brotate%2==0 and (np2==4 or np2==5)) + or (pos.brotate%2==1 and (np2==2 or np2==3)))) then + np2 = param2list[ (pos.brotate + 2)%4 +1]; + end + end +--]] + + param2_data[a:index(ax, ay, az)] = np2; + else + param2_data[a:index(ax, ay, az)] = t[2]; + end + end + end + end + + local ax = pos.x + x; + local az = pos.z + z; + local y_top = pos.y+binfo.yoff+binfo.ysize; + if( y_top+1 > maxp.y ) then + y_top = maxp.y-1; + end + local y_bottom = pos.y+binfo.yoff; + if( y_bottom < minp.y ) then + y_bottom = minp.y; + end + if( has_snow and ax >= minp.x and ax <= maxp.x and az >= minp.z and az <= maxp.z ) then + local res = handle_schematics.mg_drop_moresnow( ax, az, y_top, y_bottom-1, a, data, param2_data); + if( res and (data[ a:index(ax, res.height, az)]==cid.c_air + or data[ a:index(ax, res.height, az)]==cid.c_water )) then + data[ a:index(ax, res.height, az)] = res.suggested.new_id; + param2_data[a:index(ax, res.height, az)] = res.suggested.param2; + has_snow = false; + end + end + end + end + + -- determine suitable positions for the traders + if( handle_schematics.choose_trader_pos and #traders>0) then + extra_calls.traders = handle_schematics.choose_trader_pos(pos, minp, maxp, data, param2_data, a, extranodes, replacements, cid, extra_calls, building_nr_in_bpos, village_id, binfo_extra, road_node, traders); + print('TRADERS CHOOSEN FOR '..tostring( binfo.scm )..': '..minetest.serialize( extra_calls.traders )); + end + +end + + + +-- actually place the buildings (at least those which came as .we files; .mts files are handled later on) +-- this code was also responsible for tree placement; +-- place_buildings is used by mg_villages exclusively. It calls the local function generate_building and +-- therefore resides in this file. +handle_schematics.place_buildings = function(village, minp, maxp, data, param2_data, a, cid, village_id) + -- this function is only relevant for mg_villages + if( not( mg_villages )) then + return; + end + local vx, vz, vs, vh = village.vx, village.vz, village.vs, village.vh + local village_type = village.village_type; + + local bpos = village.to_add_data.bpos; + + local replacements = mg_villages.get_replacement_table( village.village_type, nil, village.to_add_data.replacements ); + + cid.c_chest = handle_schematics.get_content_id_replaced( 'default:chest', replacements ); + cid.c_chest_locked = handle_schematics.get_content_id_replaced( 'default:chest_locked', replacements ); + cid.c_chest_private = handle_schematics.get_content_id_replaced( 'cottages:chest_private', replacements ); + cid.c_chest_work = handle_schematics.get_content_id_replaced( 'cottages:chest_work', replacements ); + cid.c_chest_storage = handle_schematics.get_content_id_replaced( 'cottages:chest_storage', replacements ); + cid.c_chest_shelf = handle_schematics.get_content_id_replaced( 'cottages:shelf', replacements ); + cid.c_chest_ash = handle_schematics.get_content_id_replaced( 'trees:chest_ash', replacements ); + cid.c_chest_aspen = handle_schematics.get_content_id_replaced( 'trees:chest_aspen', replacements ); + cid.c_chest_birch = handle_schematics.get_content_id_replaced( 'trees:chest_birch', replacements ); + cid.c_chest_maple = handle_schematics.get_content_id_replaced( 'trees:chest_maple', replacements ); + cid.c_chest_chestnut = handle_schematics.get_content_id_replaced( 'trees:chest_chestnut', replacements ); + cid.c_chest_pine = handle_schematics.get_content_id_replaced( 'trees:chest_pine', replacements ); + cid.c_chest_spruce = handle_schematics.get_content_id_replaced( 'trees:chest_spruce', replacements ); + cid.c_sign = handle_schematics.get_content_id_replaced( 'default:sign_wall', replacements ); +--print('REPLACEMENTS: '..minetest.serialize( replacements.table )..' CHEST: '..tostring( minetest.get_name_from_content_id( cid.c_chest ))); -- TODO + + local extranodes = {} + local extra_calls = { on_constr = {}, trees = {}, chests = {}, signs = {}, traders = {} }; + + for i, pos in ipairs(bpos) do + -- roads are only placed if there are at least mg_villages.MINIMAL_BUILDUNGS_FOR_ROAD_PLACEMENT buildings in the village + if( not(pos.btype) or pos.btype ~= 'road' or village.anz_buildings > mg_villages.MINIMAL_BUILDUNGS_FOR_ROAD_PLACEMENT )then + -- replacements are in table format for mapgen-based building spawning + generate_building(pos, minp, maxp, data, param2_data, a, extranodes, replacements, cid, extra_calls, i, village_id, nil, mg_villages.road_node ) + end + end + + -- replacements are in list format for minetest.place_schematic(..) type spawning + return { extranodes = extranodes, bpos = bpos, replacements = replacements.list, dirt_roads = village.to_add_data.dirt_roads, + plantlist = village.to_add_data.plantlist, extra_calls = extra_calls }; +end + + + +-- place a schematic manually +-- +-- pos needs to contain information about how to place the building: +-- pos.x, pos.y, pos.z where the building is to be placed +-- pos.btype determines which building will be placed; if not set, binfo_extra needs to be provided +-- pos.brotate contains a value of 0-3, which determines the rotation of the building +-- pos.bsizex size of the building in x direction +-- pos.bsizez size of the building in z direction +-- pos.mirror if set, the building will be mirrored +-- pos.no_plotmarker optional; needs to be set in order to avoid the generation of a plotmarker +-- building_nr optional; used for plotmarker +-- village_id optional; used for plotmarker +-- pos.fruit optional; determines the fruit a farm is going to grow (if binfo.farming_plus is set) + +-- binfo contains general information about a building: +-- binfo.sizex size of the building in x direction +-- binfo.sizez +-- binfo.ysize +-- binfo.yoff how deep is the building burried? +-- binfo.nodenames list of the node names beeing used by the building +-- binfo.scm name of the file containing the schematic; only needed for an error message +-- binfo.scm_data_cache contains actual information about the nodes beeing used (the data) +-- binfo.is_mts optional; if set to 1, the function will abort +-- binfo.farming_plus optional; if set, pos.fruit needs to be set as well +-- binfo.axis optional; relevant for some mirroring operations +-- +-- replacement_list contains replacements in the same list format as place_schematic uses +-- +handle_schematics.place_building_using_voxelmanip = function( pos, binfo, replacement_list) + + if( not( replacement_list ) or type( replacement_list ) ~= 'table' ) then + return; + end + + -- if not defined, the building needs to start at pos.x,pos.y,pos.z - without offset + if( not( binfo.yoff )) then + binfo.yoff = 0; + end + +-- TODO: calculate the end position from the given data + -- get a suitable voxelmanip object + -- (taken from minetest_game/mods/default/trees.lua) + local vm = minetest.get_voxel_manip() + local minp, maxp = vm:read_from_map( + {x = pos.x, y = pos.y, z = pos.z}, + {x = pos.x+pos.bsizex, y = pos.y+binfo.ysize, z = pos.z+pos.bsizez} -- TODO + ) + local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp}) + local data = vm:get_data() + local param2_data = vm:get_param2_data(); + + + -- translate the replacement_list into replacements.ids and replacements.table format + -- the first two parameters are nil because we do not want a new replacement list to be generated + local replacements = handle_schematics.get_replacement_table( nil, nil, replacement_list ); + + -- only very few nodes are actually used from the cid table (content ids) + local cid = {}; + cid.c_air = minetest.get_content_id( 'air' ); + cid.c_dirt = handle_schematics.get_content_id_replaced( 'default:dirt', replacements ); + cid.c_dirt_with_grass = handle_schematics.get_content_id_replaced( 'default:dirt_with_grass',replacements ); + cid.c_sapling = handle_schematics.get_content_id_replaced( 'default:sapling', replacements ); + cid.c_jsapling = handle_schematics.get_content_id_replaced( 'default:junglesapling', replacements ); + cid.c_psapling = handle_schematics.get_content_id_replaced( 'default:pine_sapling', replacements ); + cid.c_savannasapling = handle_schematics.get_content_id_replaced( 'mg:savannasapling', replacements ); + cid.c_pinesapling = handle_schematics.get_content_id_replaced( 'mg:pinesapling', replacements ); + cid.c_plotmarker = handle_schematics.get_content_id_replaced( 'mg_villages:plotmarker', replacements ); + + cid.c_chest = handle_schematics.get_content_id_replaced( 'default:chest', replacements ); + cid.c_chest_locked = handle_schematics.get_content_id_replaced( 'default:chest_locked', replacements ); + cid.c_chest_private = handle_schematics.get_content_id_replaced( 'cottages:chest_private', replacements ); + cid.c_chest_work = handle_schematics.get_content_id_replaced( 'cottages:chest_work', replacements ); + cid.c_chest_storage = handle_schematics.get_content_id_replaced( 'cottages:chest_storage', replacements ); + cid.c_chest_shelf = handle_schematics.get_content_id_replaced( 'cottages:shelf', replacements ); + cid.c_chest_ash = handle_schematics.get_content_id_replaced( 'trees:chest_ash', replacements ); + cid.c_chest_aspen = handle_schematics.get_content_id_replaced( 'trees:chest_aspen', replacements ); + cid.c_chest_birch = handle_schematics.get_content_id_replaced( 'trees:chest_birch', replacements ); + cid.c_chest_maple = handle_schematics.get_content_id_replaced( 'trees:chest_maple', replacements ); + cid.c_chest_chestnut = handle_schematics.get_content_id_replaced( 'trees:chest_chestnut', replacements ); + cid.c_chest_pine = handle_schematics.get_content_id_replaced( 'trees:chest_pine', replacements ); + cid.c_chest_spruce = handle_schematics.get_content_id_replaced( 'trees:chest_spruce', replacements ); + cid.c_sign = handle_schematics.get_content_id_replaced( 'default:sign_wall', replacements ); + + -- for roads + cid.c_sign = handle_schematics.get_content_id_replaced( 'default:gravel', replacements ); + + local extranodes = {} + local extra_calls = { on_constr = {}, trees = {}, chests = {}, signs = {}, traders = {} }; + + generate_building(pos, minp, maxp, data, param2_data, a, extranodes, replacements, cid, extra_calls, pos.building_nr, pos.village_id, binfo, cid.c_gravel); + + -- store the changed map data + vm:set_data(data); + vm:set_param2_data(param2_data); + vm:write_to_map(); + vm:update_liquids(); + vm:update_map(); + +-- TODO: do the calls for the extranodes as well + -- replacements are in list format for minetest.place_schematic(..) type spawning + return { extranodes = extranodes, replacements = replacements.list, extra_calls = extra_calls }; +end + + + +-- places a building read from file "building_name" on the map between start_pos and end_pos using luavoxelmanip +-- returns error message on failure and nil on success +handle_schematics.place_building_from_file = function( start_pos, end_pos, building_name, replacement_list, rotate, axis, mirror, no_plotmarker ) + if( not( building_name )) then + return "No file name given. Cannot find the schematic."; + end + + local binfo = handle_schematics.analyze_file( building_name, nil, nil ); + if( not( binfo )) then + return "Failed to import schematic. Only .mts and .we are supported!"; + end + + -- nodenames and scm_data_cache can be used directly; + -- the size dimensions need to be renamed + binfo.sizex = binfo.size.x; + binfo.sizez = binfo.size.z; + binfo.ysize = binfo.size.y; + + -- this value has already been taken care of when determining start_pos + binfo.yoff = 0; + -- file name of the scm; only used for error messages + binfo.scm = building_name; + -- this is relevant for mirroring operations + binfo.axis = axis; + + + if( not( rotate ) or rotate=="0" ) then + start_pos.brotate = 0; + elseif( rotate=="90" ) then + start_pos.brotate = 1; + elseif( rotate=="180" ) then + start_pos.brotate = 2; + elseif( rotate=="270" ) then + start_pos.brotate = 3; + end + + if( start_pos.brotate > 3 ) then + start_pos.brotate = start_pos.brotate % 4; + end + + + -- determine the size of the bulding from the place we assigned to it... + start_pos.bsizex = math.abs(end_pos.x - start_pos.x)+1; + start_pos.bsizez = math.abs(end_pos.z - start_pos.z)+1; + + -- otpional; if set, the building will be mirrored + start_pos.mirror = mirror; + -- do not generate a plot marker as this is not part of a village; + -- otherwise, building_nr and village_id would have to be provided + start_pos.no_plotmarker = no_plotmarker; + + -- all those calls to on_construct need to be done now + local res = handle_schematics.place_building_using_voxelmanip( start_pos, binfo, replacement_list); + if( not(res) or not( res.extra_calls )) then + return; + end + + -- call on_construct where needed; + -- trees, chests and signs receive no special treatment here + for k, v in pairs( res.extra_calls.on_constr ) do + local node_name = minetest.get_name_from_content_id( k ); + if( minetest.registered_nodes[ node_name ].on_construct ) then + for _, pos in ipairs(v) do + minetest.registered_nodes[ node_name ].on_construct( pos ); + end + end + end + -- TODO: handle metadata (if any is provided) +end + + + +-- add the dirt roads +handle_schematics.place_dirt_roads = function(village, minp, maxp, data, param2_data, a, c_road_node) + local c_air = minetest.get_content_id( 'air' ); + for _, pos in ipairs(village.to_add_data.dirt_roads) do + handle_schematics.place_road( minp, maxp, data, param2_data, a, c_road_node, pos, c_air ); + end +end + +handle_schematics.place_road = function(minp, maxp, data, param2_data, a, c_road_node, pos, c_air ) + local param2 = 0; + if( pos.bsizex > 2 ) then + param2 = 1; + end + if( not(pos.y >= minp.y and pos.y <= maxp.y-2)) then + return; + end + for x = math.max( pos.x, minp.x ), math.min( pos.x+pos.bsizex-1, maxp.x ) do + for z = math.max( pos.z, minp.z ), math.min( pos.z+pos.bsizez-1, maxp.z ) do + -- roads have a height of 1 block + data[ a:index( x, pos.y, z)] = c_road_node; + param2_data[ a:index( x, pos.y, z)] = param2; + -- ...with air above + data[ a:index( x, pos.y+1, z)] = c_air; + data[ a:index( x, pos.y+2, z)] = c_air; + end + end +end + + +if( minetest.get_modpath('moresnow' )) then + handle_schematics.moresnow_installed = true; +end diff --git a/replacements_farming.lua b/replacements_farming.lua new file mode 100644 index 0000000..6cd2015 --- /dev/null +++ b/replacements_farming.lua @@ -0,0 +1,167 @@ + +replacements_group['farming'] = {} + +-- this contains a list of all found/available nodenames that may act as a replacement frming nodes +replacements_group['farming'].found = {}; +-- contains a list of *all* known farming names - even of mods that may not be installed +replacements_group['farming'].all = {}; + +-- contains information about how a particular node is called if a particular farming mod is used; +replacements_group['farming'].data = {}; + +-- names of traders for the diffrent fruits +replacements_group['farming'].traders = {}; + + +replacements_group['farming'].replace_material = function( replacements, old_material, new_material ) + + if( not( old_material ) or not( replacements_group['farming'].data[ old_material ]) + or not( new_material ) or not( replacements_group['farming'].data[ new_material ]) + or old_material == new_material ) then + return replacements; + end + + local old_nodes = replacements_group['farming'].data[ old_material ]; + local new_nodes = replacements_group['farming'].data[ new_material ]; + for i=1,#old_nodes do + local old = old_nodes[i]; + local new = old; + if( i<=#new_nodes and new_nodes[i] and minetest.registered_nodes[ new_nodes[i]] ) then + new = new_nodes[i]; + local found = false; + for i,v in ipairs(replacements) do + if( v and v[1]==old ) then + v[2] = new; + found = true; + end + end + if( not( found )) then + table.insert( replacements, { old, new }); + end + -- default to the last growth stage + elseif( i>#new_nodes and minetest.registered_nodes[ new_nodes[ #new_nodes ]]) then + table.insert( replacements, { old, new_nodes[ #new_nodes ] }); + end + end + return replacements; +end + + +--------------------- +-- internal functions +--------------------- +replacements_group['farming'].add_material = function( fruit, fruit_item, prefix, seperator, postfix ) + + local is_loaded = false; + if( minetest.registered_items[ fruit_item ] + and minetest.registered_nodes[ prefix..fruit..seperator.."1"..postfix ] ) then + is_loaded = true; + table.insert( replacements_group['farming'].found, fruit_item ); + end + table.insert( replacements_group['farming'].all, fruit_item ); + + local data = {}; + -- handle seeds + if( minetest.registered_items[ prefix..fruit..'_seed' ]) then + data[1] = prefix..fruit..'_seed'; + elseif( minetest.registered_items[ prefix..fruit..'seed' ]) then + data[1] = prefix..fruit..'seed'; + else + data[1] = fruit_item; + end + for i=1,8 do + local node_name = prefix..fruit..seperator..tostring(i)..postfix; + if( is_loaded and minetest.registered_nodes[ node_name ]) then + table.insert( data, node_name ); + -- if the mod is not loaded, we do not know how many growth stages it has; + -- in order to be on the safe side, store them all + elseif( not( is_loaded )) then + table.insert( data, node_name ); + end + end + -- the last plant stage (the one that gives the fruit) usually has no number + local node_name = prefix..fruit; + if( is_loaded and minetest.registered_nodes[ node_name ]) then + table.insert( data, node_name ); + elseif( not( is_loaded )) then + table.insert( data, node_name ); + end + replacements_group['farming'].data[ fruit_item ] = data; + + -- farming nodes do not count as ground (except for soil - which is not handled here) + if( mg_villages and mg_villages.node_is_ground ) then + for _,v in ipairs( data ) do + mg_villages.node_is_ground[ v ] = false; + end + end + + if( is_loaded and mobf_trader and mobf_trader.add_trader ) then + + -- TODO: use replacements for the payments where needed + local goods = { + { fruit_item.." 1", "default:coal_lump 3", "default:wood 8"}, + { fruit_item.." 10", "default:steel_ingot 2", "default:chest_locked 1"}}; + if( fruit_item ~= data[1] ) then + table.insert( goods, { data[1].." 1", "farming:scarecrow", "farming:scarecrow_light 1"}); + table.insert( goods, { data[1].." 2", "default:dirt 20", "default:bucket_water", "default:steel_ingot 4", "default:leaves 99" }); + end + table.insert( goods, {"farming:hoe_wood 1","default:wood 10", "default:cobble 10"}); + + mobf_trader.add_trader( mobf_trader.npc_trader_prototype, + "farmer growing "..fruit.."s", -- not always the right grammatical form + fruit.."_farmer_v", + goods, + { "farmer" }, + "" + ); + + replacements_group['farming'].traders[ fruit_item ] = fruit..'_farmer_v'; + end +end + + + + +-- create a list of all available fruit types +replacements_group['farming'].construct_farming_type_list = function() + + -- farming from minetest_game + replacements_group['farming'].add_material( 'wheat', 'farming:wheat', 'farming:', '_', '' ); + replacements_group['farming'].add_material( 'cotton', 'farming:cotton', 'farming:', '_', '' ); + + -- RealTest + replacements_group['farming'].add_material( 'flax', 'farming:string', 'farming:', '_', '' ); + replacements_group['farming'].add_material( 'spelt', 'farming:wheat', 'farming:', '_', '' ); + replacements_group['farming'].add_material( 'soy', 'farming:soy', 'farming:', '_', '' ); + + + -- diffrent versions of farming_plus: + -- PilzAdam: https://forum.minetest.net/viewtopic.php?t=2787 + -- TenPlus1: https://forum.minetest.net/viewtopic.php?t=9019 + -- MTDad: https://forum.minetest.net/viewtopic.php?t=10187 + local fruits = { 'strawberry', 'raspberry', + 'carrot', 'rhubarb', 'cucumber', + 'pumpkin', 'melon', + 'orange', 'lemon', 'peach', 'walnut', + 'potato','potatoe', -- diffrent mods spell them diffrently + 'tomato', 'corn' + }; + for i,fruit in ipairs( fruits ) do + if( minetest.registered_nodes[ 'farming_plus:'..fruit ] + and minetest.registered_nodes[ 'farming_plus:'..fruit..'_1' ] + and minetest.registered_items[ 'farming_plus:'..fruit..'_item' ] ) then + replacements_group['farming'].add_material( fruit, 'farming_plus:'..fruit..'_item', 'farming_plus:', '_', '' ); + end + end + -- coffee beans from farming_plus/farming_plusplus + replacements_group['farming'].add_material( 'coffee', 'farming_plus:coffee_beans', 'farming_plus:', '_', '' ); + + -- Docfarming: https://forum.minetest.net/viewtopic.php?t=3948 + fruits = {'carrot','corn','potato','raspberry'}; + for i,fruit in ipairs( fruits ) do + replacements_group['farming'].add_material( fruit, 'docfarming:'..fruit, 'docfarming:', '', '' ); + end +end + +-- create the list of known farming fruits +replacements_group['farming'].construct_farming_type_list(); diff --git a/replacements_get_table.lua b/replacements_get_table.lua new file mode 100644 index 0000000..c07e81a --- /dev/null +++ b/replacements_get_table.lua @@ -0,0 +1,21 @@ +-- mapgen based replacements work best using a table, while minetest.place_schematic(..) based spawning needs a list +handle_schematics.get_replacement_table = function( housetype, pr, replacements ) + + local rtable = {}; + local ids = {}; + if( not( replacements ) and mg_villages and mg_villages.get_replacement_list) then + replacements = mg_villages.get_replacement_list( housetype, pr ); + end + -- it is very problematic if the torches on houses melt snow and cause flooding; thus, we use a torch that is not hot + if( minetest.registered_nodes[ 'mg_villages:torch']) then + table.insert( replacements, {'default:torch', 'mg_villages:torch'}); + end + for i,v in ipairs( replacements ) do + if( v and #v == 2 ) then + rtable[ v[1] ] = v[2]; + ids[ minetest.get_content_id( v[1] )] = minetest.get_content_id( v[2] ); + end + end + return { table = rtable, list = replacements, ids = ids }; +end + diff --git a/replacements_realtest.lua b/replacements_realtest.lua new file mode 100644 index 0000000..7ba3261 --- /dev/null +++ b/replacements_realtest.lua @@ -0,0 +1,82 @@ +replacements_group['realtest'] = {} + +-- parameter: replacements, name_in_default, name_in_realtest, to_realtest=true/false +replacements_group['realtest'].stairs = function( repl, def, rt, to_realtest) + if( to_realtest ) then + if( def ~= rt ) then + table.insert( repl, {'default:'..def, 'default:'..rt}); + end + table.insert( repl, {'stairs:stair_'..def, 'default:'..rt..'_stair'}); + table.insert( repl, {'stairs:slab_'..def, 'default:'..rt..'_slab'}); + else + if( def ~= rt ) then + table.insert( repl, {'default:'..rt, 'default:'..def}); + end + table.insert( repl, {'default:'..rt..'_stair', 'stairs:stair_'..def}); + table.insert( repl, {'default:'..rt..'_stair_upside_down','stairs:stair_'..def}); + -- upside-down-slab + table.insert( repl, {'default:'..rt..'_slab_r', 'stairs:slab_'..def}); + table.insert( repl, {'default:'..rt..'_slab', 'stairs:slab_'..def}); + end + return repl; +end + +replacements_group['realtest'].replace = function( replacements ) + + local repl = {}; + local to_realtest = false; + if( not( minetest.registered_nodes[ 'default:furnace' ]) + and minetest.registered_nodes[ 'oven:oven' ]) then + to_realtest = true; + elseif( minetest.registered_nodes[ 'default:furnace' ] + and not( minetest.registered_nodes[ 'oven:oven' ])) then + to_realtest = false; + else + -- else no replacements required + return; + end + + replacements_group['realtest'].stairs( repl, 'stone', 'stone', to_realtest ); + replacements_group['realtest'].stairs( repl, 'cobble', 'stone_flat', to_realtest ); + replacements_group['realtest'].stairs( repl, 'stonebrick', 'stone_bricks', to_realtest ); + replacements_group['realtest'].stairs( repl, 'desert_stone', 'desert_stone', to_realtest ); + replacements_group['realtest'].stairs( repl, 'desert_cobble', 'desert_stone_flat', to_realtest ); + replacements_group['realtest'].stairs( repl, 'desert_stonebrick', 'desert_stone_bricks',to_realtest ); + replacements_group['realtest'].stairs( repl, 'brick', 'brick', to_realtest ); + + if( to_realtest ) then + table.insert( repl, {'default:furnace', 'oven:oven'}); + table.insert( repl, {'default:clay', 'grounds:clay'}); + -- Realtest does not know about these nodes yet + table.insert( repl, {'farming:soil_wet', 'farming:soil'}); + table.insert( repl, {'farming:desert_sand_soil', 'farming:soil'}); + table.insert( repl, {'farming:desert_sand_soil_wet','farming:soil'}); + for i=1,5 do + table.insert( repl, {'default:grass_'..i,'air' }); + end + table.insert( repl, {'default:apple', 'air' }); + table.insert( repl, {'default:obsidian_glass', 'default:glass' }); + else + table.insert( repl, {'oven:oven', 'default:furnace'}); + table.insert( repl, {'grounds:clay', 'default:clay'}); + table.insert( repl, {'farming:soil', 'farming:soil_wet'}); + end + + + for i,v in ipairs( repl ) do + if( v and v[2] and minetest.registered_nodes[ v[2]] ) then + local found = false; + for j,w in ipairs( replacements ) do + if( w and w[1] and w[1]==v[1] ) then + w[2] = v[2]; + found = true; + end + end + if( not( found )) then + table.insert( replacements, {v[1],v[2]} ); + end + end + end + return replacements; +end + diff --git a/replacements_roof.lua b/replacements_roof.lua new file mode 100644 index 0000000..44e3215 --- /dev/null +++ b/replacements_roof.lua @@ -0,0 +1,110 @@ + +replacements_group['roof'] = {} + +-- this contains a list of all found/available nodenames that may act as a replacement frming nodes +replacements_group['roof'].found = {}; +-- contains a list of *all* known roof names - even of mods that may not be installed +replacements_group['roof'].all = {}; + +-- contains information about how a particular node is called if a particular roof mod is used; +replacements_group['roof'].data = {}; + + +replacements_group['roof'].replace_material = function( replacements, old_material, new_material ) + + if( not( old_material ) or not( replacements_group['roof'].data[ old_material ]) + or not( new_material ) or not( replacements_group['roof'].data[ new_material ]) + or old_material == new_material ) then + return replacements; + end + + local old_nodes = replacements_group['roof'].data[ old_material ]; + local new_nodes = replacements_group['roof'].data[ new_material ]; + for i=1,#old_nodes do + local old = old_nodes[i]; + local new = old; + if( i<=#new_nodes and new_nodes[i] and minetest.registered_nodes[ new_nodes[i]] ) then + new = new_nodes[i]; + local found = false; + for i,v in ipairs(replacements) do + if( v and v[1]==old ) then + v[2] = new; + found = true; + end + end + if( not( found )) then + table.insert( replacements, { old, new }); + end + end + end + return replacements; +end + + +--------------------- +-- internal functions +--------------------- +replacements_group['roof'].add_material = function( nodelist ) + + local is_loaded = false; + if( minetest.registered_items[ nodelist[1] ] ) then + is_loaded = true; + table.insert( replacements_group['roof'].found, nodelist[1] ); + end + table.insert( replacements_group['roof'].all, nodelist[1]); + + replacements_group['roof'].data[ nodelist[1] ] = nodelist; +end + + + + +-- create a list of all available fruit types +replacements_group['roof'].construct_roof_type_list = function() + + -- roof from cottages + local roofs = {'straw', 'reet', 'wood', 'slate', 'red', 'brown', 'black'}; + for i,v in ipairs( roofs ) do + replacements_group['roof'].add_material( { + 'cottages:roof_connector_'..v, + 'cottages:roof_flat_'..v, + '', -- no full block available + 'cottages:roof_'..v + } ); + end + + + -- from dryplants + roofs = {'reed', 'wetreed'}; + for i,v in ipairs( roofs ) do + replacements_group['roof'].add_material( { + 'dryplants:'..v..'_roof', + 'dryplants:'..v..'_slab', + 'dryplants:'..v, + 'dryplants:'..v..'_roof', + 'dryplants:'..v..'_roof_corner', + 'dryplants:'..v..'_roof_corner_2' + } ); + end + -- roof from homedecor + roofs = {'wood', 'terracotta', 'asphalt', 'glass'}; + for i,v in ipairs( roofs ) do + replacements_group['roof'].add_material( { + 'homedecor:shingle_side_'..v, + 'homedecor:shingles_'..v, + '', + 'homedecor:shingles_'..v, + 'homedecor:shingle_inner_corner_'..v, + 'homedecor:shingle_outer_corner_'..v, + } ); + end + + replacements_group['roof'].data[ 'homedecor:shingle_side_glass' ][2] = 'homedecor:skylight'; + replacements_group['roof'].data[ 'homedecor:shingle_side_glass' ][4] = 'homedecor:skylight'; + replacements_group['roof'].data[ 'homedecor:shingle_side_asphalt'][3] = 'streets:asphalt'; + + -- TODO: slopes from technic or other slopes mods? +end + +-- create the list of known roof fruits +replacements_group['roof'].construct_roof_type_list(); diff --git a/replacements_wood.lua b/replacements_wood.lua new file mode 100644 index 0000000..c813590 --- /dev/null +++ b/replacements_wood.lua @@ -0,0 +1,233 @@ +replacements_group['wood'] = {} + +-- this contains a list of all found/available nodenames that may act as a replacement for default:wood +replacements_group['wood'].found = {}; +-- contains a list of *all* known wood names - even of mods that may not be installed +replacements_group['wood'].all = {}; + +-- contains information about how a particular node is called if a particular wood is used; +replacements_group['wood'].data = {}; + +-- names of traders for the diffrent wood types +replacements_group['wood'].traders = {}; + + +------------------------------------------------------------------------------ +-- external function; call it in order to replace old_wood with new_wood; +-- other nodes (trees, saplings, fences, doors, ...) are replaced accordingly, +-- depending on what new_wood has to offer +------------------------------------------------------------------------------ +replacements_group['wood'].replace_material = function( replacements, old_wood, new_wood ) + + if( not( old_wood ) or not( replacements_group['wood'].data[ old_wood ]) + or not( new_wood ) or not( replacements_group['wood'].data[ new_wood ]) + or old_wood == new_wood ) then + return replacements; + end + + local old_nodes = replacements_group['wood'].data[ old_wood ]; + local new_nodes = replacements_group['wood'].data[ new_wood ]; + for i=3,#old_nodes do + local old = old_nodes[i]; + local new = old; + if( i<=#new_nodes and new_nodes[i] and minetest.registered_nodes[ new_nodes[i]] ) then + new = new_nodes[i]; + local found = false; + for i,v in ipairs(replacements) do + if( v and v[1]==old ) then + v[2] = new; + found = true; + end + end + if( not( found )) then + table.insert( replacements, { old, new }); + end + end + end + return replacements; +end + + +--------------------- +-- internal functions +--------------------- +-- wood (and its corresponding tree trunk) is a very good candidate for replacement in most houses +-- helper function for replacements_group['wood'].get_wood_type_list +replacements_group['wood'].add_material = function( candidate_list, mod_prefix, w_pre, w_post, t_pre, t_post, l_pre, l_post, + s_pre, s_post, stair_pre, stair_post, slab_pre, slab_post, + fence_pre, fence_post, gate_pre, gate_post ) + if( not( candidate_list )) then + return; + end + for _,v in ipairs( candidate_list ) do + local is_loaded = false; + local wood_name = mod_prefix..w_pre..v..w_post; + -- create a complete list of all possible wood names + table.insert( replacements_group['wood'].all, wood_name ); + -- create a list of all *installed* wood types + if( minetest.registered_nodes[ wood_name ]) then + table.insert( replacements_group['wood'].found, wood_name ); + is_loaded = true; + end + + -- there is no check if the node names created here actually exist + local data = { v, -- 1. base name of the node + mod_prefix, -- 2. mod name + wood_name, -- 3. replacement for default:wood + mod_prefix..t_pre..v..t_post, -- 4. " " for default:tree + mod_prefix..l_pre..v..l_post, -- 5. " " for default:leaves + mod_prefix..s_pre..v..s_post, -- 6. " " for default:sapling + stair_pre..v..stair_post, -- 7. " " for stairs:stair_wood + slab_pre..v..slab_post, -- 8. " " for stairs:slab_wood + fence_pre..v..fence_post, -- 9. " " for default:fence_wood + gate_pre..v..gate_post..'_open', -- 10. " " for cottages:gate_open + gate_pre..v..gate_post..'_closed',-- 11. " " for cottages:gate_closed + }; + + -- normal wood does have a number of nodes which might get replaced by more specialized wood types + if( mod_prefix=='default:' and v=='' ) then + local w = 'wood'; + data[10] = 'cottages:gate_open'; + data[11] = 'cottages:gate_closed'; + data[12] = 'default:ladder'; + data[13] = 'doors:door_'..w..'_t_1'; + data[14] = 'doors:door_'..w..'_t_2'; + data[15] = 'doors:door_'..w..'_b_1'; + data[16] = 'doors:door_'..w..'_b_2'; + data[17] = 'default:bookshelf'; + data[18] = 'default:chest'; + data[19] = 'default:chest_locked'; + data[20] = 'stairs:stair_'..w..'upside_down'; + data[21] = 'stairs:slab_'..w..'upside_down'; + data[22] = 'doors:trapdoor_open'; + data[23] = 'doors:trapdoor'; + -- realtest has some further replacements + elseif( mod_prefix=='trees:' and w_post=='_planks' and t_post=='_log' ) then + data[12] = 'trees:'..v..'_ladder'; + data[13] = 'doors:door_'..v..'_t_1'; + data[14] = 'doors:door_'..v..'_t_2'; + data[15] = 'doors:door_'..v..'_b_1'; + data[16] = 'doors:door_'..v..'_b_2'; + data[17] = 'decorations:bookshelf_'..v; + data[18] = 'trees:'..v..'_chest'; + data[19] = 'trees:'..v..'_chest_locked'; + data[20] = 'trees:'..v..'_planks_stair_upside_down'; + data[21] = 'trees:'..v..'_planks_slab_upside_down'; + data[22] = 'hatches:'..v..'_hatch_opened_top'; + data[23] = 'hatches:'..v..'_hatch_opened_bottom'; + end + replacements_group['wood'].data[ wood_name ] = data; + + -- none of the wood nodes counts as ground + if( mg_villages and mg_villages.node_is_ground ) then + for _,v in ipairs( data ) do + mg_villages.node_is_ground[ v ] = false; + end + end + + + if( is_loaded and mobf_trader and mobf_trader.add_trader ) then + -- TODO: check if all offered payments exist + local goods = { + { data[3].." 4", "default:dirt 24", "default:cobble 24"}, + { data[4].." 4", "default:apple 2", "default:coal_lump 4"}, + { data[4].." 8", "default:pick_stone 1", "default:axe_stone 1"}, + { data[4].." 12", "default:cobble 80", "default:steel_ingot 1"}, + { data[4].." 36", "bucket:bucket_empty 1", "bucket:bucket_water 1"}, + { data[4].." 42", "default:axe_steel 1", "default:mese_crystal 4"}, + + { data[6].." 1", "default:mese 10", "default:steel_ingot 48"}, + -- leaves are a cheaper way of getting saplings + { data[5].." 10", "default:cobble 1", "default:dirt 2"} + }; + + mobf_trader.add_trader( mobf_trader.npc_trader_prototype, + "Trader of "..( v or "unknown" ).." wood", + v.."_wood_v", + goods, + { "lumberjack" }, + "" + ); + + replacements_group['wood'].traders[ wood_name ] = v..'_wood_v'; + end + end +end + +-- TODO: there are also upside-down variants sometimes +-- TODO: moreblocks - those may be installed and offer further replacements + +-- create a list of all available wood types +replacements_group['wood'].construct_wood_type_list = function() + + -- https://github.com/minetest/minetest_game + -- default tree and jungletree; no gates available + replacements_group['wood'].add_material( {'', 'jungle' }, 'default:', '','wood','', 'tree', '','leaves', '','sapling', + 'stairs:stair_', 'wood', 'stairs:slab_', 'wood', 'default:fence_','wood', 'NONE', '' ); + -- default:pine_needles instead of leaves; no gates available + replacements_group['wood'].add_material( {'pine' }, 'default:', '','wood','', 'tree', '','_needles','','_sapling', + 'stairs:stair_', 'wood', 'stairs:slab_', 'wood', 'default:fence_','wood', 'NONE','' ); + + -- https://github.com/Novatux/mg + -- trees from nores mapgen + replacements_group['wood'].add_material( {'savanna', 'pine' },'mg:', '','wood','', 'tree', '','leaves', '','sapling', + 'stairs:stair_','wood', 'stairs:slab_','wood', 'NONE','', 'NONE',''); + + + -- https://github.com/VanessaE/moretrees + -- minus the jungletree (already in default) + local moretrees_treelist = {"beech","apple_tree","oak","sequoia","birch","palm","spruce","pine","willow","acacia","rubber_tree","fir" }; + replacements_group['wood'].add_material( moretrees_treelist, 'moretrees:', '', '_planks', '','_trunk', '','_leaves','','_sapling', + 'moretrees:stair_','_planks', 'moretrees:slab_','_planks', 'NONE','', 'NONE',''); + + + -- https://github.com/tenplus1/ethereal + -- ethereal does not have a common naming convention for leaves + replacements_group['wood'].add_material( {'acacia','redwood'},'ethereal:', '','_wood', '','_trunk', '','_leaves', '','_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'ethereal:fence_','', 'ethereal:','gate'); + -- frost has another sapling type... + replacements_group['wood'].add_material( {'frost'}, 'ethereal:', '','_wood', '','_tree', '','_leaves', '','_tree_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'ethereal:fence_','wood', 'ethereal:','woodgate' ); + -- those tree types do not use typ_leaves, but typleaves instead... + replacements_group['wood'].add_material( {'yellow'}, 'ethereal:', '','_wood', '','_trunk', '','leaves', '','_tree_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'ethereal:fence_','wood', 'ethereal:','gate' ); + -- banana has a diffrent fence type.... + replacements_group['wood'].add_material( {'banana'}, 'ethereal:', '','_wood', '','_trunk', '','leaves', '','_tree_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'ethereal:fence_', '', 'ethereal:','gate' ); + -- palm has another name for the sapling again... + replacements_group['wood'].add_material( {'palm'}, 'ethereal:', '','_wood', '','_trunk', '','leaves', '','_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'ethereal:fence_', '', 'ethereal:','gate' ); + -- the leaves are called willow_twig here... + replacements_group['wood'].add_material( {'willow'}, 'ethereal:', '','_wood', '','_trunk', '','_twig', '','_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'ethereal:fence_', '', 'ethereal:','gate' ); + -- mushroom has its own name; it works quite well as a wood replacement; the red cap is used as leaves + -- the stairs are also called slightly diffrently (end in _trunk instead of _wood) + replacements_group['wood'].add_material( {'mushroom'}, 'ethereal:', '','_pore', '','_trunk', '','', '','_sapling', + 'stairs:stair_','_trunk', 'stairs:slab_','_trunk', 'ethereal:fence_', '', 'ethereal:','gate' ); + + + -- https://github.com/VanessaE/realtest_game + local realtest_trees = {'ash','aspen','birch','maple','chestnut','pine','spruce'}; + replacements_group['wood'].add_material( realtest_trees, 'trees:', '','_planks', '','_log', '','_leaves', '','_sapling', + 'trees:','_planks_stair', 'trees:','_planks_slab', 'fences:','_fence', 'NONE','' ); + + + -- https://github.com/Gael-de-Sailly/Forest + local forest_trees = {'oak','birch','willow','fir','mirabelle','cherry','plum','beech','ginkgo','lavender'}; + replacements_group['wood'].add_material( forest_trees, 'forest:', '', '_wood', '','_tree', '','_leaves', '','_sapling', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'NONE','', 'NONE','' ); + + -- https://github.com/bas080/trees + replacements_group['wood'].add_material( {'mangrove','palm','conifer'},'trees:', 'wood_','', 'tree_','', 'leaves_','', 'sapling_','', + 'stairs:stair_','_wood', 'stairs:slab_','_wood', 'NONE','', 'NONE','' ); + + + -- https://github.com/PilzAdam/farming_plus + -- TODO: this does not come with its own wood... banana and cocoa trees (only leaves, sapling and fruit) + -- TODO: farming_plus:TREETYP_sapling farming_plus:TREETYP_leaves farming_plus:TREETYP + -- TODO: in general: add fruits as replacements for apples +end + +-- actually construct the data structure once +replacements_group['wood'].construct_wood_type_list(); + diff --git a/rotate.lua b/rotate.lua new file mode 100644 index 0000000..91d6abd --- /dev/null +++ b/rotate.lua @@ -0,0 +1,114 @@ +local rotate_facedir = function(facedir) + return ({1, 2, 3, 0, + 13, 14, 15, 12, + 17, 18, 19, 16, + 9, 10, 11, 8, + 5, 6, 7, 4, + 21, 22, 23, 20})[facedir+1] +end + + +-- accessd through handle_schematics.mirror_facedir[ (rotation%2)+1 ][ facedir+1 ] +handle_schematics.mirror_facedir = + {{ 2, 1, 0, 3, -- 0, 1, 2, 3 + 8, 9, 10, 11, -- 4, 5, 6, 7 + 4, 5, 6, 7, -- 8, 9,10,11 + 12, 13, 14, 15, --12,13,14,15 + 16, 17, 18, 19, --16,17,18,19 + 22, 21, 20, 23 --20,21,22,23 + }, + { 0, 3, 2, 1, -- 0, 1, 2, 3 + 4, 7, 6, 5, -- 4, 5, 6, 7 + 8, 9, 10, 11, -- 8, 9,10,11 + 16, 17, 18, 19, --12,13,14,15 + 12, 15, 14, 13, --16,17,18,19 + 20, 23, 22, 21 --20,21,22,23 + }}; + +local rotate_wallmounted = function(wallmounted) + return ({0, 1, 5, 4, 2, 3})[wallmounted+1] +end + +handle_schematics.get_param2_rotated = function( paramtype2, p2 ) + local p2r = {}; + p2r[ 1 ] = p2; + if( paramtype2 == 'wallmounted' ) then + for i = 2,4 do + p2r[ i ] = rotate_wallmounted( p2r[ i-1 ]); + end + elseif( paramtype2 == 'facedir' ) then + for i = 2,4 do + p2r[ i ] = rotate_facedir( p2r[ i-1 ]); + end + p2r[5]=1; -- indicate that it is wallmounted + else + return { p2, p2, p2, p2 }; + end + return p2r; +end + + +handle_schematics.mirrored_node = {}; + +handle_schematics.add_mirrored_node_type = function( name, mirrored_name ) + handle_schematics.mirrored_node[ name ] = mirrored_name; + local id = minetest.get_content_id( name ); + local id_mi = minetest.get_content_id( mirrored_name ); + local c_ignore = minetest.get_content_id( 'ignore' ); + if( id and id_mi and id ~= c_ignore and id_mi ~= c_ignore ) then + handle_schematics.mirrored_node[ id ] = id_mi; + end +end + +local door_materials = {'wood','steel','glass','obsidian_glass'}; +for _,material in ipairs( door_materials ) do + handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_b_1', 'doors:door_'..material..'_b_2' ); + handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_t_1', 'doors:door_'..material..'_t_2' ); + handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_b_2', 'doors:door_'..material..'_b_1' ); + handle_schematics.add_mirrored_node_type( 'doors:door_'..material..'_t_2', 'doors:door_'..material..'_t_1' ); +end + + + + +handle_schematics.rotation_table = {}; +handle_schematics.rotation_table[ 'facedir' ] = {}; +handle_schematics.rotation_table[ 'wallmounted' ] = {}; + + +for paramtype2,v in pairs( handle_schematics.rotation_table ) do + for param2 = 0,23 do + + if( param2 < 6 or paramtype2 == 'facedir' ) then + local param2list = handle_schematics.get_param2_rotated( paramtype2, param2); + + handle_schematics.rotation_table[ paramtype2 ][ param2+1 ] = {}; + + for rotation = 0,3 do + local np2 = param2list[ rotation + 1]; + local mirror_x = np2; + local mirror_z = np2; + + -- mirror_x + if( #param2list==5) then + mirror_x = handle_schematics.mirror_facedir[ (( rotation +1)%2)+1 ][ np2+1 ]; + elseif( #param2list<5 + and (( rotation%2==1 and (np2==4 or np2==5)) + or ( rotation%2==0 and (np2==2 or np2==3)))) then + mirror_x = param2list[ ( rotation + 2)%4 +1]; + end + + -- mirror_z + if( #param2list==5) then + mirror_z = handle_schematics.mirror_facedir[ (rotation %2)+1 ][ np2+1 ]; + elseif( #param2list<5 + and (( rotation%2==0 and (np2==4 or np2==5)) + or ( rotation%2==1 and (np2==2 or np2==3)))) then + mirror_z = param2list[ ( rotation + 2)%4 +1]; + end + + handle_schematics.rotation_table[ paramtype2 ][ param2+1 ][ rotation+1 ] = { np2, mirror_x, mirror_z }; + end + end + end +end diff --git a/village_traders.lua b/village_traders.lua new file mode 100644 index 0000000..8935c76 --- /dev/null +++ b/village_traders.lua @@ -0,0 +1,146 @@ + + +handle_schematics.choose_traders = function( village_type, building_type, replacements ) + + if( not( building_type ) or not( village_type )) then + return; + end + + -- some jobs are obvious + if( building_type == 'mill' ) then + return { 'miller' }; + elseif( building_type == 'bakery' ) then + return { 'baker' }; + elseif( building_type == 'school' ) then + return { 'teacher' }; + elseif( building_type == 'forge' ) then + local traders = {'blacksmith', 'bronzesmith' }; + return { traders[ math.random(#traders)] }; + elseif( building_type == 'shop' ) then + local traders = {'seeds','flowers','misc','default','ore', 'fruit trader', 'wood'}; + return { traders[ math.random(#traders)] }; + -- there are no traders for these jobs - they'd require specialized mobs + elseif( building_type == 'tower' + or building_type == 'church' + or building_type == 'secular' + or building_type == 'tavern' ) then + return {}; + end + + if( village_type == 'charachoal' ) then + return { 'charachoal' }; + elseif( village_type == 'claytrader' ) then + return { 'clay' }; + end + + local res = {}; + if( building_type == 'shed' + or building_type == 'farm_tiny' + or building_type == 'house' + or building_type == 'house_large' + or building_type=='hut') then + local traders = { 'stonemason', 'stoneminer', 'carpenter', 'toolmaker', + 'doormaker', 'furnituremaker', 'stairmaker', 'cooper', 'wheelwright', + 'saddler', 'roofer', 'iceman', 'potterer', 'bricklayer', 'dyemaker', + 'dyemakerl', 'glassmaker' } + -- sheds and farms both contain craftmen + res = { traders[ math.random( #traders )] }; + if( building_type == 'shed' + or building_type == 'house' + or building_type == 'house_large' + or building_type == 'hut' ) then + return res; + end + end + + if( building_type == 'field' + or building_type == 'farm_full' + or building_type == 'farm_tiny' ) then + + local fruit = 'farming:cotton'; + if( 'farm_full' ) then + -- RealTest + fruit = 'farming:wheat'; + if( replacements_group['farming'].traders[ 'farming:soy']) then + fruit_item = 'farming:soy'; + end + if( minetest.get_modpath("mobf") ) then + local animal_trader = {'animal_cow', 'animal_sheep', 'animal_chicken', 'animal_exotic'}; + res[1] = animal_trader[ math.random( #animal_trader )]; + end + return { res[1], replacements_group['farming'].traders[ fruit_item ]}; + elseif( #replacements_group['farming'].found > 0 ) then + -- get a random fruit to grow + fruit = replacements_group['farming'].found[ math.random( #replacements_group['farming'].found) ]; + return { res[1], replacements_group['farming'].traders[ fruit_item ]}; + else + return res; + end + end + + if( building_type == 'pasture' and minetest.get_modpath("mobf")) then + local animal_trader = {'animal_cow', 'animal_sheep', 'animal_chicken', 'animal_exotic'}; + return { animal_trader[ math.random( #animal_trader )] }; + end + + + -- TODO: banana,cocoa,rubber from farming_plus? + -- TODO: sawmill + if( building_type == 'lumberjack' or village_type == 'lumberjack' ) then + -- TODO: limit this to single houses + if( replacements.table and replacements.table[ 'default:wood' ] ) then + return { replacements_group['wood'].traders[ replacements.table[ 'default:wood' ]] }; + elseif( #replacements_group['wood'].traders > 0 ) then + return { replacements_group['wood'].traders[ math.random( #replacements_group['wood'].traders) ]}; + else + return { 'common_wood'}; + end + end + + + -- tent, chateau: places for living at; no special jobs associated + -- nore,taoki,medieval,lumberjack,logcabin,canadian,grasshut,tent: further village types + + return res; +end + + +handle_schematics.choose_trader_pos = function(pos, minp, maxp, data, param2_data, a, extranodes, replacements, cid, extra_calls, building_nr_in_bpos, village_id, binfo_extra, road_node, traders) + + local trader_pos = {}; + -- determine spawn positions for the mobs + for i,tr in ipairs( traders ) do + local tries = 0; + local found = false; + local pt = {x=pos.x, y=pos.y, z=pos.z}; + while( tries < 10 and not(found)) do + -- get a random position for the trader + pt.x = pos.x+math.random(pos.bsizex); + pt.z = pos.z+math.random(pos.bsizez); + -- check if it is inside the area contained in data + if (pt.x >= minp.x and pt.x <= maxp.x) and (pt.y >= minp.y and pt.y <= maxp.y) and (pt.z >= minp.z and pt.z <= maxp.z) then + + while( pt.y < maxp.y + and (data[ a:index( pt.x, pt.y, pt.z)]~=cid.c_air + or data[ a:index( pt.x, pt.y+1, pt.z)]~=cid.c_air )) do + pt.y = pt.y + 1; + end + + -- TODO: check if this position is really suitable? traders standing on the roof are a bit odd + found = true; + end + tries = tries+1; + + -- check if this position has already been assigned to another trader + for j,t in ipairs( trader_pos ) do + if( t.x==pt.x and t.y==pt.y and t.z==pt.z ) then + found = false; + end + end + end + if( found ) then + table.insert( trader_pos, {x=pt.x, y=pt.y, z=pt.z, typ=tr, bpos_i = building_nr_in_bpos} ); + end + end + return trader_pos; +end diff --git a/worldedit_file.lua b/worldedit_file.lua new file mode 100644 index 0000000..9f421dd --- /dev/null +++ b/worldedit_file.lua @@ -0,0 +1,138 @@ +------------------------------------------------------------------------------------------ +-- This is the file +-- https://github.com/Uberi/Minetest-WorldEdit/blob/master/worldedit/serialization.lua +-- Changes: +-- * worldedit namespace renamed to worldeit_file +-- * eliminiated functions that are not needed +-- * made function load_schematic non-local +-- * originx, originy and originz are now passed as parameters to worldedit_file.load_schematic; +-- they are required for an old file format +------------------------------------------------------------------------------------------ + +worldedit_file = {} -- add the namespace + +--- Schematic serialization and deserialiation. +-- @module worldedit.serialization + +worldedit_file.LATEST_SERIALIZATION_VERSION = 5 +local LATEST_SERIALIZATION_HEADER = worldedit_file.LATEST_SERIALIZATION_VERSION .. ":" + + +--[[ +Serialization version history: + 1: Original format. Serialized Lua table with a weird linked format... + 2: Position and node seperated into sub-tables in fields `1` and `2`. + 3: List of nodes, one per line, with fields seperated by spaces. + Format: + 4: Serialized Lua table containing a list of nodes with `x`, `y`, `z`, + `name`, `param1`, `param2`, and `meta` fields. + 5: Added header and made `param1`, `param2`, and `meta` fields optional. + Header format: ,,...: +--]] + + +--- Reads the header of serialized data. +-- @param value Serialized WorldEdit data. +-- @return The version as a positive natural number, or 0 for unknown versions. +-- @return Extra header fields as a list of strings, or nil if not supported. +-- @return Content (data after header). +function worldedit_file.read_header(value) + if value:find("^[0-9]+[%-:]") then + local header_end = value:find(":", 1, true) + local header = value:sub(1, header_end - 1):split(",") + local version = tonumber(header[1]) + table.remove(header, 1) + local content = value:sub(header_end + 1) + return version, header, content + end + -- Old versions that didn't include a header with a version number + if value:find("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)") and not value:find("%{") then -- List format + return 3, nil, value + elseif value:find("^[^\"']+%{%d+%}") then + if value:find("%[\"meta\"%]") then -- Meta flat table format + return 2, nil, value + end + return 1, nil, value -- Flat table format + elseif value:find("%{") then -- Raw nested table format + return 4, nil, value + end + return nil +end + + +--- Loads the schematic in `value` into a node list in the latest format. +-- Contains code based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile) +-- by ChillCode, available under the MIT license. +-- @return A node list in the latest format, or nil on failure. +function worldedit_file.load_schematic(value, we_origin) + local version, header, content = worldedit_file.read_header(value) + local nodes = {} + if version == 1 or version == 2 then -- Original flat table format + local tables = minetest.deserialize(content) + if not tables then return nil end + + -- Transform the node table into an array of nodes + for i = 1, #tables do + for j, v in pairs(tables[i]) do + if type(v) == "table" then + tables[i][j] = tables[v[1]] + end + end + end + nodes = tables[1] + + if version == 1 then --original flat table format + for i, entry in ipairs(nodes) do + local pos = entry[1] + entry.x, entry.y, entry.z = pos.x, pos.y, pos.z + entry[1] = nil + local node = entry[2] + entry.name, entry.param1, entry.param2 = node.name, node.param1, node.param2 + entry[2] = nil + end + end + elseif version == 3 or version=="3" then -- List format + if( not( we_origin ) or #we_origin <3) then + we_origin = { 0, 0, 0 }; + end + 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), + 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