local S = minetest.get_translator("lzr_editor") local FS = function(...) return minetest.formspec_escape(S(...)) end local NS = function(s) return end local F = minetest.formspec_escape local select_item = dofile(minetest.get_modpath("lzr_editor").."/select_item.lua") -- Text color the special autosave level will be highlighted -- in the save/load dialog local AUTOSAVE_HIGHLIGHT = "#FF8060" -- List of items to give to player on editor entry. -- This must be compatible with inventory:set_list(). local INITIAL_EDITOR_ITEMS = { -- tools "lzr_tools:ultra_pickaxe", "lzr_tools:ultra_bucket", "lzr_hook:hook", "lzr_laser:block_state_toggler", "lzr_laser:trigger_tool", "lzr_laser:color_changer", "lzr_tools:variant_changer", "lzr_laser:screw_changer", -- important nodes "lzr_laser:emitter_red_fixed_on", "lzr_laser:detector_fixed", "lzr_teleporter:teleporter_off", "lzr_treasure:chest_wood_locked", "lzr_treasure:chest_dark_locked", "lzr_laser:mirror_fixed", "lzr_laser:double_mirror_00_fixed", "lzr_laser:transmissive_mirror_00_fixed", } -- If the player is this far away out of the level bounds, -- teleport back to level on level resize, level move -- or level load. local OUT_OF_BOUNDS_TOLERANCE = 1 lzr_editor = {} -- Remember if player has already entered the level editor in this session lzr_editor.first_time = true -- Remember if the WorldEdit usage warning has already been shown in this session -- (a warning is shown when a WorldEdit command is used because some commands -- might break the triggers) lzr_editor.worldedit_warning = false -- The level state holds the metadata for -- the level that is being currently edited. -- INITIAL_LEVEL_STATE is the default state -- and the state when the editor starts. -- NOTE: level size and position are stored by -- the lzr_world mod. local INITIAL_LEVEL_STATE = { name = "", file_name = "", size = lzr_globals.DEFAULT_LEVEL_SIZE, wall = lzr_globals.DEFAULT_WALL_NODE, ceiling = lzr_globals.DEFAULT_CEILING_NODE, floor = lzr_globals.DEFAULT_FLOOR_NODE, ambient = lzr_ambience.DEFAULT_AMBIENCE, sky = lzr_globals.DEFAULT_SKY, npc_texts = lzr_globals.DEFAULT_NPC_TEXTS, weather = lzr_globals.DEFAULT_WEATHER, backdrop = lzr_globals.DEFAULT_BACKDROP, backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS, triggers = "", } local level_state = table.copy(INITIAL_LEVEL_STATE) local temp_settings_state = {} -- Used to store the current contents for the -- textlist contents of the custom level file list. -- (this assumes singleplayer) local level_file_textlist_state = {} -- Error and warning texts for the level checker local error_warning_texts = { no_teleporter = S("No teleporter"), too_many_teleporters = S("More than one teleporter"), barriers = S("Barrier or rain membrane in level area"), gold_block = S("Bare gold block in level area"), plant_on_ground = S("Rooted plant in level area"), too_many_parrot_spawners = S("More than one parrot spawner"), too_many_hidden_parrot_spawners = S("More than one hidden parrot spawner"), --~ param2 is an internal value used by blocks to store some state bad_hidden_parrot_spawner = S("Bad param2 value for hidden parrot spawner"), trigger_out_of_bounds = S("Trigger is out of bounds"), trigger_moved = S("Trigger ID does not match location"), laser_incompatible = S("Laser-incompatible node found"), no_treasures = S("No treasures to collect"), --~ Warning shown when a level has an incomplete door half_door = S("Incomplete door"), --~ Warning shown when a level has an invalid door. param2 is an internal value used by blocks to store some state incompatible_door_segments_param2 = S("Mismatching param2 value for door segments"), --~ Warning shown when a level has an invalid door incompatible_door_segments_type = S("Mismatching type for door segments"), } -- Add error messages from lzr_triggers as well for id, text in pairs(lzr_triggers.CHECK_ERROR_TEXTS) do error_warning_texts["lzr_triggers_"..id] = text end -- Creates a nice-looking string listing all level warnings and errors. -- Then returns it. -- If everything is OK, returns the empty string. local list_warnings_and_errors = function() local result_str = "" local check1_ok, errors = lzr_editor.check_level_errors() if not check1_ok then -- Level error found. local error_texts = {} for e=1, #errors do table.insert(error_texts, S("• Error: @1", error_warning_texts[errors[e]] or errors[e])) end local errors_str = table.concat(error_texts, "\n") result_str = result_str .. minetest.colorize("#FF8000", errors_str) end local check2_ok, warnings = lzr_editor.check_level_warnings() if not check2_ok then local warning_texts = {} for w=1, #warnings do table.insert(warning_texts, S("• Warning: @1", error_warning_texts[warnings[w]] or warnings[w])) end local warnings_str = table.concat(warning_texts, "\n") if not check1_ok then result_str = result_str .. "\n" end result_str = result_str .. minetest.colorize("#FFFF00", warnings_str) end return result_str end -- Move player to the level if they're too far away local function move_player_to_level_if_neccessary(player) local lsoffset = vector.new(OUT_OF_BOUNDS_TOLERANCE, OUT_OF_BOUNDS_TOLERANCE, OUT_OF_BOUNDS_TOLERANCE) local lsminpos = vector.subtract(lzr_world.get_level_pos(), lsoffset) local lsmaxpos = vector.add(vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size()), lsoffset) if not vector.in_area(player:get_pos(), lsminpos, lsmaxpos) then player:set_pos(lzr_world.get_level_pos()) end end -- Give useful starter items to player local function give_initial_items(player) local inv = player:get_inventory() -- Set all items at once inv:set_list("main", INITIAL_EDITOR_ITEMS) -- Check if we actually have those items because we're paranoid for i=1, #INITIAL_EDITOR_ITEMS do local item = inv:get_stack("main", i) if item:get_name() ~= INITIAL_EDITOR_ITEMS[i] then minetest.log("error", "[lzr_editor] Wrong or missing initial item at position "..i..": expected "..INITIAL_EDITOR_ITEMS[i].." but got "..item:get_name()) break end end end -- Returns true if pos is within the current bounds -- of the actively edited level. function lzr_editor.is_in_level_bounds(pos) local offset = table.copy(lzr_world.get_level_size()) offset = vector.offset(offset, -1, -1, -1) return vector.in_area(pos, lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), offset)) end -- Enter editor state (if not already in it). -- Returns true if state changed, false if already in editor. function lzr_editor.enter_editor(player) local state = lzr_gamestate.get_state() if state == lzr_gamestate.EDITOR then return false else level_state = table.copy(INITIAL_LEVEL_STATE) local pos = lzr_world.get_level_pos() local size = lzr_world.get_level_size() lzr_levels.reset_level_area(false, pos, size) lzr_world.set_level_pos(lzr_globals.DEFAULT_LEVEL_POS) lzr_world.set_level_size(lzr_globals.DEFAULT_LEVEL_SIZE) pos = lzr_world.get_level_pos() size = lzr_world.get_level_size() lzr_triggers.reset_triggers() lzr_gamestate.set_state(lzr_gamestate.EDITOR) lzr_ambience.set_ambience(lzr_ambience.DEFAULT_AMBIENCE) lzr_sky.set_sky(lzr_globals.DEFAULT_SKY) lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER) lzr_gui.set_loading_gui(player) lzr_player.set_editor_inventory(player, true) lzr_player.set_loading_inventory_formspec(player) lzr_gamestate.set_loading(true) -- Stuff to do after the editor was loaded local done = function() lzr_player.set_editor_inventory(player) give_initial_items(player) lzr_privs.grant_edit_privs(player) if lzr_editor.first_time then local message = S("Welcome to the Level Editor!").."\n".. S("See LEVEL_EDITOR.md for instructions.") minetest.chat_send_player("singleplayer", message) lzr_editor.first_time = false end lzr_gui.set_editor_gui(player) lzr_gui.show_level_bounds(player, pos, size) lzr_gamestate.set_loading(false) minetest.log("action", "[lzr_editor] Entered and initialized level editor") end lzr_levels.build_room({pos=pos, size=size, spawn_pos=pos, yaw=0, clear=true, callback_done=done}) minetest.log("action", "[lzr_editor] Entering level editor ...") return true end end local check_for_slash = function(str) if string.match(str, "[/\\]") then return true else return false end end local save_level = function(level_name, is_autosave) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false end if check_for_slash(level_name) then return false end -- Show level errors and warnings when saving manually if not is_autosave then local result_str = list_warnings_and_errors() if result_str ~= "" then minetest.chat_send_player("singleplayer", S("The following problems were found in this level:") .. "\n" .. result_str) end end minetest.mkdir(minetest.get_worldpath().."/levels") local filename = minetest.get_worldpath().."/levels/"..level_name..".mts" local size = vector.subtract(lzr_world.get_level_size(), vector.new(1, 1, 1)) --[[ <<>> ]] local ok = minetest.create_schematic(lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), size), {}, filename, {}) --[[ ^ Note: This saves the level exactly as-is, including the current state with all active lasers and active laser blocks. This is intended. The game will automatically clear lasers, reset laser blocks and recalculate the lasers on each level start anyway, so it shouldn't matter in which 'state' the level is stored. Saving the level with the active lasers pre-calculated tho has the slight benefit that the level instantly loads with the correct lasers. ]] -- Save level data into CSV local csv_filename = minetest.get_worldpath().."/levels/"..level_name..".csv" local npc_texts_csv = "" if level_state.npc_texts and level_state.npc_texts.goldie then npc_texts_csv = level_state.npc_texts.goldie or "" end local csv_contents = lzr_csv.write_csv({{ level_name..".mts", level_state.name, -- level boundaries syntax: wall||floor|ceiling -- (the used to be a node, but it has been removed from the game) level_state.wall .. "||" .. level_state.floor .. "|" .. level_state.ceiling, level_state.ambient, level_state.sky, npc_texts_csv, level_state.weather, level_state.backdrop, minetest.pos_to_string(level_state.backdrop_pos), lzr_levels.serialize_triggers(), }}) local ok_csv = minetest.safe_file_write(csv_filename, csv_contents) if ok and ok_csv then minetest.log("action", "[lzr_editor] Level written to "..filename.." and "..csv_filename) if not is_autosave then level_state.file_name = level_name end return true, filename, csv_filename elseif ok then minetest.log("error", "[lzr_editor] Level written to "..filename..", but failed to write CSV!") return false, filename, csv_filename else minetest.log("error", "[lzr_editor] Failed to write level to "..filename) return false, filename, csv_filename end end local autosave_level = function() save_level(lzr_globals.AUTOSAVE_NAME, true) end -- Exit editor state (if not already outside of editor). -- Returns true if state changed, false if could not exit local function exit_editor(player) if lzr_gamestate.is_loading() then return false end level_state.file_name = "" local state = lzr_gamestate.get_state() if state ~= lzr_gamestate.EDITOR then return false else lzr_levels.go_to_menu() local pos = lzr_world.get_level_pos() local size = lzr_world.get_level_size() lzr_levels.reset_level_area(false, pos, size) return true end end minetest.register_chatcommand("editor_save", { description = S("Save current level"), params = S(""), func = function(name, param) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") elseif lzr_gamestate.is_loading() then return false, S("Can’t do this while loading!") end if param == "" then return false, S("No level name provided.") end local level_name = param if check_for_slash(level_name) then return false, S("Level name must not contain slash or backslash!") end local ok, filename, filename2 = save_level(level_name) if ok and filename and filename2 then --~ @1 and @2 are file locations return true, S("Level saved to @1 and @2.", filename, filename2) elseif of and filename then --~ @1 and @2 are file locations return true, S("Level saved to @1, but could not write metadata to @2.", filename, filename2) else return false, S("Error writing level file!") end end, }) local load_level = function(level_name, player) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false end if check_for_slash(level_name) then return false end local filename = level_name..".mts" local ok = lzr_util.file_exists(minetest.get_worldpath().."/levels", filename) if not ok then return false end local schem = minetest.read_schematic(minetest.get_worldpath().."/levels/"..filename, {write_yslice_prob="none"}) if schem then local csv_file = io.open(minetest.get_worldpath().."/levels/"..level_name..".csv", "r") local triggers_str = "" if csv_file then local csv_string = csv_file:read("*a") csv_file:close() local csv_parsed = lzr_csv.parse_csv(csv_string) if csv_parsed and #csv_parsed >= 1 then level_state.name = csv_parsed[1][2] local bounds = csv_parsed[1][3] local exploded_bounds = string.split(bounds, "|", true) if exploded_bounds then level_state.wall = exploded_bounds[1] local legacy_window = exploded_bounds[2] level_state.floor = exploded_bounds[3] -- Force ceiling to be barrier if legacy window -- node is used so that the level still gets -- sunlight if legacy_window == "lzr_decor:woodframed_glass" then level_state.ceiling = "lzr_core:barrier" --~ The "window boundary" refers to a special block used for the boundaries of the level to create windows in the walls. It has been removed in later versions of the game. minetest.chat_send_player(player:get_player_name(), S("Note: This level uses a legacy window boundary, which is no longer supported.")) else level_state.ceiling = exploded_bounds[4] end end level_state.ambient = csv_parsed[1][4] level_state.sky = csv_parsed[1][5] or lzr_globals.DEFAULT_SKY level_state.npc_texts = csv_parsed[1][6] if level_state.npc_texts then level_state.npc_texts = { goldie = level_state.npc_texts } else level_state.npc_texts = lzr_globals.DEFAULT_NPC_TEXTS end level_state.weather = csv_parsed[1][7] or lzr_globals.DEFAULT_WEATHER level_state.backdrop = csv_parsed[1][8] or lzr_globals.DEFAULT_BACKDROP local parsed_backdrop_pos = csv_parsed[1][9] local backdrop_pos if parsed_backdrop_pos then backdrop_pos = minetest.string_to_pos(parsed_backdrop_pos) end if backdrop_pos then level_state.backdrop_pos = backdrop_pos else level_state.backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS end triggers_str = csv_parsed[1][10] or "" else minetest.log("error", "[lzr_editor] Error parsing CSV file for "..level_name) end else minetest.log("error", "[lzr_editor] No CSV file found for "..level_name) end lzr_world.set_level_size(table.copy(schem.size)) level_state.size = lzr_world.get_level_size() level_state.name = level_state.name or "" level_state.wall = level_state.wall or lzr_globals.DEFAULT_WALL_NODE level_state.ceiling = level_state.ceiling or lzr_globals.DEFAULT_CEILING_NODE level_state.floor = level_state.floor or lzr_globals.DEFAULT_FLOOR_NODE level_state.ambient = level_state.ambient or lzr_ambience.DEFAULT_AMBIENCE level_state.sky = level_state.sky or lzr_globals.DEFAULT_SKY level_state.npc_texts = level_state.npc_texts or lzr_globals.DEFAULT_NPC_TEXTS level_state.weather = level_state.weather or lzr_globals.DEFAULT_WEATHER level_state.backdrop = level_state.backdrop or lzr_globals.DEFAULT_BACKDROP level_state.backdrop_pos = level_state.backdrop_pos or lzr_globals.DEFAULT_BACKDROP_POS if level_state.backdrop == "ocean" then lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_OCEAN)) elseif level_state.backdrop == "islands" then lzr_world.set_level_pos(table.copy(level_state.backdrop_pos)) elseif level_state.backdrop == "underground" then lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_UNDERGROUND)) elseif level_state.backdrop == "sky" then lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_SKY)) end local bounding_nodes = { node_wall = level_state.wall, node_ceiling = level_state.ceiling, node_floor = level_state.floor, } lzr_gamestate.set_loading(true) lzr_gui.set_loading_gui(player) lzr_player.set_loading_inventory_formspec(player) lzr_player.set_editor_inventory(player, true) local done = function() lzr_gui.show_level_bounds(player, lzr_world.get_level_pos(), schem.size) if lzr_ambience.ambience_exists(level_state.ambient) then lzr_ambience.set_ambience(level_state.ambient) else level_state.ambient = "none" lzr_ambience.set_ambience("none") end if lzr_sky.sky_exists(level_state.sky) then lzr_sky.set_sky(level_state.sky) else lzr_sky.set_sky(lzr_globals.DEFAULT_SKY) end if lzr_weather.weather_exists(level_state.weather) then lzr_weather.set_weather(level_state.weather) else lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER) end lzr_player.set_editor_inventory(player) lzr_gui.set_editor_gui(player) lzr_gamestate.set_loading(false) minetest.log("action", "[lzr_editor] Loaded level "..filename) end lzr_levels.prepare_and_build_custom_level(lzr_world.get_level_pos(), schem, "start", nil, bounding_nodes, triggers_str, done) level_state.file_name = level_name minetest.log("action", "[lzr_editor] Started loading level from "..filename.." ...") return true else minetest.log("error", "[lzr_editor] Failed to read level from "..filename) return false end end minetest.register_chatcommand("editor_load", { description = S("Load level"), params = S(""), func = function(name, param) local player = minetest.get_player_by_name(name) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") elseif lzr_gamestate.is_loading() then return false, S("The editor is already loading a level!") end if param == "" then return false, S("No level name provided.") end local level_name = param if check_for_slash(level_name) then return false, S("Level name must not contain slash or backslash!") end local ok = lzr_util.file_exists(minetest.get_worldpath().."/levels", level_name..".mts") if not ok then return false, S("Level file does not exist!") end local ok = load_level(level_name, player) if ok then return true, S("Level loaded.") else return false, S("Error reading level file!") end end, }) minetest.register_chatcommand("editor", { description = S("Start or exit level editor"), params = S("[ enter | exit ]"), func = function(name, param) local player = minetest.get_player_by_name(name) if param == "" or param == "enter" then if lzr_gamestate.get_state() == lzr_gamestate.EDITOR then return false, S("Already in level editor!") elseif lzr_gamestate.is_loading() then return false, S("Can’t do this while loading!") end local ok = lzr_editor.enter_editor(player) if ok then return true else minetest.log("error", "[lzr_editor] Can't enter editor for unknown reason") return false end elseif param == "exit" then if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in level editor!") elseif lzr_gamestate.is_loading() then return false, S("Can’t do this while loading!") end local ok = exit_editor(player) if ok then return true else minetest.log("error", "[lzr_editor] Can't exit editor for unknown reason") return false end end return false end, }) minetest.register_chatcommand("reset_triggers", { description = S("Remove all triggers and reset them to their initial state"), params = "", func = function(name, param) local state = lzr_gamestate.get_state() if state ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") elseif lzr_gamestate.is_loading() then return false, S("Can’t do this while loading!") end lzr_levels.init_triggers() return true, S("Triggers have been reset.") end }) -- Unlimited node placement in editor mode minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack) if placer and placer:is_player() then return lzr_gamestate.get_state() == lzr_gamestate.EDITOR end end) -- Don't pick node up if the item is already in the inventory local old_handle_node_drops = minetest.handle_node_drops function minetest.handle_node_drops(pos, drops, digger) if not digger or not digger:is_player() or lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return old_handle_node_drops(pos, drops, digger) end local inv = digger:get_inventory() if inv then for _, item in ipairs(drops) do if not inv:contains_item("main", item, true) then inv:add_item("main", item) end end end end lzr_gamestate.register_on_enter_state(function(state) if state == lzr_gamestate.EDITOR then local player = minetest.get_player_by_name("singleplayer") local inv = player:get_inventory() inv:set_list("main", {}) end end) lzr_gamestate.register_on_exit_state(function(state) if state == lzr_gamestate.EDITOR then minetest.log("action", "[lzr_editor] Autosaving level on editor exit") autosave_level() local player = minetest.get_player_by_name("singleplayer") if player then lzr_privs.revoke_edit_privs(player) end end end) local show_settings_dialog = function(player, settings_state) if not settings_state then settings_state = level_state end -- Shorthand function to get the current dropdown index of -- a multiple-choice item (ambience, sky, weather) as well -- as a string to insert in the dropdown[] formspec element local get_current_thing = function(thing_type, thing_getter, description_getter) local thing_list = "" local current_thing = 1 local things = thing_getter() for t=1, #things do local description if description_getter then description = description_getter(things[t]) else description = things[t] end -- Construct string for dropdown[] thing_list = thing_list .. F(description) -- Current thing found! if things[t] == settings_state[thing_type] then current_thing = t end -- Append comma except at end if t < #things then thing_list = thing_list .. "," end end return thing_list, current_thing end local ambient_list, current_ambient = get_current_thing("ambient", lzr_ambience.get_ambiences, lzr_ambience.get_ambience_description) local sky_list, current_sky = get_current_thing("sky", lzr_sky.get_skies, lzr_sky.get_sky_description) local weather_list, current_weather = get_current_thing("weather", lzr_weather.get_weathers, lzr_weather.get_weather_description) local backdrop_list, current_backdrop = get_current_thing("backdrop", lzr_mapgen.get_backdrops, lzr_mapgen.get_backdrop_description) local level_size = lzr_world.get_level_size() local boundary_button = function(boundaryname, nodename, y) if minetest.registered_nodes[nodename] then return "item_image_button[7.75,"..y..";0.6,0.6;"..F(nodename)..";level_"..boundaryname.."_select;]" else local btn = "image_button[7.75,"..y..";0.6,0.6;"..F("[inventorycube{unknown_node.png{unknown_node.png{unknown_node.png")..";level_"..boundaryname.."_select;]" if type(nodename) == "string" then btn = btn .. "tooltip[level_"..boundaryname.."_select;"..F(nodename).."]" end return btn end end local form = "formspec_version[7]".. "size[9,11.7]".. "box[0,0;9,0.8;#00000080]".. "label[0.4,0.4;"..FS("Level settings").."]".. "field[0.5,1.3;8,0.6;level_name;"..FS("Name")..";"..F(settings_state.name).."]".. "label[0.5,2.3;"..FS("Size").."]".. "field[1.6,2.3;1,0.6;level_size_x;"..FS("X")..";"..F(tostring(settings_state.size.x)).."]".. "field[2.62,2.3;1,0.6;level_size_y;"..FS("Y")..";"..F(tostring(settings_state.size.y)).."]".. "field[3.63,2.3;1,0.6;level_size_z;"..FS("Z")..";"..F(tostring(settings_state.size.z)).."]".. "field[0.5,3.3;7,0.6;level_wall;"..FS("Wall node")..";"..F(settings_state.wall).."]".. "field[0.5,4.3;7,0.6;level_floor;"..FS("Floor node")..";"..F(settings_state.floor).."]".. "field[0.5,5.3;7,0.6;level_ceiling;"..FS("Ceiling node")..";"..F(settings_state.ceiling).."]".. boundary_button("wall", settings_state.wall, 3.3).. boundary_button("floor", settings_state.floor, 4.3).. boundary_button("ceiling", settings_state.ceiling, 5.3).. "field[0.5,6.3;8,0.6;level_npc_goldie;"..FS("Goldie speech")..";"..F(settings_state.npc_texts.goldie).."]".. "label[0.5,7.1;"..FS("Music").."]".. "dropdown[0.5,7.3;8,0.6;level_ambient;"..ambient_list..";"..current_ambient..";true]".. "label[0.5,8.2;"..FS("Sky").."]".. "dropdown[0.5,8.4;3.5,0.6;level_sky;"..sky_list..";"..current_sky..";true]".. "label[5,8.2;"..FS("Weather").."]".. "dropdown[5,8.4;3.5,0.6;level_weather;"..weather_list..";"..current_weather..";true]".. "label[0.5,9.3;"..FS("Backdrop").."]".. "dropdown[0.5,9.5;3.5,0.6;level_backdrop;"..backdrop_list..";"..current_backdrop..";true]" -- backdrop_pos is only relevant for islands backdrop if settings_state.backdrop == "islands" then form = form .. "field[5,9.5;1,0.6;level_backdrop_pos_x;"..FS("X")..";"..F(tostring(settings_state.backdrop_pos.x)).."]".. "field[6.02,9.5;1,0.6;level_backdrop_pos_y;"..FS("Y")..";"..F(tostring(settings_state.backdrop_pos.y)).."]".. "field[7.04,9.5;1,0.6;level_backdrop_pos_z;"..FS("Z")..";"..F(tostring(settings_state.backdrop_pos.z)).."]".. "tooltip[level_backdrop_pos_x;"..FS("X coordinate of backdrop position").."]".. "tooltip[level_backdrop_pos_y;"..FS("Y coordinate of backdrop position").."]".. "tooltip[level_backdrop_pos_z;"..FS("Z coordinate of backdrop position").."]" end form = form .. "tooltip[level_name;"..FS("Level name as shown to the player").."]".. "tooltip[level_size_x;"..FS("Level size along the X axis").."]".. "tooltip[level_size_y;"..FS("Level size along the Y axis").."]".. "tooltip[level_size_z;"..FS("Level size along the Z axis").."]".. "tooltip[level_wall;"..FS("Itemstring of node to be placed on the left, front, back and right level borders").."]".. "tooltip[level_floor;"..FS("Itemstring of node to be placed at the bottom of the level").."]".. "tooltip[level_ceiling;"..FS("Itemstring of node to be placed at the top of the level").."]".. "tooltip[level_npc_goldie;"..FS("Text to be shown when player interacts with Goldie the Parrot").."]".. "tooltip[level_ambient;"..FS("Which audio ambience to play").."]".. "tooltip[level_sky;"..FS("How the sky looks like. Affects color, sun, moon, stars, clouds and the time of day").."]".. "tooltip[level_weather;"..FS("Visual weather effects (no audio)").."]".. "tooltip[level_backdrop;"..FS("The world that surrounds the level").."]".. "button_exit[0.5,10.5;3.5,0.7;level_ok;"..FS("OK").."]".. "button_exit[5,10.5;3.5,0.7;level_cancel;"..FS("Cancel").."]".. "field_close_on_enter[level_ok;true]" minetest.show_formspec(player:get_player_name(), "lzr_editor:level_settings", form) end local show_save_load_dialog = function(player, save, level_name) if not level_name then level_name = "" end local txt_caption local txt_action local action if save then txt_caption = S("Save level as …") txt_action = S("Save") action = "save" else txt_caption = S("Load level …") txt_action = S("Load") action = "load" end local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false) local levels = {} for l=1, #levels_files do if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then local name = string.sub(levels_files[l], 1, -5) table.insert(levels, name) end end table.sort(levels) level_file_textlist_state = levels local levels_str = "" for l=1, #levels do local entry = levels[l] if entry == lzr_globals.AUTOSAVE_NAME then entry = AUTOSAVE_HIGHLIGHT .. entry end levels_str = levels_str .. entry if l < #levels then levels_str = levels_str .. "," end end local level_list_elem = "" if #levels > 0 then level_list_elem = "label[0.5,1.5;"..FS("File list:").."]" .. "textlist[0.5,2;8,4;level_list;"..levels_str.."]" end local form = "formspec_version[7]".. "size[9,9]".. "box[0,0;9,0.8;#00000080]".. "label[0.4,0.4;"..F(txt_caption).."]".. level_list_elem.. "field[0.5,6.6;7,0.6;file_name;"..FS("File name")..";"..minetest.formspec_escape(level_name).."]".. "label[7.7,6.9;.mts]".. "button_exit[0.5,7.7;3.5,0.7;"..action..";"..F(txt_action).."]".. "button_exit[5,7.7;3.5,0.7;cancel;"..FS("Cancel").."]".. "field_close_on_enter["..action..";true]" local formname if save then formname = "lzr_editor:level_save" else formname = "lzr_editor:level_load" end minetest.show_formspec(player:get_player_name(), formname, form) end -- Remove all triggers that are out of level bounds local clear_out_of_bounds_triggers = function() local cleared = 0 local minpos, maxpos = lzr_world.get_level_bounds() for trigger_id, trigger in pairs(lzr_triggers.get_triggers()) do local trigger_pos = minetest.string_to_pos(trigger_id) if not vector.in_area(trigger_pos, minpos, maxpos) then lzr_triggers.remove_trigger(trigger_id) cleared = cleared + 1 end end if cleared > 0 then minetest.log("info", "[lzr_editor] Removed "..cleared.." triggers that were out of bounds") end end -- Check the level area for errors lzr_editor.check_level_errors = function() local minpos, maxpos = lzr_world.get_level_bounds() local errors = {} -- Test: Count teleporters (must have exactly 1) local teleporters = minetest.find_nodes_in_area(minpos, maxpos, "lzr_teleporter:teleporter_off") if #teleporters == 0 then table.insert(errors, "no_teleporter") elseif #teleporters > 1 then table.insert(errors, "too_many_teleporters") end -- Test: Barriers in level area (none allowed, they're only allowed for the level bounds) local barriers = minetest.find_nodes_in_area(minpos, maxpos, "group:barrier") if #barriers > 0 then table.insert(errors, "barriers") end -- Test: Bare gold block (none allowed) local gold_block = minetest.find_nodes_in_area(minpos, maxpos, "lzr_treasure:gold_block") if #gold_block > 0 then table.insert(errors, "gold_block") end -- Test: Rooted plants (none allowed, should use normal plants instead) local plants_on_ground = minetest.find_nodes_in_area(minpos, maxpos, "group:plant_on_ground") if #plants_on_ground > 0 then table.insert(errors, "plant_on_ground") end -- Test: More than one parrot spawner (max. 1 allowed) local parrot_spawners = minetest.find_nodes_in_area(minpos, maxpos, "lzr_parrot_npc:parrot_spawner") if #parrot_spawners > 1 then table.insert(errors, "too_many_parrot_spawners") end local hidden_parrot_spawners = minetest.find_nodes_in_area(minpos, maxpos, "lzr_parrot_npc:hidden_parrot_spawner") if #hidden_parrot_spawners > 1 then table.insert(errors, "too_many_hidden_parrot_spawners") end for h=1, #hidden_parrot_spawners do local node = minetest.get_node(hidden_parrot_spawners[h]) local num = (node.param2 % 4) + 1 local parrot_name = lzr_parrot_npc.get_hidden_parrot_name(num) if not parrot_name then table.insert(errors, "bad_hidden_parrot_spawner") break end end local hidden_parrot_spawners = minetest.find_nodes_in_area(minpos, maxpos, "lzr_parrot_npc:hidden_parrot_spawner") if #hidden_parrot_spawners > 1 then table.insert(errors, "too_many_hidden_parrot_spawners") end for h=1, #hidden_parrot_spawners do local node = minetest.get_node(hidden_parrot_spawners[h]) local num = (node.param2 % 4) + 1 local parrot_name = lzr_parrot_npc.get_hidden_parrot_name(num) if not parrot_name then table.insert(errors, "bad_hidden_parrot_spawner") break end end -- Test: Trigger validity check from lzr_triggers local trigger_check_ok, trigger_errors = lzr_triggers.check_triggers(true) if not trigger_check_ok then for e=1, #trigger_errors do table.insert(errors, "lzr_triggers_"..trigger_errors[e]) end end -- Test: Additional editor-specific trigger checks for trigger_id, trigger in pairs(lzr_triggers.get_triggers()) do local trigger_pos = minetest.string_to_pos(trigger_id) -- Out of level bounds if not vector.in_area(trigger_pos, minpos, maxpos) then table.insert(errors, "trigger_out_of_bounds") break end -- Trigger is not at its expected location -- (in the editor, all triggers must have their ID -- match the location) if not vector.equals(trigger_pos, trigger.location) then table.insert(errors, "trigger_moved") break end end if #errors > 0 then return false, errors else return true end end -- Check the level area for non-serious problems lzr_editor.check_level_warnings = function() local warnings = {} local minpos, maxpos = lzr_world.get_level_bounds() -- Test: Laser-incompatible nodes (should have none). -- Will look a bit ugly if a laser touches those. local incompatible = minetest.find_nodes_in_area(minpos, maxpos, "group:laser_incompatible") if #incompatible > 0 then table.insert(warnings, "laser_incompatible") end -- Test: Incompatible door segments local doors = minetest.find_nodes_in_area(minpos, maxpos, "group:door") if #doors > 0 then local half_door, inc_p2, inc_type = false, false, false for d=1, #doors do local dpos = doors[d] local dnode = minetest.get_node(dpos) local ddir = minetest.facedir_to_dir(dnode.param2) -- Only test upright door segments. Door segments that lie flat -- don't trigger warnings. if ddir.y == 0 then local altpos local g = minetest.get_item_group(dnode.name, "door") if g == 1 then altpos = vector.offset(dpos, 0, 1, 0) elseif g == 2 then altpos = vector.offset(dpos, 0, -1, 0) else minetest.log("error", "[lzr_editor] Door node '"..dnode.name.."' has bad door group rating: "..g) break end local altnode = minetest.get_node(altpos) local ga = minetest.get_item_group(altnode.name, "door") if ga == 0 then half_door = true elseif altnode.param2 ~= dnode.param2 then inc_p2 = true else local g_exit = minetest.get_item_group(dnode.name, "door_exit") local ga_exit = minetest.get_item_group(altnode.name, "door_exit") local g_locked = minetest.get_item_group(dnode.name, "door_locked") local ga_locked = minetest.get_item_group(altnode.name, "door_locked") if g_exit ~= ga_exit or g_locked ~= ga_locked then inc_type = true end end end end if half_door then table.insert(warnings, "half_door") end if inc_p2 then table.insert(warnings, "incompatible_door_segments_param2") end if inc_type then table.insert(warnings, "incompatible_door_segments_type") end end -- Test: No treasures to collect (should have at least 1). -- The level is still playable, but it will be an instant win. local no_treasures = true local chests = minetest.find_nodes_in_area(minpos, maxpos, "group:chest") for c=1, #chests do local node = minetest.get_node(chests[c]) local g = minetest.get_item_group(node.name, "chest") -- Count locked and unlocked chests -- Opened chests with gold blocks are not counted here, -- they’re immediately counted as collected when the -- level starts. if g == 1 or g == 2 then no_treasures = false end end if no_treasures then table.insert(warnings, "no_treasures") end if #warnings > 0 then return false, warnings else return true end end minetest.register_chatcommand("editor_check", { description = S("Check current level for problems"), params = "", func = function(name, param) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") elseif lzr_gamestate.is_loading() then return false, S("Can’t do this while loading!") end local check1_ok, errors = lzr_editor.check_level_errors() local result_str = list_warnings_and_errors() if result_str == "" then return true, S("No problems found.") else -- Note: We return true because the *command* itself -- succeeded, it's the level that has errors. return true, S("The following problems were found:").."\n"..result_str end end, }) minetest.register_on_player_receive_fields(function(player, formname, fields) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return elseif lzr_gamestate.is_loading() then minetest.chat_send_player(player:get_player_name(), S("Can’t do this while loading!")) return end if formname ~= "lzr_editor:level_settings" and string.sub(formname, 1, 27) ~= "lzr_editor:select_item_page" then temp_settings_state = {} end local get_current_thing = function(thing_type, selected_thing, thing_getter) local thing_list = "" local current_thing = 1 local things = thing_getter() selected_thing = tonumber(selected_thing) if not selected_thing then return nil end for t=1, #things do if t == selected_thing then return things[t] end end return nil end local pname = player:get_player_name() -- Read settings fields into target table (without validation) local parse_settings_fields = function(target_table, fields) if fields.level_name then target_table.name = fields.level_name end if not target_table.size then target_table.size = {} end target_table.size.x = fields.level_size_x or target_table.size.x target_table.size.y = fields.level_size_y or target_table.size.y target_table.size.z = fields.level_size_z or target_table.size.z target_table.floor = fields.level_floor or target_table.floor target_table.ceiling = fields.level_ceiling or target_table.ceiling target_table.wall = fields.level_wall or target_table.wall if fields.level_npc_goldie then target_table.npc_texts = {} target_table.npc_texts.goldie = fields.level_npc_goldie end target_table.ambient = get_current_thing("ambience", fields.level_ambient, lzr_ambience.get_ambiences) or target_table.ambient target_table.sky = get_current_thing("sky", fields.level_sky, lzr_sky.get_skies) or target_table.sky target_table.weather = get_current_thing("weather", fields.level_weather, lzr_weather.get_weathers) or target_table.weather target_table.backdrop = get_current_thing("backdrop", fields.level_backdrop, lzr_mapgen.get_backdrops) or target_table.backdrop if not target_table.backdrop_pos then target_table.backdrop_pos = {} end target_table.backdrop_pos.x = fields.level_backdrop_pos_x or target_table.backdrop_pos.x or "0" target_table.backdrop_pos.y = fields.level_backdrop_pos_y or target_table.backdrop_pos.y or "0" target_table.backdrop_pos.z = fields.level_backdrop_pos_z or target_table.backdrop_pos.z or "0" end local fix_settings_fields = function(target_table) -- Enforce size bounds for _, axis in pairs({"x","y","z"}) do target_table.size[axis] = math.floor(tonumber(target_table.size[axis]) or lzr_globals.DEFAULT_LEVEL_SIZE[axis]) if target_table.size[axis] > lzr_globals.MAX_LEVEL_SIZE[axis] then target_table.size[axis] = lzr_globals.MAX_LEVEL_SIZE[axis] end if target_table.size[axis] < lzr_globals.MIN_LEVEL_SIZE[axis] then target_table.size[axis] = lzr_globals.MIN_LEVEL_SIZE[axis] end end -- Enforce backdrop limits if level_state.backdrop == "islands" then local mg_edge_min, mg_edge_max = minetest.get_mapgen_edges() mg_edge_max = vector.subtract(mg_edge_max, lzr_globals.MAX_LEVEL_SIZE) local EDGE_BUFFER = 1000 -- make sure the level is far away from the zone boundaries -- enforce we're in the map for _, axis in pairs({"x","y","z"}) do -- Non-numeric values get converted to 0 target_table.backdrop_pos[axis] = math.floor(tonumber(fields["level_backdrop_pos_"..axis]) or 0) target_table.backdrop_pos[axis] = math.min(mg_edge_max[axis]-EDGE_BUFFER-lzr_globals.MAX_LEVEL_SIZE[axis], math.max(mg_edge_min[axis]+EDGE_BUFFER, target_table.backdrop_pos[axis])) end -- enforce that we're in the islands zone if target_table.backdrop_pos.z < lzr_globals.DEEP_OCEAN_Z + EDGE_BUFFER + lzr_globals.MAX_LEVEL_SIZE.z then target_table.backdrop_pos.z = lzr_globals.DEEP_OCEAN_Z + EDGE_BUFFER + lzr_globals.MAX_LEVEL_SIZE.z end end end local merge_settings = function(target_table, merge_table) for k,v in pairs(merge_table) do if type(v) == "table" then target_table[k] = table.copy(v) else target_table[k] = v end end end if formname == "lzr_editor:level_settings" then if not fields.level_cancel and not fields.quit and not fields.level_ok then -- select_item button clicked if fields.level_wall_select or fields.level_ceiling_select or fields.level_floor_select then parse_settings_fields(temp_settings_state, fields) local state = table.copy(level_state) merge_settings(state, temp_settings_state) fix_settings_fields(state) temp_settings_state = state local bname if fields.level_wall_select then bname = "wall" elseif fields.level_ceiling_select then bname = "ceiling" elseif fields.level_floor_select then bname = "floor" end select_item.show_dialog(pname, bname) return end -- Dropdown changed if (fields.level_sky or fields.level_backdrop or fields.level_ambient or fields.level_weather) then parse_settings_fields(temp_settings_state, fields) local state = table.copy(level_state) merge_settings(state, temp_settings_state) fix_settings_fields(level_state) show_settings_dialog(player, state) return end end if fields.level_cancel or (fields.quit and not fields.level_ok) then temp_settings_state = {} return end if fields.level_ok then local old_level_pos = table.copy(lzr_world.get_level_pos()) local old_level_size = table.copy(lzr_world.get_level_size()) local old_floor = level_state.floor local old_ceiling = level_state.ceiling local old_wall = level_state.wall parse_settings_fields(temp_settings_state, fields) parse_settings_fields(level_state, fields) if temp_settings_state then merge_settings(level_state, temp_settings_state) end fix_settings_fields(level_state) lzr_world.set_level_size(level_state.size) if level_state.backdrop == "ocean" then lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_OCEAN)) elseif level_state.backdrop == "islands" then lzr_world.set_level_pos(table.copy(level_state.backdrop_pos)) elseif level_state.backdrop == "underground" then lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_UNDERGROUND)) elseif level_state.backdrop == "sky" then lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_SKY)) end local rebuild_room, relocate_room = false, false local nodes = {} nodes.node_floor = level_state.floor nodes.node_ceiling = level_state.ceiling nodes.node_wall = level_state.wall if old_floor ~= nodes.node_floor or old_ceiling ~= nodes.node_ceiling or old_wall ~= nodes.node_wall then for k,v in pairs(nodes) do if not minetest.registered_nodes[v] then nodes[k] = lzr_globals.DEFAULT_WALL_NODE level_state[string.sub(k, 6)] = lzr_globals.DEFAULT_WALL_NODE end end rebuild_room = true elseif not vector.equals(lzr_world.get_level_size(), old_level_size) then rebuild_room = true end if not vector.equals(lzr_world.get_level_pos(), old_level_pos) then relocate_room = true rebuild_room = true end if relocate_room or rebuild_room then minetest.log("action", "[lzr_editor] Autosaving level on level rebuild") save_level(lzr_globals.AUTOSAVE_NAME, true) end if relocate_room then -- Relocating the room will clear the level. -- TODO: Allow to move the room non-destructively. lzr_levels.reset_level_area(false, old_level_pos, old_level_size) lzr_triggers.reset_triggers() end if rebuild_room then lzr_levels.resize_room(old_level_size, lzr_world.get_level_size(), nodes) lzr_gui.show_level_bounds(player, lzr_world.get_level_pos(), lzr_world.get_level_size()) -- Some trigger nodes might have been removed if the level -- size was reduced, so we must clear some triggers clear_out_of_bounds_triggers() end -- Move player if too far out of new level area move_player_to_level_if_neccessary(player) if lzr_ambience.ambience_exists(level_state.ambient) then lzr_ambience.set_ambience(level_state.ambient) else lzr_ambience.set_ambience("none") end if lzr_sky.sky_exists(level_state.sky) then lzr_sky.set_sky(level_state.sky) else lzr_sky.set_sky(lzr_globals.DEFAULT_SKY) end if lzr_weather.weather_exists(level_state.weather) then lzr_weather.set_weather(level_state.weather) else lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER) end end return -- Select level boundary elseif string.sub(formname, 1, 27) == "lzr_editor:select_item_page" then local rest = string.sub(formname, 28, string.len(formname)) local split = string.split(rest, "%%", true, 2) local page = tonumber(split[1]) local boundaryname = split[2] -- Change page in select_item dialog if page ~= nil then if fields.previous and page > 1 then select_item.show_dialog_page(player:get_player_name(), boundaryname, page - 1) return elseif fields.next then select_item.show_dialog_page(pname, boundaryname, page + 1) return end end local item for field,_ in pairs(fields) do if string.sub(field, 1, 5) == "item_" then item = string.sub(field, 6, string.len(field)) break end end if item then temp_settings_state[boundaryname] = item local state = table.copy(level_state) merge_settings(state, temp_settings_state) show_settings_dialog(player, state) return elseif fields.quit or fields.cancel then select_item.reset_player_info(pname) local state = table.copy(level_state) merge_settings(state, temp_settings_state) show_settings_dialog(player, state) end elseif formname == "lzr_editor:level_save" or formname == "lzr_editor:level_load" then if fields.level_list then local evnt = minetest.explode_textlist_event(fields.level_list) if evnt.type == "CHG" or evnt.type == "DBL" then local file = level_file_textlist_state[evnt.index] if file then if formname == "lzr_editor:level_save" then show_save_load_dialog(player, true, file) else show_save_load_dialog(player, false, file) end return end end elseif (fields.load or (formname == "lzr_editor:level_load" and fields.key_enter)) and fields.file_name then if fields.file_name == "" then minetest.chat_send_player(pname, S("No level name provided.")) return end if check_for_slash(fields.file_name) then minetest.chat_send_player(pname, S("File name must not contain slash or backslash!")) return false end local exists = lzr_util.file_exists(minetest.get_worldpath().."/levels", fields.file_name..".mts") if not exists then minetest.chat_send_player(pname, S("Level file does not exist!")) return end local ok, filename = load_level(fields.file_name, player) if ok then minetest.chat_send_player(pname, S("Level loaded.")) else minetest.chat_send_player(pname, S("Error reading level file!")) end elseif (fields.save or (formname == "lzr_editor:level_save" and fields.key_enter)) and fields.file_name then if fields.file_name == "" then minetest.chat_send_player(pname, S("No level name provided.")) return end if check_for_slash(fields.file_name) then minetest.chat_send_player(pname, S("File name must not contain slash or backslash!")) return false end local ok, filename, filename2 = save_level(fields.file_name) if ok and filename and filename2 then minetest.chat_send_player(pname, S("Level saved to @1 and @2.", filename, filename2)) elseif ok and filename then minetest.chat_send_player(pname, S("Level saved to @1, but could not write metadata to @2.", filename, filename2)) else minetest.chat_send_player(pname, S("Error writing level file!")) end end return end if fields.__lzr_level_editor_exit then exit_editor() return elseif fields.__lzr_level_editor_settings then show_settings_dialog(player) return elseif fields.__lzr_level_editor_save then show_save_load_dialog(player, true, level_state.file_name) return elseif fields.__lzr_level_editor_load then show_save_load_dialog(player, false, level_state.file_name) return elseif fields.__lzr_level_editor_get_item then lzr_getitem.show_formspec(pname) return end end) -- Returns list of all custom level file names, sorted. -- The file name suffix is omitted. lzr_editor.get_custom_levels = function() local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false) local levels = {} for l=1, #levels_files do if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then local name = string.sub(levels_files[l], 1, -5) table.insert(levels, name) end end table.sort(levels) return levels end -- Returns proper level name of a custom level. -- The proper level name is read from a matching CSV file. -- If no such file is found, the empty string is returned. -- * level_name: Level file name without suffix -- * with_fallback: If true, will return "Untitled ()" -- instead of the empty string if the level name is empty or undefined lzr_editor.get_custom_level_name = function(level_name, with_fallback) local csv_file = io.open(minetest.get_worldpath().."/levels/"..level_name..".csv", "r") local pname if csv_file then local csv_string = csv_file:read("*a") csv_file:close() local csv_parsed = lzr_csv.parse_csv(csv_string) if csv_parsed and #csv_parsed >= 1 then pname = csv_parsed[1][2] end end if not pname then pname = "" end if pname == "" and with_fallback then return S("Untitled (@1)", level_name) end return pname end -- Expose invisible blocks in editor mode local invis_display_timer = 0 local INVIS_DISPLAY_UPDATE_TIME = 1 local INVIS_DISPLAY_RANGE = 8 minetest.register_globalstep(function(dtime) local state = lzr_gamestate.get_state() if state ~= lzr_gamestate.EDITOR then return end invis_display_timer = invis_display_timer + dtime if invis_display_timer < INVIS_DISPLAY_UPDATE_TIME then return end invis_display_timer = 0 local player = minetest.get_player_by_name("singleplayer") if not player then return end local pos = vector.round(player:get_pos()) local r = INVIS_DISPLAY_RANGE local vm = minetest.get_voxel_manip() local emin, emax = vm:read_from_map(vector.offset(pos, -r, -r, -r), vector.offset(pos, r, r, r)) local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax}) local data = vm:get_data() for x=pos.x-r, pos.x+r do for y=pos.y-r, pos.y+r do for z=pos.z-r, pos.z+r do local vi = area:index(x,y,z) local nodename = minetest.get_name_from_content_id(data[vi]) local texture -- TODO: Find a way to mark water source barriers. -- (as of MT 5.9.1, particles are invisible when underwater) if minetest.get_item_group(nodename, "rain_membrane") > 0 then texture = "lzr_core_rain_membrane.png" elseif minetest.get_item_group(nodename, "barrier") > 0 then texture = "lzr_core_barrier.png" end if texture then minetest.add_particle({ pos = {x=x, y=y, z=z}, texture = texture, glow = minetest.LIGHT_MAX, size = 8, expirationtime = 1.1, }) end end end end end) minetest.register_chatcommand("editor_clear", { privs = {}, params = "[ clear | regenerate ]", description = S("Remove all blocks in the current level area or regenerate the map"), func = function(name, param) local state = lzr_gamestate.get_state() if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return false, S("Not in editor mode!") elseif lzr_gamestate.is_loading() then return false, S("Can’t do this while loading!") end local pos = lzr_world.get_level_pos() local size = lzr_world.get_level_size() local bounding_nodes = { node_wall = level_state.wall, node_ceiling = level_state.ceiling, node_floor = level_state.floor, } local done = function() lzr_gamestate.set_loading(false) minetest.chat_send_player(name, S("Level cleared.")) end -- Clear level by removing all blocks if param == "" or param == "clear" then minetest.log("action", "[lzr_editor] Autosaving level before clearing it") autosave_level() minetest.log("action", "[lzr_editor] Clearing level ...") -- Clearing the level counts as "loading". This blocks all -- other commands until the operation is complete. lzr_gamestate.set_loading(true) lzr_triggers.reset_triggers() lzr_levels.build_room({pos=pos, size=size, nodes=bounding_nodes, clear=true, clear_border=false, callback_done=done}) return true, S("Clearing level …") -- Clear level by regenerating the map elseif param == "regenerate" then minetest.log("action", "[lzr_editor] Autosaving level before clearing it") autosave_level() minetest.log("action", "[lzr_editor] Clearing level ...") lzr_gamestate.set_loading(true) lzr_triggers.reset_triggers() lzr_levels.reset_level_area(true, pos, size) lzr_levels.build_room({pos=pos, size=size, nodes=bounding_nodes, callback_done=done}) return true, S("Clearing level …") else -- Invalid parameter return false end end, }) -- Show a warning when issuing a WorldEdit command in the editor. -- Using WorldEdit to change blocks can lead to an invalid level state. -- In particular, it might break the trigger definitions minetest.register_on_chatcommand(function(name, command, params) local cdef = minetest.registered_chatcommands[command] if not cdef then return end if lzr_editor.worldedit_warning then return end if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then return end local origin = cdef.mod_origin if cdef.mod_origin == "worldedit" or cdef.mod_origin == "worldedit_commands" or cdef.mod_origin == "worldedit_shortcommands" then local message = S("WARNING: Changing a trigger block with a WorldEdit command may break the triggers. You may need to call /reset_triggers after doing so.") minetest.chat_send_player(name, minetest.colorize("#ffff00", message)) lzr_editor.worldedit_warning = true end end)