local S = minetest.get_translator("lzr_level_select") local F = minetest.formspec_escape local FS = function(...) return minetest.formspec_escape(S(...)) end -- Directory separator local DIR_DELIM = "/" lzr_level_select = {} local current_core_level_selection = nil local current_custom_level_selection = nil local current_custom_level_pack_selection = "__singleton" local current_stats_level_pack_selection = "__core" local custom_levels local custom_level_packs = {} local message_form = function(player, message) return "formspec_version[7]size[8,3.5]".. "textarea[0.5,0.5;7,1.8;;;"..F(message).."]".. "button_exit[3,2.5;2,0.6;okay;"..FS("OK").."]" end local update_custom_level_packs = function(skip_core, level_pack_selection) local packnames = lzr_levels.get_level_pack_names() local mypacks = {} custom_level_packs = {} table.insert(custom_level_packs, "__singleton") table.insert(mypacks, FS("Single levels")) local dropdown_idx local pi = 1 -- counted pack index for p=1, #packnames do local packname = packnames[p] if (not skip_core) or packname ~= "__core" then pi = pi + 1 if packname == level_pack_selection then dropdown_idx = pi end table.insert(custom_level_packs, packname) local pack = lzr_levels.get_level_pack(packname) local title = pack.title or packname table.insert(mypacks, minetest.formspec_escape(title)) end end if level_pack_selection == "__singleton" then dropdown_idx = 1 end if not dropdown_idx then dropdown_idx = 1 end return mypacks, dropdown_idx end lzr_level_select.open_dialog = function(player, level_set) local caption if level_set == "core" then caption = S("Select level:") elseif level_set == "custom" then caption = S("Select level pack:") else minetest.log("error", "[lzr_level_select] open_dialog called with unknown level_set!") return false end local form = "formspec_version[7]size[6,10]".. "label[0.5,0.4;"..F(caption).."]".. "button_exit[1.5,8.5;3,1;okay;"..FS("Start").."]" local mypacks local form_levellist if level_set == "custom" then local mypacks, dropdown_idx = update_custom_level_packs(true, current_custom_level_pack_selection) local mypacks_str = table.concat(mypacks, ",") form = form .. "dropdown[0.5,0.7;5,0.6;levelpack;"..mypacks_str..";"..dropdown_idx..";true]" form_levellist = "label[0.5,1.65;"..FS("Select level:").."]".. "tablecolumns[color;text]".. "table[0.5,2;5,5.9;levellist;" else form_levellist = "tablecolumns[color;text]".. "table[0.5,0.8;5,7.5;levellist;" end local list = {} local entry_header = "" local first_uncompleted_level = nil if level_set == "core" then -- Built-in level pack local level_data = lzr_levels.get_level_pack("__core") local completed_levels = lzr_levels.get_completed_levels(level_data) for l=1, #level_data do local filename = level_data[l].filename local level_id = string.sub(filename, 1, -5) if completed_levels[level_id] then entry_header = "#00FF00" else if not first_uncompleted_level then first_uncompleted_level = l end entry_header = "" end table.insert(list, entry_header..","..F(lzr_levels.get_level_name(l, level_data, true))) end if not first_uncompleted_level then first_uncompleted_level = 1 end current_core_level_selection = first_uncompleted_level else if current_custom_level_pack_selection == "__singleton" then -- Pseudo-levelpack containing single unsorted levels (editor levels) local list_levels = lzr_editor.get_custom_levels() custom_levels = {} for l=1, #list_levels do local levelname = list_levels[l] -- Hide the autosave level because it's not meant -- for playing if levelname ~= lzr_globals.AUTOSAVE_NAME then table.insert(custom_levels, levelname) local proper_name = lzr_editor.get_custom_level_name(levelname, true) table.insert(list, entry_header..","..F(proper_name)) end end first_uncompleted_level = 1 current_custom_level_selection = 1 else -- Custom level pack local level_data = lzr_levels.get_level_pack(current_custom_level_pack_selection) local completed_levels = lzr_levels.get_completed_levels(level_data) for l=1, #level_data do local filename = level_data[l].filename local level_id = string.sub(filename, 1, -5) if completed_levels[level_id] then entry_header = "#00FF00" else if not first_uncompleted_level then first_uncompleted_level = l end entry_header = "" end table.insert(list, entry_header..","..F(lzr_levels.get_level_name(l, level_data, true))) end if not first_uncompleted_level then first_uncompleted_level = 1 end current_custom_level_selection = first_uncompleted_level end end if not first_uncompleted_level then first_uncompleted_level = 1 end local list_str = table.concat(list, ",") form_levellist = form_levellist .. list_str .. ";"..first_uncompleted_level.."]" local formname if level_set == "core" then if #list == 0 then form = message_form(player, S("There are no levels.")) else form = form .. form_levellist end formname = "lzr_level_select:levellist" else if #list == 0 then local level_packs = lzr_levels.get_level_pack_names() if #level_packs <= 1 then form = message_form(player, S("There are no level packs or custom single levels.").."\n".. S("Install a level pack or use the level editor to build your own levels.")) elseif current_custom_level_pack_selection == "__singleton" then form = form .. "textarea[0.5,1.7;5,5;;;".. FS("There are no custom single levels.").."\n".. FS("Install one from your friends or use the level editor to build your own levels.").."]n" else form = form .. "textarea[0.5,1.7;5,5;;;".. FS("This level pack is empty.").."]" end else form = form .. form_levellist end formname = "lzr_level_select:levellist_custom" end minetest.show_formspec(player:get_player_name(), formname, form) end local load_custom_level = function(level, player) minetest.log("action", "[lzr_level_select] Player selects custom level: "..level) local csv = minetest.get_worldpath().."/levels/"..level..".csv" local levels_path = minetest.get_worldpath().."/levels" local pname = player:get_player_name() local custom_level_data, error_type, error_msg, error_detail = lzr_levels.analyze_levels(csv, levels_path) if custom_level_data then custom_level_data.is_singleton = true -- Load level with metadata (preferred) lzr_levels.start_level(1, custom_level_data) elseif error_type == "load_error" then -- Fallback when CSV file could not be read custom_level_data = lzr_levels.create_fallback_level_data(level, levels_path) if not custom_level_data then local form = message_form(player, S("Level could not be loaded. No level CSV file was found, and the fallback mechanism failed as well.")) minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form) minetest.log("action", "[lzr_level_select] Player tried to load broken custom level: "..tostring(level)..". Missing CSV file, and the fallback, failed as well.") else custom_level_data.is_singleton = true lzr_levels.start_level(1, custom_level_data) minetest.log("action", "[lzr_level_select] No CSV file found for custom level: "..tostring(level)..". Using fallback.") end elseif error_type == "csv_error" then local form = message_form(player, S("The level could not be loaded. Invalid syntax of level CSV file.")) minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form) minetest.log("action", "[lzr_level_select] Player tried to load broken custom level "..tostring(level)..". Invalid CSV syntax.") elseif error_type == "bad_schematic" then local error_append = "" if error_detail then local reason if error_detail == "file_nonexistant" then reason = S("The level schematic file chould not be found.") elseif error_detail == "no_teleporter" then reason = S("There’s no teleporter for the player to start on.") elseif error_detail == "too_many_teleporters" then reason = S("There’s more than one teleporter.") elseif error_detail == "too_many_parrot_spawners" then reason = S("There’s more than one Goldie the Parrot.") elseif error_detail == "barriers" then reason = S("There’s a barrier or barrier-like node in the level.") elseif error_detail == "gold_block" then reason = S("There’s a bare gold block in the level.") elseif error_detail == "plant_on_ground" then reason = S("There’s a rooted plant in the level.") elseif error_detail == "schematic_load_error" then reason = S("Error while loading schematic file.") else reason = error_detail end --~ Reason shown on level loading error error_append = "\n\n" .. S("Reason: @1", reason) end local form = message_form(player, S("This level is unplayable.")..error_append) minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form) minetest.log("action", "[lzr_level_select] Player tried to load broken custom level "..tostring(level)..". Bad schematic.") else local form = message_form(player, S("The level could not be loaded due to an unknown error.")) minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form) minetest.log("error", "[lzr_level_select] Unknown error while loading custom level: "..tostring(level)..". Aborting.") end end lzr_level_select.open_stats_dialog = function(player) local level_packs_names = lzr_levels.get_level_pack_names() local mypacks, dropdown_idx = update_custom_level_packs(false, current_stats_level_pack_selection) local mypacks_str = table.concat(mypacks, ",") local form = "formspec_version[7]size[6,10]".. "box[0,0;6,0.8;#00000080]".. "label[0.5,0.4;"..FS("Level pack stats").."]".. "label[0.5,1.3;"..FS("Select level pack:").."]".. "dropdown[0.5,1.5;5,0.6;levelpack;"..mypacks_str..";"..dropdown_idx..";true]".. "button_exit[1.5,8.5;3,1;okay;"..FS("Okay").."]".. "box[0.5,2.3;5,5.9;#00000020]".. "hypertext[0.5,2.3;5,5.9;info;" local info local color = "#00ff00" local H = minetest.hypertext_escape if current_stats_level_pack_selection == "__singleton" then local count = #lzr_editor.get_custom_levels() local path = ""..DIR_DELIM.."levels" info = ""..H(S("Single levels")).."\n".. ""..H(S("Description")).."\n".. H(S("Single, unsorted levels that don’t belong to any level pack.")).."\n".. ""..H(S("Stats")).."\n".. --~ @1 = number of levels H(S("• Levels: @1", count)).."\n\n".. ""..H(S("File location")).."\n".. H(S("Single levels are stored in:")).."\n".. ""..H(path).."" else local level_data = lzr_levels.get_level_pack(current_stats_level_pack_selection) local treasures_total = lzr_levels.count_total_treasures(level_data) local treasures_found = lzr_levels.count_total_collected_treasures(level_data) local treasures_percent = string.format("%d", (treasures_found / treasures_total) * 100) local levels_total = #level_data local levels_completed = lzr_levels.count_completed_levels(level_data) local levels_percent = string.format("%d", (levels_completed / levels_total) * 100) local title = level_data.title or level_data.name local pack_description = level_data.description or S("(no description provided)") info = ""..H(title).."\n".. ""..H(S("Description")).."\n".. H(pack_description).."\n".. ""..H(S("Stats")).."\n".. --~ @1 = total number of levels, @2 = number of completed levels, @3 percentage of level completion H(S("• Levels: @1 (@2 completed, @3%)", levels_total, levels_completed, levels_percent)).."\n".. --~ @1 = total number of gold blocks, @2 = number of found gold blocks, @3 percentage of found gold blocks H(S("• Gold blocks: @1 (@2 found, @3%)", treasures_total, treasures_found, treasures_percent)).."\n\n".. ""..H(S("Mod")).."\n".. ""..H(level_data.mod_origin).."" local privs = minetest.get_player_privs(player:get_player_name()) if minetest.settings:get_bool("lzr_debug", true) or privs.server or privs.debug then info = info .."\n".. --~ For the technical level pack identifier ""..H(S("Level pack ID")).."\n".. ""..H(level_data.name).."" end end form = form .. F(info) .. "]" minetest.show_formspec(player:get_player_name(), "lzr_level_select:stats", form) end minetest.register_on_player_receive_fields(function(player, formname, fields) if formname == "lzr_level_select:levellist" then if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then return end local level_data = lzr_levels.get_level_pack("__core") if fields.okay then if current_core_level_selection then lzr_levels.start_level(current_core_level_selection, level_data) end elseif fields.levellist then local expl = minetest.explode_table_event(fields.levellist) if expl.type == "CHG" then current_core_level_selection = expl.row elseif expl.type == "DCL" then current_core_level_selection = expl.row lzr_levels.start_level(current_core_level_selection, level_data) minetest.close_formspec(player:get_player_name(), "lzr_level_select:levellist") elseif expl.type == "INV" then current_core_level_selection = nil end end elseif formname == "lzr_level_select:levellist_custom" then if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then return end if fields.okay then if custom_levels and current_custom_level_selection then if current_custom_level_pack_selection == "__singleton" then local level = custom_levels[current_custom_level_selection] if level then load_custom_level(level, player) end else local level_data = lzr_levels.get_level_pack(current_custom_level_pack_selection) lzr_levels.start_level(current_custom_level_selection, level_data) end end elseif fields.levellist then local expl = minetest.explode_table_event(fields.levellist) if expl.type == "CHG" then current_custom_level_selection = expl.row elseif expl.type == "DCL" then current_custom_level_selection = expl.row if current_custom_level_pack_selection == "__singleton" then if custom_levels then local level = custom_levels[current_custom_level_selection] if level then load_custom_level(level, player) end end else local level_data = lzr_levels.get_level_pack(current_custom_level_pack_selection) lzr_levels.start_level(current_custom_level_selection, level_data) end minetest.close_formspec(player:get_player_name(), "lzr_level_select:levellist_custom") elseif expl.type == "INV" then current_custom_level_selection = nil end elseif fields.levelpack then local idx = tonumber(fields.levelpack) current_custom_level_pack_selection = custom_level_packs[idx] lzr_level_select.open_dialog(player, "custom") end elseif formname == "lzr_level_select:stats" then if fields.levelpack then local idx = tonumber(fields.levelpack) current_stats_level_pack_selection = custom_level_packs[idx] if not fields.quit then lzr_level_select.open_stats_dialog(player) end end end end) minetest.register_chatcommand("level", { privs = { server = true }, description = S("Go to level"), params = S("[] "), func = function(name, param) if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then return false, S("Not possible during the level solution test!") end if lzr_gamestate.is_loading() then return false, S("Can’t start a level while loading!") end -- Parse arguments. level argument may be relative (tilde notation) local pack, level = string.match(param, "([a-zA-Z0-9_]+) (~?-?[0-9]*)") local current_data = lzr_levels.get_current_level_data() local current_pack if current_data then current_pack = current_data.name end -- Select level pack. Defaults to the current level pack -- if playing one or __core if playing no level pack -- (singleton levels don't count) if not pack then if current_pack then pack = current_pack else pack = "__core" end level = string.match(param, "(~?-?[0-9]*)") end if not level then return false end -- Handle relative level argument (e.g. "~1" to move to the next level) local is_relative_level = string.sub(level, 1, 1) == "~" if is_relative_level then local current_level = lzr_levels.get_current_level() -- Relative level argument is only allowed if playing in a level -- and the chosen level pack is the same as the current one. if not current_level then return false, S("Level argument can’t be relative when not playing in a level.") end if current_pack ~= pack then return false, S("Level argument can’t be relative when choosing a different level pack.") end level = minetest.parse_relative_number(level, current_level) else level = tonumber(level) end if not level then return false end local level_data = lzr_levels.get_level_pack(pack) if not level_data then return false, S("Level pack “@1” doesn’t exist.", pack) end if level < 1 or level > #level_data then --~ @1, @2, and @3 are level numbers return false, S("Level @1 does not exist in this level pack. Available levels range from @2 to @3.", level, 1, #level_data) end lzr_levels.start_level(level, level_data) return true end, }) minetest.register_chatcommand("level_pack_stats", { description = S("Display stats about the installed level packs"), params = "", func = function(name, param) local player = minetest.get_player_by_name(name) if not player then return false, S("No player.") end lzr_level_select.open_stats_dialog(player) return true end, })