2024-12-15 14:20:32 +01:00

1533 lines
54 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
--[[ <<<The file is saved here >>> ]]
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|<unused>|floor|ceiling
-- (the <unused> 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("<level name>"),
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("Cant 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("<level name>"),
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("Cant 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("Cant 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("Cant 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,
-- theyre 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("Cant 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("Cant 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 (<file name>)"
-- 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("Cant 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)