local handle_schematics = {} -- node name used to indicate where the building will eventually be placed handle_schematics.SCAFFOLDING = 'random_buildings:support'; handle_schematics.AUTODECAY = 'apartment:autodecay'; handle_schematics.ENABLE_SLOW_DECAY = false -- 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', "r") if (file == nil) then 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 file.close(file) 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 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 }; end -- depending on the orientation (param2) of the build chest, the start position of the building may have to be moved; -- 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 ) 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 > 0 ) 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 -- call on_construct and after_place_node for nodes that require it; -- set up steel doors in a usable way; -- set up apartments from the apartment mod; -- placer is the player who initialized the placement of the schematic (placer will be passed on to after_place_node etc) handle_schematics.update_nodes = function( start_pos, end_pos, on_constr, after_place_node, placer, extra_params ) local p={}; local i=0; local v=0; -- call on_construct for all the nodes that require it for i, v in ipairs( on_constr ) do -- there are only very few nodes which need this special treatment local nodes = minetest.find_nodes_in_area( start_pos, end_pos, v); for _, p in ipairs( nodes ) do minetest.registered_nodes[ v ].on_construct( p ); end end if( placer ) then for i, v in ipairs( after_place_node ) do -- there are only very few nodes which need this special treatment local nodes = minetest.find_nodes_in_area( start_pos, end_pos, v); for _, p in ipairs( nodes ) do minetest.registered_nodes[ v ].after_place_node( p, placer, nil, nil ); end end local player_name = placer:get_player_name(); -- steel doors are annoying because the cannot be catched with the functions above local doornodes = minetest.find_nodes_in_area( start_pos, end_pos, {'doors:door_steel_b_1','doors:door_steel_b_2', 'doors:door_steel_t_1','doors:door_steel_t_2'}); for _, p in ipairs( doornodes ) do local meta = minetest.get_meta( p ); meta:set_string("doors_owner", player_name ); meta:set_string("infotext", "Owned by "..player_name) end -- prepare apartment rental panels local nodes = minetest.find_nodes_in_area( start_pos, end_pos, {'apartment:apartment'} ); if( extra_params and extra_params.apartment_type and extra_params.apartment_name ) then for _, p in ipairs(nodes ) do local meta = minetest.get_meta( p ); meta:set_string( 'original_owner', player_name ); -- lua can't count variables of this type on its own... local nr = 1; for _, _ in pairs( apartment.apartments ) do nr = nr+1; end -- this depends on relative position and param2 of the formspec local fields = { quit=true, store=true, size_up = math.abs( end_pos.y - p.y-1), size_down = math.abs(start_pos.y - p.y), norm_right = math.abs( end_pos.x - p.x-1), norm_left = math.abs(start_pos.x - p.x), norm_back = math.abs( end_pos.z - p.z-1), norm_front = math.abs(start_pos.z - p.z), category = extra_params.apartment_type, -- numbering them all seems best descr = extra_params.apartment_name }; -- up and down are independent of rotation fields.size_up = math.abs( end_pos.y - p.y-1); fields.size_down = math.abs(start_pos.y - p.y); local node = minetest.get_node( p ); if( node.param2 == 0 ) then -- z gets larger fields.size_left = fields.norm_left; fields.size_right = fields.norm_right; fields.size_back = fields.norm_back; fields.size_front = fields.norm_front; elseif( node.param2 == 1 ) then -- x gets larger fields.size_left = fields.norm_back; fields.size_right = fields.norm_front; fields.size_back = fields.norm_right; fields.size_front = fields.norm_left; elseif( node.param2 == 2 ) then -- z gets smaller fields.size_left = fields.norm_right; fields.size_right = fields.norm_left; fields.size_back = fields.norm_front; fields.size_front = fields.norm_back; elseif( node.param2 == 3 ) then -- x gets smaller fields.size_left = fields.norm_front; fields.size_right = fields.norm_back; fields.size_back = fields.norm_left; fields.size_front = fields.norm_right; end -- configure and prepare the apartment apartment.on_receive_fields( p, nil, fields, placer); end end end end -- this is lua...it doesn't contain the basic functions handle_schematics.table_contains = function( table, value ) local i = 1; local v; for i, v in ipairs( table ) do if( v==value ) then return true; end end return false; end handle_schematics.place_schematic = function( pos, param2, path, mirror, replacement_function, replacement_param, placer, do_copies, extra_params ) local node = minetest.env:get_node( pos ); if( not( node ) or not( node.param2 ) or node.name=="air") then if( not( param2 )) then return false; end node = {name="", param2 = param2 }; end local building_data = handle_schematics.analyze_mts_file( path ); if( not( building_data ) or not( building_data.size )) then if( placer ) then minetest.chat_send_player( placer:get_player_name(), 'Could not place schematic. Please check the filename.'); end return; end local position_data = handle_schematics.translate_param2_to_rotation( node.param2, mirror, pos, building_data.size, building_data.rotated, building_data.burried ); local replacements = {}; if( replacement_function ) then replacements = replacement_function( building_data.nodenames, replacement_param ); elseif( replacement_param and not replacement_param.even ) then replacements = replacement_param; end local force_place = true; -- when building scaffolding, do not replace anything yet if( replacement_function and replacement_function == handle_schematics.replacement_function_scaffolding ) then force_place = false; end -- it is possible that replacements require calls to on_constr/after_place_node -- and that the nodes that are introduced through replacements where not present in the original schematic local all_replacements = {}; for i, v in ipairs( replacements ) do table.insert( all_replacements, v[2] ); end if( replacement_param and replacement_param.even and replacement_param.odd ) then for i, v in ipairs( replacement_param.even ) do table.insert( all_replacements, v[2] ); end for i, v in ipairs( replacement_param.odd ) do table.insert( all_replacements, v[2] ); end end for i, v in ipairs( all_replacements ) do if( minetest.registered_nodes[ v ] and minetest.registered_nodes[ v ].on_construct and not(handle_schematics.table_contains( building_data.on_constr, v ))) then table.insert( building_data.on_constr, v ); end -- some nodes need after_place_node to be called for initialization if( minetest.registered_nodes[ v ] and minetest.registered_nodes[ v ].after_place_node and not(handle_schematics.table_contains( building_data.after_place_node, v ))) then table.insert( building_data.after_place_node, v ); end end -- apartments need a name if they are to be configured if( extra_params and not( extra_params.apartment_type )) then extra_params.apartment_type = 'apartment'; end -- actually place the schematic if( not( do_copies ) or not( do_copies.h ) or not( do_copies.v )) then minetest.place_schematic( position_data.start_pos, path..'.mts', tostring(position_data.rotate), replacements, force_place ); handle_schematics.update_nodes( position_data.start_pos, position_data.end_pos, building_data.on_constr, building_data.after_place_node, placer, extra_params ); else -- place multiple copies local vector = {h=-1,v=1}; if( node.param2 == 0 or node.param2 == 3) then --node.param2 == 1 or node.param2 == 3 ) then vector.h = 1; end -- it looks best if every second house is built out of another material local replacements_even = replacements; local replacements_odd = replacements; if( replacement_param and replacement_param.even and replacement_param.odd ) then replacements_even = replacement_param.even; replacements_odd = replacement_param.odd; end local p = {x=position_data.start_pos.x , y=position_data.start_pos.y, z=position_data.start_pos.z }; for j=1,do_copies.v do p.x = position_data.start_pos.x; p.z = position_data.start_pos.z; for i=1,do_copies.h do -- horizontal copies local key = ''; local val = {}; local p_end = {x=(p.x+position_data.max.x), y=(p.y+position_data.max.y), z=(p.z+position_data.max.z)}; for key,val in pairs( apartment.apartments ) do if( val and val.pos and (val.pos.x >= p.x) and (val.pos.x <= p_end.x) and (val.pos.y >= p.y) and (val.pos.y <= p_end.y) and (val.pos.z >= p.z) and (val.pos.z <= p_end.z)) then -- TODO: add FAIL if the apartment is still rented if( placer ) then minetest.chat_send_player( placer:get_player_name(), 'Removing Apartment '..tostring( key ).. ' (new usage for that place). Position: '..minetest.serialize( val.pos )); end print( 'Removing Apartment '..tostring( key )..' (new usage for that place). Position: '..minetest.serialize( val.pos )); apartment.apartments[ key ] = nil; end end -- switch replacements between houses if( i%2==0 ) then minetest.place_schematic( p, path..'.mts', tostring(position_data.rotate), replacements_even, force_place ); else minetest.place_schematic( p, path..'.mts', tostring(position_data.rotate), replacements_odd, force_place ); end -- generate apartment name if( extra_params and extra_params.apartment_type and extra_params.apartment_house_name ) then local nr = i-1; local apartment_name = ''; -- find the first free number for an apartment with this apartment_house_name while( nr < 99 and apartment_name == '' ) do nr = nr+1; apartment_name = extra_params.apartment_house_name..' '..tostring(j); if( nr < 10 ) then apartment_name = apartment_name..'0'..tostring(nr); elseif( nr<100 ) then apartment_name = apartment_name..tostring(nr); else apartment_name = ''; end -- avoid duplicates if( apartment.apartments[ apartment_name ] ) then apartment_name = ''; end end if( apartment_name ) then extra_params.apartment_name = apartment_name; else extra_params.apartment_name = nil; extra_params.apartment_type = nil; end end -- replacements_even/replacements_odd ought to affect only DECORATIVE nodes - and none that have on_construct/after_place_node! handle_schematics.update_nodes( p, {x=p.x+position_data.max.x, y=p.y+position_data.max.y, z=p.z+position_data.max.z}, building_data.on_constr, building_data.after_place_node, placer, extra_params ); if( node.param2 == 0 or node.param2 == 2 ) then p.x = p.x + vector.h*position_data.max.x; else p.z = p.z + vector.h*position_data.max.z; end end p.y = p.y + vector.v*position_data.max.y; end if( node.param2 == 0 or node.param2 == 2 ) then position_data.end_pos.x = position_data.start_pos.x + vector.h*position_data.max.x*do_copies.h; else position_data.end_pos.z = position_data.start_pos.z + vector.h*position_data.max.z*do_copies.v; end position_data.end_pos.y = position_data.start_pos.y + vector.v*position_data.max.y*do_copies.v; end return {start_pos = position_data.start_pos, end_pos = position_data.end_pos }; end -- replace all nodes with scaffolding ones so that the player can see where the real building will be placed handle_schematics.replacement_function_scaffolding = function( nodenames ) local replacements = {}; for _,v in ipairs( nodenames ) do table.insert( replacements, { v, handle_schematics.SCAFFOLDING }) end return replacements; end -- places nodes that look like leaves at the positions where the building was; -- those nodes will decay using an abm; -- this gradual disappearance of the building helps to understand the player what -- just happend (=building was removed) and where it happened handle_schematics.replacement_function_decay = function( nodenames ) local replacements = {}; for _,v in ipairs( nodenames ) do if( handle_schematics.ENABLE_SLOW_DECAY ) then table.insert( replacements, { v, handle_schematics.AUTODECAY }) else table.insert( replacements, { v, 'air' }) end end return replacements; end handle_schematics.update_apartment_spawner_formspec = function( pos ) local meta = minetest.get_meta( pos ); if( not( meta ) or not( meta:get_string('path')) or meta:get_string('path')=='') then return 'size[9,7]'.. 'label[2.0,-0.3;Apartment Spawner]'.. 'label[0.5,0.5;Load schematic from file:]'.. 'field[5.0,0.9;4.0,0.5;path;;apartment_4x11_0_180]'.. 'label[0.5,1.5;Name for this apartment house:]'.. 'field[5.0,1.9;4.0,0.5;apartment_house_name;;Enter house name]'.. 'label[0.5,2.0;Category (i.e. house, shop):]'.. 'field[5.0,2.4;4.0,0.5;apartment_type;;apartment]'.. 'label[0.5,2.5;Horizontal copies (to the left):]'.. 'field[5.0,2.9;0.5,0.5;h;;1]'.. 'label[0.5,3.0;Vertical copies (upwards):]'.. 'field[5.0,3.4;0.5,0.5;v;;1]'.. 'label[0.5,3.5;Replace clay in 1st building:]'.. 'field[5.0,3.9;4.0,0.5;replacement_1;;default:sandstonebrick]'.. 'label[0.5,4.0;Replace clay in 2nd building:]'.. 'field[5.0,4.4;4.0,0.5;replacement_2;;default:brick]'.. 'button_exit[4,6.0;2,0.5;store;Spawn building]'.. 'button_exit[1,6.0;1,0.5;abort;Abort]'; end return 'size[9,7]'.. 'label[2.0,-0.3;Information about the spawned Apartment]'.. 'label[0.5,0.5;The schematic was loaded from:]'.. 'label[5.0,0.5;'..tostring( meta:get_string('path'))..']'.. 'label[0.5,1.5;Name for this apartment house:]'.. 'label[5.0,1.5;'..tostring( meta:get_string('apartment_house_name'))..']'.. 'label[0.5,2.0;Category (i.e. house, shop):]'.. 'label[5.0,2.0;'..tostring( meta:get_string('apartment_type'))..']'.. 'label[0.5,2.5;Horizontal copies (to the left):]'.. 'label[5.0,2.5;'..tostring( meta:get_string('h'))..']'.. 'label[0.5,3.0;Vertical copies (upwards):]'.. 'label[5.0,3.0;'..tostring( meta:get_string('v'))..']'.. 'label[0.5,3.5;Replace clay in 1st building:]'.. 'label[5.0,3.5;'..tostring( meta:get_string('replacement_1'))..']'.. 'label[0.5,4.0;Replace clay in 2nd building:]'.. 'label[5.0,4.0;'..tostring( meta:get_string('replacement_2'))..']'.. 'label[0.5,4.5;This building was spawned by:]'.. 'label[5.0,4.5;'..tostring( meta:get_string('placed_by'))..']'.. 'button_exit[4,6.0;2,0.5;store;Remove building]'.. 'button_exit[1,6.0;1,0.5;abort;OK]'; end handle_schematics.on_receive_fields = function(pos, formname, fields, sender) local meta = minetest.get_meta( pos ); if( not( sender )) then return; end pname = sender:get_player_name(); if( not( minetest.check_player_privs(pname, {apartment_unrent=true}))) then minetest.chat_send_player( pname, 'You do not have the necessary privileges.'); return; end if( meta and (not( meta:get_string('path')) or meta:get_string('path')=='') and fields.store) then -- check if all params have been supplied if( not( fields.path ) or not( fields.apartment_house_name ) or not( fields.apartment_type ) or not( fields.h ) or not( fields.v ) or not( fields.replacement_1 ) or not( fields.replacement_2 )) then minetest.chat_send_player( pname, 'Please fill all fields with information.'); return; end fields.h = tonumber( fields.h ); if( fields.h < 1 or fields.h > 20 ) then fields.h = 1; end fields.h = tostring( fields.h ); fields.v = tonumber( fields.v ); if( fields.v < 1 or fields.v > 20 ) then fields.v = 1; end fields.v = tostring( fields.v ); meta:set_string('path', fields.path ); meta:set_string('apartment_house_name', fields.apartment_house_name ); meta:set_string('apartment_type', fields.apartment_type ); meta:set_string('h', fields.h ); meta:set_string('v', fields.v ); meta:set_string('replacement_1', fields.replacement_1 ); meta:set_string('replacement_2', fields.replacement_2 ); meta:set_string('placed_by', pname ); meta:set_string('formspec', handle_schematics.update_apartment_spawner_formspec( pos )); minetest.chat_send_player( pname, 'Placing building '..tostring( fields.path )); local path = minetest.get_modpath("apartment")..'/schems/'..fields.path; local mirror = 0; local replacement_function = nil; local replacement_param = { odd={{'default:clay',fields.replacement_1}}, even={{'default:clay',fields.replacement_2}}}; local res = {}; res = handle_schematics.place_schematic( pos, nil, path, mirror, replacement_function, replacement_param, sender, {h=fields.h,v=fields.v}, { apartment_type = fields.apartment_type, apartment_house_name = fields.apartment_house_name}) if( res and res.start_pos ) then meta:set_string('start_pos', minetest.serialize( res.start_pos )); meta:set_string('end_pos', minetest.serialize( res.end_pos )); end return; end -- TODO minetest.chat_send_player( pname, 'Dig this spawner in order to remove the building.'); end minetest.register_node("apartment:build_chest", { description = "Apartment spawner", tiles = {"default_chest_side.png", "default_chest_top.png^door_steel.png", "default_chest_side.png", "default_chest_side.png", "default_chest_side.png", "default_chest_lock.png^door_steel.png"}, paramtype2 = "facedir", groups = {snappy=2,choppy=2,oddly_breakable_by_hand=2}, legacy_facedir_simple = true, after_place_node = function(pos, placer, itemstack) local meta = minetest.get_meta( pos ); meta:set_string('formspec', handle_schematics.update_apartment_spawner_formspec( pos )); end, on_receive_fields = function( pos, formname, fields, sender ) handle_schematics.on_receive_fields(pos, formname, fields, sender) end, -- if the building chest is removed, remove the building as well - and place nodes looking like leaves and autodecaying in order -- to indicate where the building has been after_dig_node = function(pos, oldnode, oldmetadata, digger) local meta = minetest.get_meta( pos ); if( oldmetadata and oldmetadata.fields and oldmetadata.fields.path ) then local replacement_function = handle_schematics.replacement_function_decay; local replacement_param = nil; local path = minetest.get_modpath("apartment")..'/schems/'..oldmetadata.fields.path; minetest.chat_send_player( digger:get_player_name(), 'Removing building '..tostring( oldmetadata.fields.path )); handle_schematics.place_schematic( pos, oldnode.param2, path, 0, replacement_function, replacement_param, digger, {h=oldmetadata.fields.h,v=oldmetadata.fields.v} ) end end, -- check if digging is allowed can_dig = function(pos,player) if( not( player )) then return false; end local pname = player:get_player_name(); if( not( minetest.check_player_privs(pname, {apartment_unrent=true}))) then minetest.chat_send_player( pname, 'You do not have the apartment_unrent priv which is necessary to dig this node.'); return false; end local meta = minetest.get_meta( pos ); local old_placer = meta:get_string('placed_by'); if( old_placer and old_placer ~= '' and old_placer ~= pname ) then minetest.chat_send_player( pname, 'Only '..tostring( old_placer )..' can dig this node.'); return false; end return true; end, }) if handle_schematics.ENABLE_SLOW_DECAY then minetest.register_node( handle_schematics.AUTODECAY, { description = "decaying building", drawtype = "allfaces_optional", visual_scale = 1.3, tiles = {"default_leaves.png"}, paramtype = "light", waving = 1, is_ground_content = false, groups = {snappy=3}, }) minetest.register_abm({ nodenames = {handle_schematics.AUTODECAY}, -- A low interval and a high inverse chance spreads the load interval = 2, chance = 3, action = function(p0, node, _, _) minetest.remove_node( p0 ); end }) end