790 lines
26 KiB
Lua

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
-- 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
local INITIAL_EDITOR_ITEMS = {
-- hotbar slots
"lzr_tools:ultra_pickaxe",
"screwdriver2:screwdriver",
"lzr_teleporter:teleporter_off",
"lzr_laser:emitter_on",
"lzr_laser:detector",
"lzr_laser:mirror",
"lzr_laser:crate",
"lzr_treasure:chest_wood_locked",
-- rest of inventory
"lzr_tools:ultra_bucket",
"lzr_laser:emit_toggler",
"lzr_laser:barricade",
"lzr_laser:emitter_takable_on",
"lzr_laser:detector_takable",
"lzr_laser:mirror_takable",
"lzr_laser:crate_takable",
"lzr_treasure:chest_wood_unlocked",
}
lzr_editor = {}
lzr_editor.first_time = true
-- 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.
local INITIAL_LEVEL_STATE = {
name = "",
file_name = "",
size = lzr_globals.DEFAULT_LEVEL_SIZE,
wall = lzr_globals.DEFAULT_WALL_NODE,
window = lzr_globals.DEFAULT_WINDOW_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,
}
local level_state = table.copy(INITIAL_LEVEL_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 = {}
-- Give useful starter items to player
local function give_initial_items(player)
local inv = player:get_inventory()
-- Clear inventory first
inv:set_list("main", {})
-- Add itmes
local errored = false
for i=1, #INITIAL_EDITOR_ITEMS do
local item = INITIAL_EDITOR_ITEMS[i]
local leftover = inv:add_item("main", item)
if not leftover:is_empty() then
minetest.log("error", "[lzr_editor] Could not give initial item "..item.." (leftover="..leftover:to_string().."). "..
"get_size('main')="..inv:get_size("main").."; get_list('main')="..dump(inv:get_list("main")))
errored = true
end
end
if errored then
minetest.log("error", "[lzr_editor] Could not give one or more initial items")
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(level_state.size)
offset = vector.offset(offset, -1, -1, -1)
return vector.in_area(pos, lzr_globals.LEVEL_POS, vector.add(lzr_globals.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
lzr_levels.clear_playfield(level_state.size)
level_state = table.copy(INITIAL_LEVEL_STATE)
local pos = lzr_globals.LEVEL_POS
local size = level_state.size
lzr_levels.build_room({pos=pos, size=size, spawn_pos=pos, yaw=0})
lzr_gui.show_level_bounds(player, pos, size)
lzr_gamestate.set_state(lzr_gamestate.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
minetest.mkdir(minetest.get_worldpath().."/levels")
local filename = minetest.get_worldpath().."/levels/"..level_name..".mts"
local size = vector.subtract(level_state.size, vector.new(1, 1, 1))
-- TODO: Don't save the lasers
local ok = minetest.create_schematic(lzr_globals.LEVEL_POS, vector.add(lzr_globals.LEVEL_POS, size), {}, filename, {})
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_state.wall .. "|" .. level_state.window .. "|" .. level_state.floor .. "|" .. level_state.ceiling,
level_state.ambient,
level_state.sky,
npc_texts_csv,
level_state.weather,
}})
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
-- Exit editor state (if not already outside of editor).
-- Returns true if state changed, false if already out of editor.
local function exit_editor(player)
level_state.file_name = ""
local state = lzr_gamestate.get_state()
if state ~= lzr_gamestate.EDITOR then
return false
else
lzr_levels.go_to_menu()
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!")
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
return true, S("Level saved to @1 and @2.", filename, filename2)
elseif of and filename then
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,
})
-- Returns true if the given file exists, false otherwise.
-- * path: Path to file (without file name)
-- * filename: File name of file (without path)
local file_exists = function(path, filename)
local levels = minetest.get_dir_list(path, false)
for l=1, #levels do
if levels[l] == filename then
return true
end
end
return false
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 = 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")
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, "|")
if exploded_bounds then
level_state.wall = exploded_bounds[1]
level_state.window = exploded_bounds[2]
level_state.floor = exploded_bounds[3]
level_state.ceiling = exploded_bounds[4]
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
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
level_state.size = table.copy(schem.size)
level_state.name = level_state.name or ""
level_state.wall = level_state.wall or lzr_globals.DEFAULT_WALL_NODE
level_state.window = level_state.window or lzr_globals.DEFAULT_WINDOW_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
local bounding_nodes = {
node_wall = level_state.wall,
node_window = level_state.window,
node_ceiling = level_state.ceiling,
node_floor = level_state.floor,
}
lzr_levels.prepare_and_build_custom_level(schem, nil, nil, bounding_nodes)
lzr_gui.show_level_bounds(player, lzr_globals.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
level_state.file_name = level_name
minetest.log("action", "[lzr_editor] Level loaded 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!")
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 = 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
local ok = lzr_editor.enter_editor(player)
if ok then
return true
else
return false, S("Already in level editor!")
end
elseif param == "exit" then
local ok = exit_editor(player)
if ok then
return true
else
return false, S("Not in level editor!")
end
end
return false
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 message = S("Welcome to the Level Editor!").."\n"..
S("See LEVEL_EDITOR.md for instructions.")
lzr_player.set_editor_inventory(player)
lzr_gui.set_editor_gui(player)
lzr_ambience.set_ambience(lzr_ambience.DEFAULT_AMBIENCE)
lzr_sky.set_sky(lzr_globals.DEFAULT_SKY)
lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER)
give_initial_items(player)
lzr_privs.grant_edit_privs(player)
if lzr_editor.first_time then
minetest.chat_send_player("singleplayer", message)
lzr_editor.first_time = false
end
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")
save_level(lzr_globals.AUTOSAVE_NAME, true)
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)
-- 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)
local thing_list = ""
local current_thing = 1
local things = thing_getter()
for t=1, #things do
-- Construct string for dropdown[]
thing_list = thing_list .. F(things[t])
-- Current thing found!
if things[t] == level_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)
local sky_list, current_sky = get_current_thing("sky", lzr_sky.get_skies)
local weather_list, current_weather = get_current_thing("weather", lzr_weather.get_weathers)
-- TODO: Use this string when we have a parrot model
local goldie_speech = NS("Goldie speech")
local form = "formspec_version[4]"..
"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(level_state.name).."]"..
"label[0.5,2.3;Size]"..
"field[1.6,2.3;1,0.6;level_size_x;"..FS("X")..";"..F(tostring(level_state.size.x)).."]"..
"field[2.62,2.3;1,0.6;level_size_y;"..FS("Y")..";"..F(tostring(level_state.size.y)).."]"..
"field[3.63,2.3;1,0.6;level_size_z;"..FS("Z")..";"..F(tostring(level_state.size.z)).."]"..
"field[0.5,3.3;8,0.6;level_wall;"..FS("Wall node")..";"..F(level_state.wall).."]"..
"field[0.5,4.3;8,0.6;level_window;"..FS("Window node")..";"..F(level_state.window).."]"..
"field[0.5,5.3;8,0.6;level_floor;"..FS("Floor node")..";"..F(level_state.floor).."]"..
"field[0.5,6.3;8,0.6;level_ceiling;"..FS("Ceiling node")..";"..F(level_state.ceiling).."]"..
"field[0.5,7.3;8,0.6;level_npc_goldie;"..FS("Information block text")..";"..F(level_state.npc_texts.goldie).."]"..
"label[0.5,8.1;"..FS("Ambience").."]"..
"dropdown[0.5,8.3;3.5,0.6;level_ambient;"..ambient_list..";"..current_ambient..";false]"..
"label[0.5,9.2;"..FS("Sky").."]"..
"dropdown[0.5,9.4;3.5,0.6;level_sky;"..sky_list..";"..current_sky..";false]"..
"label[5,9.2;"..FS("Weather").."]"..
"dropdown[5,9.4;3.5,0.6;level_weather;"..weather_list..";"..current_weather..";false]"..
"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[4]"..
"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
minetest.register_on_player_receive_fields(function(player, formname, fields)
local pname = player:get_player_name()
if formname == "lzr_editor:level_settings" then
if fields.level_cancel or (not fields.level_ok and not fields.key_enter) then
return
end
local invalid = false
if not fields.level_name or not tonumber(fields.level_size_x) or not tonumber(fields.level_size_y) or not tonumber(fields.level_size_z)
or not fields.level_floor or not fields.level_ceiling or not fields.level_window or not fields.level_wall
or not fields.level_ambient or not fields.level_sky or not fields.level_npc_goldie or not fields.level_weather then
return
end
local old_level_size = table.copy(level_state.size)
level_state.name = fields.level_name
level_state.size.x = math.floor(tonumber(fields.level_size_x))
level_state.size.y = math.floor(tonumber(fields.level_size_y))
level_state.size.z = math.floor(tonumber(fields.level_size_z))
for _, axis in pairs({"x","y","z"}) do
if level_state.size[axis] > lzr_globals.PLAYFIELD_SIZE[axis]-1 then
level_state.size[axis] = lzr_globals.PLAYFIELD_SIZE[axis]-1
end
if level_state.size[axis] < lzr_globals.PLAYFIELD_SIZE_MIN[axis]-1 then
level_state.size[axis] = lzr_globals.PLAYFIELD_SIZE_MIN[axis]-1
end
end
local old_floor = level_state.floor
local old_window = level_state.window
local old_ceiling = level_state.ceiling
local old_wall = level_state.wall
level_state.floor = fields.level_floor
level_state.window = fields.level_window
level_state.ceiling = fields.level_ceiling
level_state.wall = fields.level_wall
level_state.ambient = fields.level_ambient
level_state.sky = fields.level_sky
level_state.npc_texts = {}
level_state.npc_texts.goldie = fields.level_npc_goldie
level_state.weather = fields.level_weather
local rebuild_room = false
local nodes = {}
nodes.node_floor = level_state.floor
nodes.node_window = level_state.window
nodes.node_ceiling = level_state.ceiling
nodes.node_wall = level_state.wall
if old_floor ~= nodes.floor or old_window ~= nodes.window or old_ceiling ~= nodes.ceiling or old_wall ~= nodes.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
end
if not vector.equals(level_state.size, old_level_size) then
rebuild_room = true
end
if rebuild_room then
minetest.log("action", "[lzr_editor] Autosaving level on level rebuild")
save_level(lzr_globals.AUTOSAVE_NAME, true)
lzr_levels.resize_room(old_level_size, level_state.size, nodes)
rebuild_room = true
lzr_gui.show_level_bounds(player, lzr_globals.LEVEL_POS, level_state.size)
end
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
return
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 = 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
if nodename == "lzr_core:barrier" then
texture = "lzr_core_barrier.png"
elseif nodename == "lzr_core:rain_membrane" then
texture = "lzr_core_rain_membrane.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)