1985 lines
76 KiB
Lua
1985 lines
76 KiB
Lua
gamedata = {
|
|
|
|
}
|
|
|
|
local http = minetest.get_http_api()
|
|
|
|
-- Used for brevity when making fullscreen things
|
|
window = minetest.get_window_info()
|
|
size = window.max_formspec_size
|
|
dpi = {
|
|
x = window.size.x /size.x,
|
|
y = window.size.y /size.y
|
|
}
|
|
|
|
-- Default textures
|
|
default_textures = minetest.get_texturepath_share().."/base/pack/"
|
|
|
|
-- FIXME: Assets
|
|
assets = os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"
|
|
|
|
local version = minetest.get_version()
|
|
|
|
-- This is where all view-specific state information goes.
|
|
state = {}
|
|
|
|
local fe = minetest.formspec_escape
|
|
local hte = minetest.hypertext_escape
|
|
|
|
function include(file)
|
|
-- FIXME: Use get_builtin_path()
|
|
dofile(os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"..file)
|
|
end
|
|
|
|
dofile(minetest.get_builtin_path().."common/settings/settingtypes.lua")
|
|
|
|
|
|
-- Theme
|
|
|
|
theme = {
|
|
text_color = "#aaa",
|
|
muted_text_color = "#777",
|
|
code_color = "#da8",
|
|
link_color = "#8ad",
|
|
mod_label_color = "#888",
|
|
game_label_color = "#888",
|
|
texture_pack_label_color = "#888",
|
|
|
|
bg = "",
|
|
|
|
modal_bg = "#0008",
|
|
|
|
syntax_keyword = "#d8d",
|
|
syntax_keyword_value = "#79e",
|
|
syntax_identifier = "#adf",
|
|
syntax_operator = "#f66",
|
|
syntax_string = "#da8",
|
|
syntax_number = "#cdb",
|
|
syntax_comment = "#777",
|
|
syntax_placeholder = "#ccc",
|
|
}
|
|
|
|
--End theme
|
|
|
|
|
|
---[[
|
|
|
|
minetest.set_formspec_prepend("\
|
|
style[*;textcolor=#aaa]\
|
|
style_type[field;border=false]\
|
|
style_type[pwdfield;border=false]\
|
|
style_type[image_button;border=false]\
|
|
style_type[button;border=false;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
|
|
style_type[button:hovered;border=false;bgimg="..assets.."btn_bg_2_hover.png;bgimg_middle=8,8]\
|
|
style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0]\
|
|
")
|
|
|
|
local meta_header = "formspec_version[8]\
|
|
size["..size.x..","..size.y.."]\
|
|
padding[0,0]\
|
|
bgcolor[#000;true;#151618]\
|
|
style_type[box;bordercolors=#124722;borderwidths=-5;colors=#151618]\
|
|
image[0,"..(size.y *0.08)..");"..size.x..","..(size.x *(72/672))..";"..default_textures.."menu_header.png]\
|
|
"
|
|
-- box[0,0;"..size.x..","..size.y..";]\
|
|
|
|
local game_header = "formspec_version[8]\
|
|
size["..size.x..","..size.y.."]\
|
|
padding[0,0]\
|
|
bgcolor[#0000;true;#0000]\
|
|
"
|
|
local servers_header = "formspec_version[8]\
|
|
size["..size.x..","..size.y.."]\
|
|
padding[0,0]\
|
|
bgcolor[#0000;true;#151618]\
|
|
"
|
|
|
|
local content_header = "formspec_version[8]\
|
|
size["..size.x..","..size.y.."]\
|
|
padding[0,0]\
|
|
bgcolor[#0000;true;#151618]\
|
|
"
|
|
|
|
local default_game_menu = [[
|
|
<meta>
|
|
enable_clouds = true
|
|
</meta>
|
|
<main>
|
|
@if:@selected_world:fi2
|
|
@set:list_width:@WIDTH * 0.3
|
|
@else:fi2
|
|
@set:list_width:@WIDTH * 0.8
|
|
@endif:fi2
|
|
|
|
label[0,0;{{ selected_world }}]
|
|
|
|
image[${@WIDTH * 0.1 +-0.1},${@HEIGHT * 0.1 +-0.1};${@WIDTH * 0.8 + 0.2},${@HEIGHT * 0.8 + 0.2};$DEFAULT_ASSET_PATH/bg_translucent.png;8,8]
|
|
scroll_container[${@WIDTH * 0.1},${@HEIGHT * 0.1};${@list_width},${@HEIGHT * 0.8};worldscroll;vertical;;0,0]
|
|
@foreach:@WORLDS:worlds
|
|
@if:@i % 2:sel
|
|
style[.select_world_${@path};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
style[.select_world_${@path}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
style[.select_world_${@path}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
@else:sel
|
|
style[.select_world_${@path};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
style[.select_world_${@path}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
style[.select_world_${@path}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
@endif:sel
|
|
button[0,${(@i +-1) * 0.5};${@list_width},0.5;.select_world_${@path};${@name}]
|
|
@endforeach:worlds
|
|
scroll_container_end[]
|
|
|
|
@if:@selected_world:fi
|
|
box[${@WIDTH * 0.4 +-0.05},${@HEIGHT * 0.1 +-0.1};0.1,${@HEIGHT * 0.8 + 0.2};#292d2fff]
|
|
scroll_container[${@WIDTH * 0.4 + 0.05},${@HEIGHT * 0.1};${@WIDTH * 0.8 +-0.05},${@HEIGHT * 0.8};worlconfigscroll;vertical;;0,0]
|
|
@set:j:0
|
|
|
|
@if:@setting_damage:fii
|
|
@if:@damage_enabled:fiii
|
|
image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_filled.png;.set_damage_enabled_to_0;]
|
|
@else:fiii
|
|
image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_empty.png;.set_damage_enabled_to_1;]
|
|
@endif:fiii
|
|
label[0.5,${@j + 0.25};Damage]
|
|
@set:j:@j + 0.5
|
|
@else:fii
|
|
|
|
@endif:fii
|
|
|
|
@if:@setting_creative:fii
|
|
@if:@creative_enabled:fiii
|
|
image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_filled.png;.set_creative_enabled_to_0;]
|
|
@else:fiii
|
|
image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_empty.png;.set_creative_enabled_to_1;]
|
|
@endif:fiii
|
|
label[0.5,${@j + 0.25};Creative]
|
|
@set:j:@j + 0.5
|
|
@else:fii
|
|
|
|
@endif:fii
|
|
|
|
@set:play_str:Play
|
|
|
|
@if:@setting_server:fii
|
|
@if:@server_enabled:fiii
|
|
image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_filled.png;.set_server_enabled_to_0;]
|
|
@set:play_str:Host server
|
|
@else:fiii
|
|
image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_empty.png;.set_server_enabled_to_1;]
|
|
@set:play_str:Play
|
|
@endif:fiii
|
|
|
|
label[0.5,${@j + 0.25};Server]
|
|
@set:j:@j + 0.5
|
|
@else:fii
|
|
|
|
@endif:fii
|
|
|
|
@set:j:@j + 1
|
|
|
|
button[${@WIDTH * 0.05},${@j};${@WIDTH * 0.4},0.75;.overlay_dialog_modconfig;Configure mods]
|
|
button[${@WIDTH * 0.05},${@j + 1};${@WIDTH * 0.4},0.75;.play;${@play_str}]
|
|
|
|
scroll_container_end[]
|
|
scrollbar[-800,6;0,2;vertical;worldconfigscroll;]
|
|
@else:fi
|
|
|
|
@endif:fi
|
|
scrollbaroptions[arrows=hide]
|
|
scrollbar[-800,6;0,2;vertical;worldscroll;]
|
|
</main>
|
|
<modconfig>
|
|
image[${@WIDTH * 0.05 +-0.1},${@HEIGHT * 0.05 +-0.1};${@WIDTH * 0.4 + 0.2},${@HEIGHT * 0.9 +-0.75};$DEFAULT_ASSET_PATH/btn_bg.png;8,8]
|
|
hypertext[${@WIDTH * 0.05},${@HEIGHT * 0.05};${@WIDTH * 0.4},0.75;;<global valign=middle halign=center><b>Available mods</b>]
|
|
scroll_container[${@WIDTH * 0.05},${@HEIGHT * 0.05 + 0.75};${@WIDTH * 0.4},${@HEIGHT * 0.9 +-1.7};modsscroll;vertical;;0,0]
|
|
@foreach:@MODS:m
|
|
@if:@i % 2:sel
|
|
style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
@else:sel
|
|
style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
@endif:sel
|
|
button[0,${@i * 0.5 +-0.5};${@WIDTH * 0.4},0.5;.select_mod_${@name};${@name}]
|
|
@endforeach:m
|
|
scroll_container_end[]
|
|
scrollbar[-800,6;0,2;vertical;modsscroll;]
|
|
|
|
image[${@WIDTH * 0.55 +-0.1},${@HEIGHT * 0.05 +-0.1};${@WIDTH * 0.4 + 0.2},${@HEIGHT * 0.9 +-0.75};$DEFAULT_ASSET_PATH/btn_bg.png;8,8]
|
|
hypertext[${@WIDTH * 0.55},${@HEIGHT * 0.05};${@WIDTH * 0.4},0.75;;<global valign=middle halign=center><b>Mods for ${@selected_world_name}</b>]
|
|
scroll_container[${@WIDTH * 0.55},${@HEIGHT * 0.05 + 0.75};${@WIDTH * 0.4},${@HEIGHT * 0.9 +-1.7};worldmodsscroll;vertical;;0,0]
|
|
@foreach:@WORLDMODS:wm
|
|
@if:@game_mod:gm
|
|
@if:@i % 2:sel
|
|
style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
|
|
@else:sel
|
|
style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
|
|
@endif:sel
|
|
button[0,${@i * 0.5 +-0.5};${@WIDTH * 0.4},0.5;.select_mod_${@name};${@name}]
|
|
@else:gm
|
|
|
|
@endif:gm
|
|
@endforeach:wm
|
|
scroll_container_end[]
|
|
scrollbar[-800,6;0,2;vertical;worldmodsscroll;]
|
|
|
|
image_button[${@WIDTH * 0.45},${@HEIGHT * 0.5 +-(@WIDTH * 0.1)};${@WIDTH * 0.1},${@WIDTH * 0.1};$DEFAULT_ASSET_PATH/arrow_right.png;.add_mod_to_world;]
|
|
image_button[${@WIDTH * 0.45},${@HEIGHT * 0.5};${@WIDTH * 0.1},${@WIDTH * 0.1};$DEFAULT_ASSET_PATH/arrow_left.png;.remove_mod_from_world;]
|
|
tooltip[.add_mod_to_world;Add mod to world;#444;#aaa]
|
|
tooltip[.remove_mod_from_world;Remove mod from world;#444;#aaa]
|
|
|
|
button[${@WIDTH * 0.1},${@HEIGHT * 0.95 +-0.75};${@WIDTH * 0.4},0.75;.unoverlay_dialog;Cancel]
|
|
button[${@WIDTH * 0.5},${@HEIGHT * 0.95 +-0.75};${@WIDTH * 0.4},0.75;.unoverlay_dialog;Confirm]
|
|
</modconfig>
|
|
<addworld>
|
|
label[2,2;Add World, asset path is $ASSET_PATH]
|
|
button[2,3;3,1;.show_dialog_main;back]
|
|
|
|
@foreach:[hello,there,bob]:myloop
|
|
label[6,${@i + 3};${@item}]
|
|
@endforeach:myloop
|
|
</addworld>
|
|
]]
|
|
|
|
|
|
function core.on_before_close()
|
|
--minetest.settings:write()
|
|
end
|
|
|
|
|
|
minetest.async_jobs = {}
|
|
|
|
local function handle_job(jobid, serialized_retval)
|
|
local retval = minetest.deserialize(serialized_retval)
|
|
assert(type(minetest.async_jobs[jobid]) == "function")
|
|
minetest.async_jobs[jobid](retval)
|
|
minetest.async_jobs[jobid] = nil
|
|
end
|
|
|
|
minetest.async_event_handler = handle_job
|
|
|
|
function minetest.handle_async(func, parameter, callback)
|
|
-- Serialize parameters
|
|
local serialized_param = minetest.serialize(parameter)
|
|
|
|
if serialized_param == nil then
|
|
return false
|
|
end
|
|
|
|
local jobid = minetest.do_async_callback(func, serialized_param)
|
|
|
|
minetest.async_jobs[jobid] = callback
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- MARK: - Helpers
|
|
|
|
local function readable_content_type(type)
|
|
if type == "game" then
|
|
return "Game"
|
|
elseif type == "mod" then
|
|
return "Mod"
|
|
elseif type == "modpack" then
|
|
return "Mod collection"
|
|
elseif type == "txp" then
|
|
return "Texture pack"
|
|
else
|
|
return "Unknown"
|
|
end
|
|
end
|
|
|
|
-- Returns the contents of the file, or nil in case of failure.
|
|
local function read_file(path)
|
|
local f = io.open(path)
|
|
if f then
|
|
local data = f:read("a")
|
|
f:close()
|
|
if data == "" then return end
|
|
return data
|
|
end
|
|
end
|
|
|
|
-- Check if a file exists.
|
|
local function file_exists(path)
|
|
local f = io.open(path)
|
|
if f then
|
|
f:close()
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Returns the get_games() entry corresponding to a given game ID.
|
|
local function get_game_info(game)
|
|
for _, x in ipairs(minetest.get_games()) do
|
|
if x.id == game then
|
|
return x
|
|
end
|
|
end
|
|
end
|
|
|
|
local function get_world_index(path)
|
|
for i, x in ipairs(minetest.get_worlds()) do
|
|
if x.path == path then
|
|
return i
|
|
end
|
|
end
|
|
return -1
|
|
end
|
|
|
|
function get_worlds_for_game(id)
|
|
local out = {}
|
|
for _, x in ipairs(minetest.get_worlds()) do
|
|
if x.gameid == id then
|
|
out[#out +1] = x
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
function get_mods_for_game(id)
|
|
local out = {}
|
|
for _, x in ipairs(get_all_mods()) do
|
|
local conf = Settings(x.path.."mod.conf")
|
|
local unsupported_games = conf:get("unsupported_games")
|
|
if unsupported_games then
|
|
if table.indexof(unsupported_games:trim():split(","), id) == -1 then
|
|
out[#out +1] = x
|
|
end
|
|
else
|
|
local supported_games = conf:get("supported_games")
|
|
if supported_games then
|
|
if table.indexof(supported_games:trim():split(","), id) ~= -1 then
|
|
out[#out +1] = x
|
|
end
|
|
else
|
|
out[#out +1] = x
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function get_game_mods(id)
|
|
local out = {}
|
|
local base = get_game_info(id).path.."/mods/"
|
|
for _, x in pairs(minetest.get_dir_list(base, true)) do
|
|
out[#out +1] = {
|
|
name = x,
|
|
path = base..x
|
|
}
|
|
end
|
|
return out
|
|
end
|
|
|
|
function get_mods_for_world(world)
|
|
local conf = Settings(world.."/world.mt")
|
|
local mods = {}
|
|
local game
|
|
local game_mods = {}
|
|
for k, v in pairs(conf:to_table()) do
|
|
if k == "gameid" then
|
|
game = v
|
|
elseif k:sub(1, 9) == "load_mod_" then
|
|
if v ~= "false" and v ~= "nil" and v then
|
|
mods[k:sub(10)] = minetest.get_user_path().."/"..v
|
|
end
|
|
end
|
|
end
|
|
if game then
|
|
for _, x in ipairs(get_game_mods(game)) do
|
|
game_mods[x.name] = x.path
|
|
end
|
|
end
|
|
local config = minetest.check_mod_configuration(world, mods)
|
|
-- print(dump(config))
|
|
for _, x in ipairs(config.unsatisfied_mods) do
|
|
x.unsatisfied = true
|
|
config.satisfied_mods[#config.satisfied_mods +1] = x
|
|
end
|
|
if game then
|
|
for _, x in ipairs(config.satisfied_mods) do
|
|
if game_mods[x.name] then
|
|
x.game_provided = true
|
|
end
|
|
end
|
|
end
|
|
return config.satisfied_mods
|
|
end
|
|
|
|
-- Returns a list of content available for download.
|
|
function get_available_content()
|
|
local version = minetest.get_version()
|
|
local cdb = minetest.settings:get("contentdb_url")
|
|
local url = cdb.."/api/packages/?type=mod&type=game&type=txp&protocol_version="..minetest.get_max_supp_proto().."&engine_version="..minetest.urlencode(version.string)
|
|
|
|
for _, x in pairs(minetest.settings:get("contentdb_flag_blacklist"):split(",")) do
|
|
x = x:trim()
|
|
if x ~= "" then
|
|
url = url.."&hide="..minetest.urlencode(x)
|
|
end
|
|
end
|
|
|
|
local languages
|
|
local lang = minetest.get_language()
|
|
if lang ~= "" then
|
|
languages = { lang, "en;q=0.8" }
|
|
else
|
|
languages = { "en" }
|
|
end
|
|
|
|
local http = minetest.get_http_api()
|
|
local response = http.fetch_sync({
|
|
url = url,
|
|
extra_headers = {
|
|
"Accept-Language: "..table.concat(languages, ", ")
|
|
},
|
|
})
|
|
if not response.succeeded then
|
|
return
|
|
end
|
|
local items = minetest.parse_json(response.data)
|
|
local out = {}
|
|
for _, x in pairs(items) do
|
|
out[#out +1] = x
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Returns a list of all the installed mods.
|
|
function get_all_mods(dir)
|
|
local all = false
|
|
if not dir then
|
|
if state.all_mods then return state.all_mods end
|
|
dir = minetest.get_modpath()
|
|
all = true
|
|
end
|
|
local out = {}
|
|
for _, x in ipairs(minetest.get_dir_list(dir, true)) do
|
|
local info = minetest.get_content_info(dir.."/"..x)
|
|
if info.type == "mod" or info.type == "modpack" then
|
|
out[#out +1] = info
|
|
end
|
|
end
|
|
if all then
|
|
state.all_mods = out
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Returns a list of all the installed games.
|
|
function get_all_games(dir)
|
|
local all = false
|
|
if not dir then
|
|
if state.all_games then return state.all_games end
|
|
dir = minetest.get_gamepath()
|
|
all = true
|
|
end
|
|
local out = {}
|
|
for _, x in ipairs(minetest.get_dir_list(dir, true)) do
|
|
local info = minetest.get_content_info(dir.."/"..x)
|
|
if info.type == "game" then
|
|
out[#out +1] = info
|
|
end
|
|
end
|
|
if all then
|
|
state.all_games = out
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Returns a list of all the installed texture packs.
|
|
function get_all_texture_packs(dir)
|
|
local all = false
|
|
if not dir then
|
|
if state.all_texture_packs then return state.all_texture_packs end
|
|
dir = minetest.get_texturepath()
|
|
all = true
|
|
end
|
|
local out = {}
|
|
for _, x in ipairs(minetest.get_dir_list(dir, true)) do
|
|
local info = minetest.get_content_info(dir.."/"..x)
|
|
if info.type == "txp" then
|
|
out[#out +1] = info
|
|
end
|
|
end
|
|
if all then
|
|
state.all_texture_packs = out
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Return a list of all content, as a conglomeration of the lists of mods, games, and texture packs.
|
|
function get_all_content()
|
|
if not state.all_content then
|
|
state.all_content = {}
|
|
table.insert_all(state.all_content, get_all_mods())
|
|
table.insert_all(state.all_content, get_all_games())
|
|
table.insert_all(state.all_content, get_all_texture_packs())
|
|
table.sort(state.all_content, function(a, b)
|
|
if not a then
|
|
return b ~= nil
|
|
elseif not b then
|
|
return a == nil
|
|
end
|
|
return a.name < b.name
|
|
end)
|
|
end
|
|
return state.all_content
|
|
end
|
|
|
|
-- Parses text as .conf format.
|
|
function parse_conf_text(txt)
|
|
local out = {}
|
|
local multiline = false
|
|
local key = ""
|
|
local value = ""
|
|
for line in (txt.."\n"):gmatch("(.*)\n") do
|
|
if multiline then
|
|
if line:find("\"\"\"") then
|
|
line = line:gsub("\"\"\"", ""):trim()
|
|
multiline = false
|
|
out[key] = value.."\n"..line
|
|
key = ""
|
|
value = ""
|
|
else
|
|
value = value.."\n"..line:trim()
|
|
end
|
|
else
|
|
local k, v = line:match("(.-)=(.*)")
|
|
if not k then break end
|
|
if v:find("\"\"\"") then
|
|
key = k:trim()
|
|
value = v:gsub("\"\"\"", ""):trim()
|
|
multiline = true
|
|
else
|
|
out[k:trim()] = v:trim()
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function markdown_to_hypertext_format(md)
|
|
return md:gsub("\n%s*####%s*([^\n]-)\n", "\n<b>%1</b>\n")
|
|
:gsub("\n%s*###%s*([^\n]-)\n", "\n<big>%1</big>\n")
|
|
:gsub("([^\n]-)\n%-+\n", "\n<big>%1</big>\n")
|
|
:gsub("\n%s*##%s*([^\n]-)\n", "\n<bigger>%1</bigger>\n")
|
|
:gsub("\n%s*#%s*([^\n]-)\n", "\n<style size=48>%1</style>\n")
|
|
:gsub("%*%*([^`\n]-)%*%*", "<b>%1</b>")
|
|
:gsub("%*([^`\n]-)%*", "<i>%1</i>")
|
|
:gsub("__([^`\n]-)__", "<b>%1</b>")
|
|
:gsub("\n_([^`\n]-)_", "<i>%1</i>")
|
|
:gsub("%*%s+(.-)\n", "• %1\n")
|
|
-- :gsub("```%a-\n(.-)```", "<mono><style color="..theme.code_color..">%1</style></mono>")
|
|
-- :gsub("`([^`]-)`", "<mono><style color="..theme.code_color..">%1</style></mono>")
|
|
-- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them.
|
|
:gsub("%[?!%[[^%]]-%]%([^\n]+%)", "")
|
|
end
|
|
|
|
|
|
local keywords = {
|
|
"and", "break", "do", "else", "elseif", "end", --[["false",]] "for", "function",
|
|
"goto", "if", "in", "local", --[["nil",]] "not", "or", "repeat", "return",
|
|
"then", --[["true",]] "until", "while"
|
|
}
|
|
local keyword_set = {}
|
|
for _, kw in ipairs(keywords) do
|
|
keyword_set[kw] = true
|
|
end
|
|
|
|
local keyword_value_set = {
|
|
["true"] = true,
|
|
["false"] = true,
|
|
["nil"] = true,
|
|
["math.huge"] = true
|
|
}
|
|
|
|
local function colorize(str, color)
|
|
return "<style color="..(theme["syntax_"..color] or "#fff")..">"..str.."</style>"
|
|
end
|
|
|
|
local function syntax_highlight(code)
|
|
local out = ""
|
|
local pos = 1
|
|
while pos <= #code do
|
|
local matched = false
|
|
|
|
-- Whitespace
|
|
local ws_start, ws_end = code:find("^%s+", pos)
|
|
if ws_start then
|
|
out = out..colorize(code:sub(ws_start, ws_end), "whitespace")
|
|
pos = ws_end + 1
|
|
matched = true
|
|
end
|
|
|
|
-- Single-line comment
|
|
if not matched then
|
|
local com_start, com_end = code:find("^%-%-[^\n]*", pos)
|
|
if com_start then
|
|
out = out..colorize(code:sub(com_start, com_end), "comment")
|
|
pos = com_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- String (double-quoted, simple escape handling)
|
|
if not matched then
|
|
local str_start, str_end = code:find('^"[^"]*"', pos)
|
|
if str_start then
|
|
out = out..colorize(code:sub(str_start, str_end), "string")
|
|
pos = str_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- String (single-quoted, similar)
|
|
if not matched then
|
|
local str_start, str_end = code:find("^'[^']*", pos)
|
|
if str_start then
|
|
out = out..colorize(code:sub(str_start, str_end), "string")
|
|
pos = str_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- Documentation placeholders. These aren't in Lua, but that means I
|
|
-- can highlight them without worrying about confusing alternative meanings.
|
|
if not matched then
|
|
local str_start, str_end = code:find('^\\?<[^>]*>', pos)
|
|
if str_start then
|
|
out = out..colorize(code:sub(str_start, str_end), "placeholder")
|
|
pos = str_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- Number (basic integer/float)
|
|
if not matched then
|
|
local num_start, num_end = code:find("^%d+%.?%d*[eE]?[+-]?%d*", pos)
|
|
if num_start then
|
|
out = out..colorize(code:sub(num_start, num_end), "number")
|
|
pos = num_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- Identifier/Keyword
|
|
if not matched then
|
|
local id_start, id_end, id_text = code:find("^([%a_][%w_]*)", pos)
|
|
if id_start then
|
|
if keyword_set[id_text] then
|
|
out = out..colorize(id_text, "keyword")
|
|
elseif keyword_value_set[id_text] then
|
|
out = out..colorize(id_text, "keyword_value")
|
|
else
|
|
out = out..colorize(id_text, "identifier")
|
|
end
|
|
pos = id_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- Operators/Punctuation (catch-all for non-alphanumeric)
|
|
if not matched then
|
|
local op_start, op_end = code:find("^[^%s%a%d_\"'{}(),%][]", pos)
|
|
if op_start then
|
|
local op = code:sub(op_start, op_end)
|
|
out = out..colorize(op, op == "-" and code:sub(op_end +1, op_end +2):find "^%d" and "number" or "operator")
|
|
pos = op_end + 1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
-- If nothing matched, advance to avoid infinite loop (e.g., invalid char)
|
|
if not matched then
|
|
out = out..colorize(code:sub(pos, pos), "generic")
|
|
pos = pos + 1
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function markdown_to_hypertext(md)
|
|
md = ("\n"..hte(md or "").."\n")
|
|
local out = ""
|
|
-- The following is necessary because code blocks should not have formatting applied to their contents.
|
|
local offset = 1
|
|
while true do
|
|
local block_start, block_end, lang, block = md:find("```(%a*)\n(.-)```", offset)
|
|
local code_start, code_end, code = md:find("[^`]`([^`\n]+)`", offset)
|
|
if not block_start and not code_start then break end
|
|
if block_start and block_start < (code_start or math.huge) then
|
|
-- Don't apply syntax highlighting to blocks annotated as a language other than Lua.
|
|
-- However, assume that blocks with no annotation are probably Lua.
|
|
if lang == "lua" then
|
|
block = syntax_highlight(block)
|
|
end
|
|
-- The block:gsub hack is used so that leading whitespace (indentation) is not trimmed.
|
|
out = out..markdown_to_hypertext_format(md:sub(offset, block_start -1)).."<mono><style color="..theme.code_color..">"..block:gsub("\n", "\n\u{a0}").."</style></mono>"
|
|
offset = block_end +1
|
|
end
|
|
if code_start and code_start < (block_start or math.huge) then
|
|
out = out..markdown_to_hypertext_format(md:sub(offset, code_start)).."<mono><style color="..theme.code_color..">"..code.."</style></mono>"
|
|
offset = code_end +1
|
|
end
|
|
end
|
|
out = out..markdown_to_hypertext_format(md:sub(offset))
|
|
-- md = md
|
|
-- :gsub("\n%s*####%s*([^\n]-)\n", "\n<b>%1</b>\n")
|
|
-- :gsub("\n%s*###%s*([^\n]-)\n", "\n<big>%1</big>\n")
|
|
-- :gsub("([^\n]-)\n%-+\n", "\n<big>%1</big>\n")
|
|
-- :gsub("\n%s*##%s*([^\n]-)\n", "\n<bigger>%1</bigger>\n")
|
|
-- :gsub("\n%s*#%s*([^\n]-)\n", "\n<style size=48>%1</style>\n")
|
|
-- :gsub("%*%*([^`\n]-)%*%*", "<b>%1</b>")
|
|
-- :gsub("%*([^`\n]-)%*", "<i>%1</i>")
|
|
-- :gsub("__([^`\n]-)__", "<b>%1</b>")
|
|
-- :gsub("\n_([^`\n]-)_", "<i>%1</i>")
|
|
-- :gsub("%*%s+(.-)\n", "• %1\n")
|
|
-- -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them.
|
|
-- :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "")
|
|
-- print(md)
|
|
return fe(out)
|
|
end
|
|
|
|
|
|
include "templates.lua"
|
|
|
|
--[[ MARK: - Stylesheet Parser
|
|
The question of custom themes being what it is, and in order to avoid an
|
|
unreasonably convoluted settings page, I've opted to allow users to declare
|
|
themes simply by editing a stylesheet.
|
|
|
|
Stylesheets are defined in basic toml format, like so:
|
|
```
|
|
[button]
|
|
|
|
color = #faa
|
|
|
|
[label]
|
|
|
|
color = #444
|
|
|
|
[.table_even]
|
|
|
|
bgcolor = #111
|
|
```
|
|
|
|
Each section begins with a header (denoted by brackets), which specifies the type of element to apply the style to. Prefixing a header name with a dot will cause that header to match elements which are given a certain class.
|
|
|
|
Rules take the form of `<property> = <value>`. Rules specified prior to any header will apply to all elements.
|
|
--]]
|
|
|
|
local function build_stylesheet_rules(rules)
|
|
local out = {}
|
|
local offset = 1
|
|
while true do
|
|
local a, b, key, value = rules:find("(%w+)%s*=%s*([%w%p]+)", offset)
|
|
if not a then break end
|
|
out[key] = value
|
|
offset = b
|
|
end
|
|
return out
|
|
end
|
|
|
|
function build_stylesheet(sheet)
|
|
-- Strip commetns
|
|
sheet = sheet:gsub("%-%-.*\n", "")
|
|
local out = {}
|
|
local last_header
|
|
local offset = 1
|
|
while true do
|
|
local a, b, header = sheet:find("%[%s*([%a._-]+)%s*%]", offset)
|
|
if not a then break end
|
|
out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset, a -1))
|
|
last_header = header
|
|
offset = b +1
|
|
end
|
|
out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset))
|
|
return out
|
|
end
|
|
|
|
print(dump(build_stylesheet([[
|
|
[button]
|
|
|
|
color = #faa
|
|
|
|
[label]
|
|
|
|
color = #444
|
|
]])))
|
|
|
|
function evaluate_stylesheet()
|
|
|
|
end
|
|
|
|
-- MARK: - Views
|
|
|
|
-- The meta menu, for choosing which game menu to enter or entering the servers/content views.
|
|
local last_game = minetest.settings:get("menu_last_game")
|
|
function show_meta_menu(v)
|
|
state.loc = "games"
|
|
local games = minetest.get_games()
|
|
if #games < 1 then
|
|
minetest.update_formspec(meta_header.."\
|
|
hypertext[0,0;"..size.x..","..size.y..";ht;<global valign=middle halign=center><tag name=action color=#222 hovercolor=#444>\
|
|
<style size=72 color=#222><b>No games installed.</b></style>\
|
|
<style size=36 color=#222><action name=content>Click here to install a game.</action></style>]\
|
|
")
|
|
return
|
|
end
|
|
local idx = 1
|
|
for i, x in ipairs(games) do
|
|
if x.id == last_game then idx = i end
|
|
end
|
|
if v then idx = v else v = idx end
|
|
local fs = ""
|
|
|
|
|
|
fs = fs.."\
|
|
scroll_container[0,0;"..size.x..","..size.y..";carousel;horizontal;;]\
|
|
box[0,0;"..(#games *5)..",1;#0000]\
|
|
"
|
|
|
|
|
|
local center = (size.x /2) +(v /10)
|
|
|
|
for i, x in ipairs(games) do
|
|
fs = fs.."\
|
|
image_button["..(center +(i -(#games /2)) *0.25)..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == idx and "circle_light" or "circle")..".png;jump"..i..";]\
|
|
"
|
|
|
|
local dist = i -idx
|
|
local scale = 4 /(math.abs(dist) +1)
|
|
if scale > 0.1 then
|
|
--This looks neat, but is useless for UI purposes: center +10^(1 /math.abs(dist)) *math.sign(dist)
|
|
local lc = center +math.abs(dist *10)^0.55 *math.sign(dist)
|
|
if x.menuicon_path == "" then
|
|
x.menuicon_path = assets.."games.png"
|
|
end
|
|
--TODO: Do these need some kind of background?
|
|
fs = fs.."\
|
|
image_button["..(lc -(scale /2))..","..(size.y /2 -(scale /2) +(4 -scale) /2 +2)..";"..scale..","..scale..";"..fe(x.menuicon_path)..";game"..fe(x.id)..";]\
|
|
tooltip[game"..fe(x.id)..";"..fe(x.title)..";#444;#aaa]\
|
|
"
|
|
if dist == 0 then
|
|
fs = fs.."\
|
|
button["..(lc -(scale /2))..","..(size.y /2 +4)..";4,0.5;nobg;"..fe(x.title).."]\
|
|
"
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
fs = fs.."\
|
|
scroll_container_end[]\
|
|
scrollbaroptions[min=1;max="..#games..";smallstep=1]\
|
|
scrollbar[0,-800;"..size.x..",0;horizontal;carousel;"..(v or "").."]\
|
|
"
|
|
|
|
minetest.update_formspec(meta_header..fs.."\
|
|
style[show_servers_view;border=false;bgimg="..fe(assets).."menu_servers.png;bgimg_middle=0]\
|
|
style[show_servers_view:hovered;border=false;bgimg="..fe(assets).."menu_servers_hovered.png;bgimg_middle=0]\
|
|
button["..(size.x *0.05)..","..(size.y *0.3)..";4,4;show_servers_view;]\
|
|
button["..(size.x *0.05)..","..(size.y *0.3 +4)..";4,0.5;nobg;Servers]\
|
|
style[show_content_view;border=false;bgimg="..fe(assets).."menu_content.png;bgimg_middle=0]\
|
|
style[show_content_view:hovered;border=false;bgimg="..fe(assets).."menu_content_hovered.png;bgimg_middle=0]\
|
|
button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3)..";4,4;show_content_view;]\
|
|
button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3 +4)..";4,0.5;nobg;Content]\
|
|
style[show_settings_view,show_about_view;bgimg="..assets.."menu_tab_bg.png]\
|
|
style[show_settings_view:hovered,show_about_view:hovered;bgimg="..assets.."menu_tab_bg.png;bgcolor=#fffd]\
|
|
button[1,0;2,0.5;show_about_view;About]\
|
|
button["..(size.x -3)..",0;2,0.5;show_settings_view;Settings]\
|
|
")
|
|
end
|
|
|
|
-- Shows games' custom menus.
|
|
function show_game_menu(args)
|
|
if not state.current_game then
|
|
minetest.log("warning", "Main menu attempted to show a menu for a nonexistent game; aborting.")
|
|
return
|
|
end
|
|
if not args then args = {} end
|
|
local game = state.current_game
|
|
if not game.menu then
|
|
local file = read_file(game.path.."/menu/mainmenu.txt") or default_game_menu
|
|
|
|
game.disabled_settings = {}
|
|
local settings = Settings(game.path.."/game.conf")
|
|
|
|
local disabled_settings = string.split(settings:get("disabled_settings") or "", ",")
|
|
for _, x in ipairs(disabled_settings) do
|
|
x = x:trim()
|
|
if x == "enable_damage" then
|
|
game.disabled_settings.damage = false
|
|
elseif x == "!enable_damage" then
|
|
game.disabled_settings.damage = true
|
|
elseif x == "creative_mode" then
|
|
game.disabled_settings.creative = false
|
|
elseif x == "!creative_mode" then
|
|
game.disabled_settings.creative = true
|
|
elseif x == "enable_server" then
|
|
game.disabled_settings.server = false
|
|
end
|
|
end
|
|
|
|
local overlays = {}
|
|
local backgrounds = {}
|
|
local headers = {}
|
|
local footers = {}
|
|
local music = {}
|
|
|
|
for _, x in ipairs(minetest.get_dir_list(game.path.."/menu", false)) do
|
|
if x:sub(1, string.len("background")) == "background" then
|
|
local a, b, id = x:find("background.(%d+).png")
|
|
if a or x == "background.png" then
|
|
backgrounds[(tonumber(id) or 0) +1] = x
|
|
end
|
|
elseif x:sub(1, string.len("overlay")) == "overlay" then
|
|
local a, b, id = x:find("overlay.(%d+).png")
|
|
if a or x == "overlay.png" then
|
|
overlays[(tonumber(id) or 0) +1] = x
|
|
end
|
|
elseif x:sub(1, string.len("header")) == "header" then
|
|
local a, b, id = x:find("header.(%d+).png")
|
|
if a or x == "header.png" then
|
|
headers[(tonumber(id) or 0) +1] = x
|
|
end
|
|
elseif x:sub(1, string.len("footer")) == "footer" then
|
|
local a, b, id = x:find("footer.(%d+).png")
|
|
if a or x == "footer.png" then
|
|
footers[(tonumber(id) or 0) +1] = x
|
|
end
|
|
elseif x:sub(1, string.len("theme")) == "theme" then
|
|
local a, b, id = x:find("theme.(%d+).ogg")
|
|
if a or x == "theme.ogg" then
|
|
music[(tonumber(id) or 0) +1] = x
|
|
end
|
|
end
|
|
end
|
|
|
|
if #backgrounds > 0 then
|
|
local path = game.path.."/menu/"..backgrounds[math.random(#backgrounds)]
|
|
if minetest.set_background("background", path) then
|
|
game.background = path
|
|
end
|
|
elseif #overlays > 0 then
|
|
local path = game.path.."/menu/"..overlays[math.random(#overlays)]
|
|
-- I don't know why, but using "overlay" here simply does nothing. Whatever
|
|
-- the difference is, it isn't visual, so this effectively 'fixes' it.
|
|
if minetest.set_background("background", path) then
|
|
game.overlay = path
|
|
end
|
|
else
|
|
minetest.set_background("background", "")
|
|
end
|
|
|
|
if #headers > 0 then
|
|
local path = game.path.."/menu/"..headers[math.random(#headers)]
|
|
if minetest.set_background("header", path) then
|
|
game.header = path
|
|
end
|
|
end
|
|
|
|
if #music > 0 then
|
|
local path = game.path.."/menu/"..music[math.random(#music)]
|
|
if minetest.set_background("header", path) then
|
|
game.music = path
|
|
end
|
|
end
|
|
|
|
game.menu = build_game_menu(file)
|
|
|
|
state.menu_vars = {}
|
|
|
|
state.menu_vars.setting_damage = game.disabled_settings.damage == nil
|
|
state.menu_vars.setting_creative = game.disabled_settings.creative == nil
|
|
state.menu_vars.setting_server = game.disabled_settings.server == nil
|
|
end
|
|
|
|
if args.show_dialog then
|
|
state.menu_current = {args.show_dialog}
|
|
elseif args.overlay_dialog then
|
|
state.menu_current[#state.menu_current +1] = args.overlay_dialog
|
|
elseif args.unoverlay_dialog and #state.menu_current > 1 then
|
|
state.menu_current[#state.menu_current] = nil
|
|
end
|
|
|
|
state.menu_vars.WIDTH = size.x
|
|
state.menu_vars.HEIGHT = size.y
|
|
|
|
local fs = ""
|
|
|
|
if not game.background and not game.overlay then
|
|
fs = "\
|
|
bgcolor[#000;true;#151618]\
|
|
"
|
|
end
|
|
|
|
|
|
for i, x in ipairs(state.menu_current) do
|
|
if i > 1 then
|
|
fs = fs.."\
|
|
box[0,0;"..size.x..","..size.y..";#0009]\
|
|
"
|
|
end
|
|
fs = fs..evaluate_game_dialog(game.menu[x], state.menu_vars)
|
|
end
|
|
-- print(fs)
|
|
|
|
minetest.update_formspec(game_header..fs.."\
|
|
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
|
|
")
|
|
end
|
|
|
|
function add_favorite_server(address, port)
|
|
state.favorite_servers[#state.favorite_servers +1] = {address = address, port = port}
|
|
local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w")
|
|
f:write(minetest.write_json(state.favorite_servers))
|
|
f:flush()
|
|
f:close()
|
|
end
|
|
|
|
function remove_favorite_server(address, port)
|
|
local idx
|
|
for i, x in pairs(state.favorite_servers) do
|
|
if x.address == address and x.port == port then
|
|
idx = i
|
|
break
|
|
end
|
|
end
|
|
if idx then
|
|
table.remove(state.favorite_servers, idx)
|
|
local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w")
|
|
f:write(minetest.write_json(state.favorite_servers))
|
|
f:flush()
|
|
f:close()
|
|
end
|
|
end
|
|
|
|
function refresh_server_list()
|
|
minetest.handle_async(
|
|
function(param)
|
|
local http = minetest.get_http_api()
|
|
local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format(
|
|
"https://servers.luanti.org" or minetest.settings:get("serverlist_url"),
|
|
minetest.get_min_supp_proto(),
|
|
minetest.get_max_supp_proto()
|
|
)
|
|
|
|
local response = http.fetch_sync{url = url}
|
|
if not response.succeeded then
|
|
return {}
|
|
end
|
|
|
|
local retval = minetest.parse_json(response.data)
|
|
return retval and retval.list or {}
|
|
end,
|
|
nil,
|
|
function(result)
|
|
local list = table.copy(state.favorite_servers)
|
|
table.insert_all(list, result)
|
|
state.serverlist = list
|
|
if state.serverlist_filtered then
|
|
state.serverlist_filtered = search_server_list(state.servers_filter)
|
|
end
|
|
if state.loc == "servers" then
|
|
show_servers_menu()
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
function search_server_list(input)
|
|
if input:trim() == "" then
|
|
return nil
|
|
end
|
|
|
|
local search_str = ""
|
|
local words = {}
|
|
local mods = {}
|
|
local games = {}
|
|
local players = {}
|
|
|
|
input = input:lower()
|
|
|
|
for x in input:gmatch("%S+") do
|
|
if x:sub(1, 4) == "mod:" then
|
|
mods[#mods +1] = x:sub(5)
|
|
elseif x:sub(1, 7) == "player:" then
|
|
players[#players +1] = x:sub(8)
|
|
elseif x:sub(1, 5) == "game:" then
|
|
games[#games +1] = x:sub(6)
|
|
else
|
|
words[#words +1] = x
|
|
end
|
|
end
|
|
|
|
-- print(dump({words = words, mods= mods, players =players, games = games}, " "))
|
|
|
|
local out = {}
|
|
for _, x in ipairs(state.serverlist) do
|
|
local passed = true
|
|
|
|
for _, a in ipairs(words) do
|
|
if not (x.description and x.description:lower():find(a, 1, true) or x.name and x.name:lower():find(a, 1, true)) then
|
|
passed = false
|
|
end
|
|
end
|
|
|
|
-- PUC Lua doesn't have `continue`, hence the indentation.
|
|
if passed then
|
|
if #games > 0 then
|
|
passed = false
|
|
end
|
|
for _, a in ipairs(games) do
|
|
if a == x.gameid then
|
|
passed = true
|
|
end
|
|
end
|
|
|
|
if passed then
|
|
if x.mods and #mods > 0 then
|
|
local found = 0
|
|
for _, a in ipairs(x.mods) do
|
|
for _, b in ipairs(mods) do
|
|
if a == b then
|
|
found = found +1
|
|
end
|
|
end
|
|
end
|
|
passed = found == #mods
|
|
else
|
|
passed = not not x.mods
|
|
end
|
|
|
|
if passed then
|
|
if x.clients_list and #players > 0 then
|
|
local found = 0
|
|
for _, a in ipairs(x.clients_list) do
|
|
for _, b in ipairs(players) do
|
|
if a:lower() == b then
|
|
found = found +1
|
|
end
|
|
end
|
|
end
|
|
|
|
passed = found == #players
|
|
else
|
|
passed = not not x.clients_list
|
|
end
|
|
if passed then
|
|
out[#out +1] = x
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Shows the server list.
|
|
function show_servers_menu()
|
|
local fs = ""
|
|
local loading
|
|
if not state.serverlist then
|
|
loading = true
|
|
refresh_server_list()
|
|
local favorite_servers = minetest.parse_json(read_file(minetest.get_user_path().."/client/serverlist/favoriteservers.json") or "{}")
|
|
for _, x in ipairs(favorite_servers) do
|
|
x.favorite = true
|
|
end
|
|
state.favorite_servers = favorite_servers
|
|
state.serverlist = favorite_servers
|
|
end
|
|
|
|
-- Overlay dialogs don't occlude area tooltips, so such tooltips are disabled in that case.
|
|
local showing_dialog = state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players
|
|
|
|
local current_server
|
|
local joining_server
|
|
|
|
fs = "\
|
|
style[servers_filter;border=false;textcolor=#aaa]\
|
|
image["..(size.x *0.095)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.65)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
field["..(size.x *0.095 +0.1)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.45)..",0.75;servers_filter;;"..(state.menu_vars.servers_filter or "").."]\
|
|
style[servers_search,servers_refresh,servers_cancel;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
|
|
image_button["..(size.x *0.91 -5.65)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."search.png;servers_search;]\
|
|
tooltip[servers_search;Search server list;#444;#aaa]\
|
|
image_button["..(size.x *0.91 -4.8)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."cancel.png;servers_cancel;]\
|
|
tooltip[servers_cancel;Cancel search;#444;#aaa]\
|
|
image_button["..(size.x *0.91 -3.95)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."refresh.png;servers_refresh;]\
|
|
tooltip[servers_refresh;Refresh server list;#444;#aaa]\
|
|
button["..(size.x *0.91 -3.1)..","..(size.y *0.1 -0.85)..";3,0.75;direct_connection;Direct Connection...]\
|
|
image["..(size.x *0.095)..","..(size.y *0.1)..";"..(size.x *0.81)..","..(size.y *0.8)..";"..assets.."btn_bg.png;8,8]\
|
|
scroll_container[0,"..(size.y *0.1 +0.1)..";"..size.x..","..(size.y *0.8 -0.2)..";serverscroll;vertical;;0,0]\
|
|
"
|
|
local listx = size.x *0.1
|
|
local infox = state.current_server and size.x *0.6 or size.x *0.9
|
|
local max = size.x *0.9
|
|
local i = 0
|
|
for idx, x in ipairs(state.serverlist_filtered or state.serverlist) do
|
|
-- Skip incompatible servers. You can't join them, so showing them is rather pointless.
|
|
if (x.proto_max or version.proto_min) >= version.proto_min then
|
|
local ping_lvl = 0
|
|
local lag = (x.lag or 0) * 1000 + (x.ping or 0) * 250
|
|
if lag <= 125 then
|
|
ping_lvl = 4
|
|
elseif lag <= 175 then
|
|
ping_lvl = 3
|
|
elseif lag <= 250 then
|
|
ping_lvl = 2
|
|
elseif lag <= 400 then
|
|
ping_lvl = 1
|
|
end
|
|
|
|
local name = x.name and x.name:trim() or ""
|
|
if name == "" then
|
|
name = minetest.colorize("#888", x.address..":"..x.port)
|
|
end
|
|
local label_offset = 0.1
|
|
fs = fs.."\
|
|
style[serverinfo"..idx..";border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#"..(i %2 == 1 and "373530" or "403e39").."ff]\
|
|
style[serverinfo"..idx..":hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0]\
|
|
button["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;serverinfo"..idx..";]\
|
|
"
|
|
if x.favorite then
|
|
fs = fs.."\
|
|
image["..(listx)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_favorite.png]\
|
|
"..(showing_dialog and "" or "tooltip["..(listx)..","..(i *0.5)..";0.5,0.5;Favorite server;#444;#aaa]\
|
|
")
|
|
label_offset = 0.6
|
|
end
|
|
local clients = ""
|
|
local color = "#aaa"
|
|
local icons_offset = (infox -0.6)
|
|
if x.clients then
|
|
icons_offset = icons_offset -1
|
|
clients = x.clients..(x.clients_max and "/"..x.clients_max or "")
|
|
if x.clients > 0 and x.clients_max then
|
|
local percent = x.clients /x.clients_max
|
|
if percent < 0.75 then
|
|
color = "#638b67"
|
|
elseif percent < 1 then
|
|
color = "#a69174"
|
|
else
|
|
color = "#9d5b5b"
|
|
end
|
|
end
|
|
fs = fs.."\
|
|
hypertext["..icons_offset..","..(i *0.5)..";1,0.5;;<global valign=middle halign=center color="..color..">"..fe(clients).."]\
|
|
"
|
|
end
|
|
if x.pvp == false then
|
|
icons_offset = icons_offset -0.5
|
|
fs = fs.."\
|
|
image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_peaceful.png]\
|
|
"..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Peaceful;#444;#aaa]\
|
|
")
|
|
end
|
|
if x.creative then
|
|
icons_offset = icons_offset -0.5
|
|
fs = fs.."\
|
|
image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_creative.png]\
|
|
"..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Creative;#444;#aaa]\
|
|
")
|
|
end
|
|
fs = fs.."\
|
|
label["..(listx +label_offset)..","..(i *0.5 +0.25)..";"..fe(name).."]\
|
|
"..((x.ping or x.lag) and "image["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_icon_ping_"..ping_lvl..".png]\
|
|
"..(showing_dialog and "" or "tooltip["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;Ping: "..math.floor(lag)..";#444;#aaa]\
|
|
") or "")
|
|
i = i +1
|
|
end
|
|
end
|
|
if loading then
|
|
fs = fs.."\
|
|
hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;<global valign=middle halign=center color=#777>Loading...]\
|
|
"
|
|
end
|
|
if i < 1 then
|
|
fs = fs.."\
|
|
hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;<global valign=middle halign=center color=#777>No servers found.]\
|
|
"
|
|
end
|
|
fs = fs.."\
|
|
scroll_container_end[]\
|
|
scrollbar[-800,0;1,1;vertical;serverscroll;"..(state.menu_vars.serverscroll and tonumber(state.menu_vars.serverscroll:sub(5)) or 0).."]\
|
|
"
|
|
-- Server detail view.
|
|
if state.current_server then
|
|
fs = fs.."box["..(infox -0.05)..","..(size.y *0.1 +0.05)..";0.1,"..(size.y *0.8 -0.1)..";#292d2fff]\
|
|
container["..infox..","..(size.y *0.1).."]\
|
|
hypertext[0,0;"..(max -infox)..",0.75;;<global valign=middle halign=center color=#aaa><b>"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."</b>]\
|
|
image[0.1,0.75;"..(max -infox -0.2)..",0.5;"..assets.."btn_bg_2_dark.png;8,8]\
|
|
box["..((max -infox) *0.7 -0.05)..",0.75;0.1,0.5;#292d2fff]\
|
|
hypertext[0.2,0.8;"..((max -infox) *0.7 -0.2)..",0.5;server_address;<global valign=middle color=#aaa>"..fe(hte(state.current_server.address)).."]\
|
|
hypertext["..((max -infox) *0.7 +0.1)..",0.8;"..((max -infox) *0.3 -0.2)..",0.5;server_port;<global valign=middle color=#aaa>"..fe(hte(tostring(state.current_server.port))).."]\
|
|
"
|
|
local buttons_offset = 0.1
|
|
if state.current_server.mods then
|
|
fs = fs.."\
|
|
image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_mods.png;show_server_mods;]\
|
|
tooltip[show_server_mods;Show mod list;#444;#aaa]\
|
|
"
|
|
buttons_offset = buttons_offset +0.5
|
|
end
|
|
if state.current_server.clients_list then
|
|
fs = fs.."\
|
|
image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_players.png;show_server_players;]\
|
|
tooltip[show_server_players;Show player list;#444;#aaa]\
|
|
"
|
|
buttons_offset = buttons_offset +0.5
|
|
end
|
|
if state.current_server.favorite then
|
|
fs = fs.."\
|
|
image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_unfavorite.png;unfavorite_server;]\
|
|
tooltip[unfavorite_server;Remove from favorites;#444;#aaa]\
|
|
"
|
|
buttons_offset = buttons_offset +0.5
|
|
end
|
|
local y = buttons_offset > 0.1 and 2.25 or 1.75
|
|
fs = fs.."\
|
|
hypertext[0.1,"..y..";"..(max -infox -0.2)..","..((size.y *0.8 -1.5 -1.05))..";server_desc;<global color=#aaa>"..fe(hte(state.current_server.description or "")).."]\
|
|
button[0.1,"..(size.y *0.8 -1)..";"..(max -infox -0.2)..",0.75;join_server;Join Server]\
|
|
container_end[]\
|
|
"
|
|
end
|
|
|
|
fs = fs.."\
|
|
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
|
|
"
|
|
|
|
-- Mod list dialog.
|
|
if state.showing_server_mods then
|
|
fs = fs.."\
|
|
box[0,0;"..size.x..","..size.y..";#0008]\
|
|
style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
|
|
style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\
|
|
image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\
|
|
scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\
|
|
"
|
|
for i, x in ipairs(state.current_server.mods) do
|
|
fs = fs.."\
|
|
button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\
|
|
label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\
|
|
"
|
|
end
|
|
fs = fs.."\
|
|
scroll_container_end[]\
|
|
scrollbar[-800,0;1,1;vertical;modsscroll;]\
|
|
button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\
|
|
"
|
|
end
|
|
|
|
-- Player list dialog.
|
|
if state.showing_server_players then
|
|
fs = fs.."\
|
|
box[0,0;"..size.x..","..size.y..";#0008]\
|
|
style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
|
|
style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\
|
|
image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\
|
|
scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\
|
|
"
|
|
for i, x in ipairs(state.current_server.clients_list) do
|
|
fs = fs.."\
|
|
button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\
|
|
label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\
|
|
"
|
|
end
|
|
fs = fs.."\
|
|
scroll_container_end[]\
|
|
scrollbar[-800,0;1,1;vertical;modsscroll;]\
|
|
button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\
|
|
"
|
|
end
|
|
|
|
-- Connection dialog.
|
|
if state.joining_server or state.connecting_to_server then
|
|
local offset = 0
|
|
fs = fs.."\
|
|
box[0,0;"..size.x..","..size.y..";#0008]\
|
|
image["..(size.x *0.25 -0.2)..","..(size.y *0.25 -0.1)..";"..(size.x *0.5 +0.4)..","..(size.y *0.5 +0.2)..";"..assets.."btn_bg.png;8,8]\
|
|
scroll_container["..(size.x *0.25)..","..(size.y *0.25)..";"..(size.x *0.5)..","..(size.y *0.5 +0.2)..";serverconfscroll;vertical;;0,0]\
|
|
"
|
|
|
|
if state.server_connection_error then
|
|
offset = offset +0.75
|
|
fs = fs.."\
|
|
hypertext[0,0;"..(size.x *0.5)..",0.75;;<global valign=middle color=#9d5b5b>"..fe(hte(state.server_connection_error.msg)).."]\
|
|
set_focus["..fe(state.server_connection_error.element)..";true]\
|
|
"
|
|
else
|
|
fs = fs.."\
|
|
set_focus[server_"..(state.joining_server and (minetest.settings:get("name") and "password" or "username") or "address")..";true]\
|
|
"
|
|
end
|
|
|
|
-- Make the address and port immutable if joining from the serverlist.
|
|
if state.joining_server then
|
|
fs = fs.."\
|
|
hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;<global valign=middle halign=center color=#aaa>Connecting to <b>"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."</b>...]\
|
|
"
|
|
offset = offset +1
|
|
else
|
|
fs = fs.."\
|
|
set_focus[server_address;true]\
|
|
hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;<global valign=middle halign=center color=#aaa>Connecting to server...]\
|
|
"
|
|
offset = offset +1
|
|
fs = fs.."\
|
|
image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
label[0.2,"..(0.1 +offset)..";Address]\
|
|
field[0.2,"..(0.25 +offset)..";"..(size.x *0.4 -0.4)..",0.75;server_address;;"..(state.menu_vars.server_address or "").."]\
|
|
box["..(size.x *0.4 -0.25)..","..(0.25 +offset)..";0.1,0.75;#292d2fff]\
|
|
label["..(size.x *0.4 -0.2)..","..(0.1 +offset)..";Port]\
|
|
field["..(size.x *0.4)..","..(0.25 +offset)..";"..(size.x *0.1 -0.2)..",0.75;server_port;;"..(state.menu_vars.server_port or "").."]\
|
|
"
|
|
offset = offset +1
|
|
end
|
|
fs = fs.."\
|
|
image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
label[0.2,"..(0.1 +offset)..";Username]\
|
|
field[0.2,"..(0.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_username;;"..minetest.settings:get("name").."]\
|
|
image[0,"..(1.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
style[server_password;border=false;textcolor=#aaa;bgimg="..assets.."btn_bg.png]\
|
|
label[0.2,"..(1.1 +offset)..";Password]\
|
|
pwdfield[0.2,"..(1.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_password;]\
|
|
button[0,"..(2.25 +offset)..";"..(size.x *0.5)..",0.75;cancel_join_server;Cancel]\
|
|
button[0,"..(3.25 +offset)..";"..(size.x *0.5)..",0.75;confirm_join_server;Join]\
|
|
scroll_container_end[]\
|
|
scrollbar[-800,0;1,1;vertical;serverconfscroll;"..(state.menu_vars.serverconfscroll and tonumber(state.menu_vars.serverconfscroll:sub(5)) or 0).."]\
|
|
"
|
|
end
|
|
minetest.update_formspec(servers_header..fs)
|
|
end
|
|
|
|
-- Show the content manager.
|
|
function show_content_menu()
|
|
local fs = ""
|
|
|
|
if not state.all_content then
|
|
get_all_content()
|
|
end
|
|
|
|
local w = size.x
|
|
local h = size.y -1.1
|
|
|
|
if not state.current_package then
|
|
|
|
fs = "\
|
|
scroll_container[0,1.1;"..w..","..h..";contentscroll;horizontal;;]\
|
|
"
|
|
|
|
local cols = math.floor(w /2.5)
|
|
local spacing_x = w %2.5 /cols +0.5
|
|
|
|
local rows = math.floor(h /3)
|
|
local spacing_y = h %3 /rows +0.5
|
|
|
|
local pages = math.ceil(#state.all_content /(rows *cols))
|
|
local page = state.content_page or 1
|
|
|
|
local offset = page /10
|
|
|
|
local x = 0
|
|
local y = 0
|
|
local i = (page -1) *(rows *cols) +1
|
|
|
|
while true do
|
|
local pkg = state.all_content[i]
|
|
if not pkg then break end
|
|
if not pkg.icon then
|
|
pkg.icon = pkg.type == "game" and pkg.path.."/menu/icon.png" or pkg.path.."/icon.png"
|
|
if not file_exists(pkg.icon) then
|
|
pkg.icon = assets.."menu_content.png"
|
|
end
|
|
end
|
|
local title = pkg.title:trim()
|
|
if title == "" then
|
|
title = pkg.name
|
|
end
|
|
fs = fs.."\
|
|
image_button["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2))..";2,2;"..fe(pkg.icon)..";view_package_"..i..";]\
|
|
tooltip[view_package_"..i..";"..fe(title)..";#444;#aaa]\
|
|
hypertext["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2) +2)..";2,0.75;nobg;<global color=#aaa valign=middle halign=center>"..fe(hte(title)).."]\
|
|
"
|
|
i = i +1
|
|
x = x +1
|
|
if x > cols -1 then
|
|
x = 0
|
|
y = y +1
|
|
if y > rows -1 then break end
|
|
end
|
|
end
|
|
|
|
fs = fs.."\
|
|
box[0,0;"..(pages *w)..",1;#0000]\
|
|
scroll_container_end[]\
|
|
scrollbaroptions[min=1;max="..pages..";smallstep=1]\
|
|
scrollbar[-800,0;1,1;horizontal;contentscroll;"..page.."]\
|
|
"
|
|
|
|
fs = fs.."\
|
|
box[0,0;"..size.x..",1;#403e39ff]\
|
|
box[0,1;"..size.x..",0.1;#292d2fff]\
|
|
style[content_search,content_cancel,go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\
|
|
image_button[0.125,0.125;0.75,0.75;"..assets.."search.png;content_search;]\
|
|
tooltip[content_search;Search;#444;#aaa]\
|
|
image_button[1,0.125;0.75,0.75;"..assets.."cancel.png;content_cancel;]\
|
|
tooltip[content_cancel;Cancel;#444;#aaa]\
|
|
style[test;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
|
|
style[test:hovered;bgimg="..assets.."white.png;bgimg_middle=0]\
|
|
image[2,0.125;"..(size.x -9)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
field[2.1,0.125;"..(size.x -9.2)..",0.75;content_filter;;]\
|
|
button["..(w -6)..",0;4,1;test;Browse Online Content...]\
|
|
image_button["..(w -1.5)..",0.125;0.75,0.75;"..assets.."new_package.png;new_package;]\
|
|
tooltip[new_package;New Package...;#444;#aaa]\
|
|
"
|
|
|
|
local start = w /2 -(pages *0.125)
|
|
|
|
for i = 1, pages do
|
|
fs = fs.."\
|
|
image_button["..(start +(i *0.25))..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == page and "circle_light.png" or "circle.png")..";page"..i..";]\
|
|
"
|
|
end
|
|
|
|
else
|
|
|
|
local pkg = state.all_content[state.current_package]
|
|
local desc = ""
|
|
if file_exists(pkg.path.."/README.md") then
|
|
desc = read_file(pkg.path.."/README.md")
|
|
end
|
|
local pkg_type = pkg.type
|
|
if pkg_type == "mod" then
|
|
pkg_type = "<style color="..theme.mod_label_color..">\\[Mod\\]</style>"
|
|
elseif pkg_type == "modpack" then
|
|
pkg_type = "<style color="..theme.mod_label_color..">\\[Mod Collection\\]</style>"
|
|
elseif pkg_type == "game" then
|
|
pkg_type = "<style color="..theme.game_label_color..">\\[Game\\]</style>"
|
|
elseif pkg_type == "txp" then
|
|
pkg_type = "<style color="..theme.texture_pack_label_color..">\\[Texture Pack\\]</style>"
|
|
end
|
|
fs = fs.."\
|
|
style[go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\
|
|
image_button["..(size.x -1)..",0.5;0.5,0.5;"..assets.."cancel.png;go_back;]\
|
|
imdage["..(size.x *0.1 -0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.6)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\
|
|
hypertext["..(size.x *0.1)..","..(size.y *0.1)..";"..(size.x *0.6 -0.2)..","..(size.y *0.8)..";aa;\
|
|
<global halign=center color="..theme.text_color..">\
|
|
"..markdown_to_hypertext(desc).."\
|
|
]\
|
|
image["..(size.x *0.7 +0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.2 +0.2)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\
|
|
hypertext["..(size.x *0.7 +0.2)..","..(size.y *0.1)..";"..(size.x *0.2)..","..(size.y *0.8)..";bb;\
|
|
<global halign=center color="..theme.text_color..">\
|
|
<img name='"..fe(hte(pkg.icon)).."' width="..(size.x *0.1 *dpi.x).." height="..(size.x *0.1 *dpi.y)..">\
|
|
<center><big>"..fe(hte(pkg.title or pkg.name)).."</big></center>\
|
|
"..pkg_type.." <style color="..theme.muted_text_color..">"..fe(hte(pkg.name)).."</style>\
|
|
Creator: <b>"..fe(hte(pkg.author)).."</b>\
|
|
Location: "..(pkg.path:sub(2):gsub("/", "<style size=12> \\> </style>")).."\
|
|
"..(pkg.type == "modpack" and "Modules: "..#minetest.get_dir_list(pkg.path, true) or "").."\
|
|
]\
|
|
button["..(size.x *0.7 +0.2)..","..(size.y *0.9 -0.75)..";"..(size.x *0.2)..",0.75;browse_source;Browse Source]\
|
|
"
|
|
end
|
|
|
|
fs = fs.."\
|
|
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
|
|
"
|
|
|
|
minetest.update_formspec(content_header..fs)
|
|
end
|
|
|
|
function show_about_menu()
|
|
if not state.lua_api then
|
|
state.lua_api = {}
|
|
state.lua_api_sections = {}
|
|
-- TODO: Make this portable
|
|
local doc = read_file(minetest.get_builtin_path().."/../luanti/lua_api.md")
|
|
local offset = 1
|
|
while true do
|
|
local a, b, heading = doc:find("([^\n]+)\n=+\n", offset)
|
|
if not a then break end
|
|
if offset > 1 then
|
|
state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset, a -1)
|
|
end
|
|
state.lua_api_sections[#state.lua_api_sections +1] = heading
|
|
offset = b
|
|
end
|
|
state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset)
|
|
end
|
|
local fs = "\
|
|
style[docs_section;bgimg="..assets.."white.png;bgcolor=#fff0]\
|
|
style[docs_section:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
|
|
box[0,0;"..(size.x *0.25 -0.55)..","..size.y..";#403e39ff]\
|
|
box["..(size.x *0.25 -0.55)..",0;0.1,"..size.y..";#292d2fff]\
|
|
scroll_container[0,0;"..(size.x *0.25 -0.55)..","..size.y..";sidebarscroll;vertical;;0,0]\
|
|
"
|
|
for i, x in ipairs(state.lua_api_sections) do
|
|
fs = fs.."\
|
|
box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\
|
|
button[0,"..((i -1) *0.8)..";"..(size.x *0.25 -0.55)..",0.75;docs_section;"..fe(x).."]\
|
|
"
|
|
end
|
|
fs = fs.."\
|
|
scroll_container_end[]\
|
|
scrollbar[-800,-800;0,0;vertical;sidebarscroll;"..(state.menu_vars.sidebarscroll and state.menu_vars.sidebarscroll:sub(5) or "").."]\
|
|
hypertext["..(size.x *0.25)..",0;"..size.x..","..size.y..";aa;<global color="..theme.text_color..">\
|
|
"..markdown_to_hypertext(state.lua_api[state.menu_vars.docs_section or state.lua_api_sections[1]]).."\
|
|
]"
|
|
|
|
fs = fs.."\
|
|
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
|
|
"
|
|
|
|
minetest.update_formspec(content_header..fs)
|
|
end
|
|
|
|
function show_settings_menu()
|
|
if not state.settings then
|
|
state.settings = {}
|
|
state.settings_categories = {}
|
|
local settings = settingtypes.parse_config_file(minetest.get_builtin_path().."/settingtypes.txt")
|
|
for _, x in ipairs(settings) do
|
|
if x.type == "category" then
|
|
if x.level == 1 and not x.name:find "Hide:" then
|
|
state.settings_categories[#state.settings_categories +1] = x.name
|
|
state.settings[x.name] = {}
|
|
end
|
|
else
|
|
local s = state.settings[state.settings_categories[#state.settings_categories]]
|
|
s[#s +1] = x
|
|
end
|
|
end
|
|
end
|
|
|
|
local fs = "\
|
|
box[0,0;"..(size.x *0.25 -0.75)..","..size.y..";#403e39ff]\
|
|
box["..(size.x *0.25 -0.75)..",0;0.1,"..size.y..";#292d2fff]\
|
|
scroll_container[0,0;"..(size.x *0.25 -0.75)..","..size.y..";sidebarscroll;vertical;;0,0]\
|
|
style[show_theme;bgimg="..assets.."white.png;bgcolor=#fff0]\
|
|
style[show_theme:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
|
|
button[0,0;"..(size.x *0.25 -0.55)..",0.75;show_theme;Theme]\
|
|
"
|
|
|
|
for i, category in ipairs(state.settings_categories) do
|
|
fs = fs.."\
|
|
style[show_category;bgimg="..assets.."white.png;bgcolor=#fff0]\
|
|
style[show_category:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
|
|
box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\
|
|
button[0,"..(i *0.8)..";"..(size.x *0.25 -0.55)..",0.75;show_category;"..fe(category).."]\
|
|
"
|
|
end
|
|
|
|
fs = fs.."scroll_container_end[]\
|
|
scrollbar[-800,-800;0,0;vertical;sidebarscroll;]\
|
|
scroll_container["..(size.x *0.25)..",0;"..(size.x *0.75)..","..size.y..";settingsscroll;vertical;;0,0]\
|
|
"
|
|
|
|
if state.settings_category then
|
|
local page = state.settings[state.settings_category]
|
|
local y = 0.25
|
|
for i, x in ipairs(page) do
|
|
if x.type == "string" then
|
|
fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\
|
|
image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\
|
|
button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\
|
|
"
|
|
y = y +1.2
|
|
elseif x.type == "bool" then
|
|
local state = minetest.settings:get_bool(x.name, x.default)
|
|
fs = fs.."image_button[0,"..y..";0.75,0.75;"..assets..(state and "checkbox_filled" or "checkbox_empty")..".png;toggle_setting_"..fe(x.name)..";]\
|
|
label[1,"..(y +0.375)..";"..fe(x.readable_name).."]\
|
|
"
|
|
y = y +1
|
|
elseif x.type == "int" or x.type == "float" then
|
|
fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\
|
|
image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
|
|
field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\
|
|
button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\
|
|
"
|
|
y = y +1.2
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
fs = fs.."\
|
|
scroll_container_end[]\
|
|
scrollbar[-800,-800;0,0;vertical;settingsscroll;]\
|
|
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
|
|
"
|
|
|
|
minetest.update_formspec(content_header..fs)
|
|
end
|
|
|
|
|
|
function minetest.button_handler(data)
|
|
if not state.menu_vars then state.menu_vars = {} end
|
|
setmetatable(state.menu_vars, {__index = data})
|
|
if state.loc == "games" then
|
|
if data.content or data.ht == "action:content" then
|
|
if state.loc ~= "content" then show_content() end
|
|
elseif data.games then
|
|
if state.loc ~= "games" then show_meta_menu() end
|
|
elseif data.carousel and data.carousel:sub(1, 4) == "CHG:" then
|
|
local v = tonumber(data.carousel:sub(5))
|
|
show_meta_menu(v)
|
|
else
|
|
for k, v in pairs(data) do
|
|
if k:sub(1, 4) == "game" then
|
|
local id = k:sub(5)
|
|
state.current_game = get_game_info(id)
|
|
state.menu_current = {"main"}
|
|
state.loc = "game"
|
|
state.menu_vars = {}
|
|
show_game_menu()
|
|
minetest.settings:set("menu_last_game", id)
|
|
last_game = id
|
|
elseif k:sub(1, 4) == "jump" then
|
|
local v = tonumber(k:sub(5))
|
|
show_meta_menu(v)
|
|
end
|
|
end
|
|
end
|
|
elseif state.loc == "servers" then
|
|
if data.servers_refresh then
|
|
refresh_server_list()
|
|
elseif data.servers_search or data.key_enter_field == "servers_filter" then
|
|
state.servers_filter = data.servers_filter
|
|
state.serverlist_filtered = search_server_list(data.servers_filter)
|
|
show_servers_menu()
|
|
elseif data.servers_cancel then
|
|
state.serverlist_filtered = nil
|
|
state.servers_filter = nil
|
|
data.servers_filter = ""
|
|
show_servers_menu()
|
|
elseif data.unfavorite_server then
|
|
state.current_server.favorite = nil
|
|
remove_favorite_server(state.current_server.address, state.current_server.port)
|
|
show_servers_menu()
|
|
elseif data.show_server_mods then
|
|
state.showing_server_mods = true
|
|
show_servers_menu()
|
|
elseif data.show_server_players then
|
|
state.showing_server_players = true
|
|
show_servers_menu()
|
|
elseif data.close_dialog then
|
|
state.showing_server_players = nil
|
|
state.showing_server_mods = nil
|
|
show_servers_menu()
|
|
elseif data.direct_connection then
|
|
state.connecting_to_server = true
|
|
show_servers_menu()
|
|
elseif data.join_server then
|
|
state.joining_server = true
|
|
show_servers_menu()
|
|
elseif data.cancel_join_server then
|
|
state.joining_server = nil
|
|
state.connecting_to_server = nil
|
|
show_servers_menu()
|
|
elseif data.confirm_join_server or data.key_enter_field == "server_username" or data.key_enter_field == "server_password" or data.key_enter_field == "server_address" then
|
|
minetest.settings:set("name", data.server_username)
|
|
local address
|
|
local port
|
|
if state.connecting_to_server then
|
|
if not tonumber(data.server_port) then
|
|
state.server_connection_error = {
|
|
msg = "Invalid port.",
|
|
element = "server_port"
|
|
}
|
|
show_servers_menu()
|
|
state.server_connection_error = nil
|
|
return
|
|
end
|
|
address = data.server_address:lower()
|
|
port = data.server_port:lower()
|
|
else
|
|
address = state.current_server.address:lower()
|
|
port = state.current_server.port
|
|
end
|
|
local is_favorite = false
|
|
for _, x in ipairs(state.favorite_servers) do
|
|
if x.address == address and x.port == port then
|
|
is_favorite = true
|
|
end
|
|
end
|
|
if not is_favorite then
|
|
add_favorite_server(address, port)
|
|
end
|
|
minetest.settings:set("address", address)
|
|
minetest.settings:set("remote_port", port)
|
|
gamedata = {
|
|
playername = data.server_username,
|
|
password = data.server_password,
|
|
address = address,
|
|
port = port,
|
|
selected_world = 0,
|
|
singleplayer = false
|
|
}
|
|
|
|
minetest.start()
|
|
else
|
|
for k, v in pairs(data) do
|
|
if k:sub(1, string.len("serverinfo")) == "serverinfo" then
|
|
local idx = k:sub(string.len("serverinfo>"))
|
|
if idx:sub(1, 1) == "f" then
|
|
state.current_server = state.favorite_servers[tonumber(idx:sub(2))]
|
|
else
|
|
state.current_server = (state.serverlist_filtered or state.serverlist)[tonumber(idx)]
|
|
end
|
|
show_servers_menu()
|
|
end
|
|
end
|
|
end
|
|
elseif state.loc == "content" then
|
|
if data.contentscroll and data.contentscroll:sub(1, 4) == "CHG:" then
|
|
state.content_page = tonumber(data.contentscroll:sub(5))
|
|
show_content_menu()
|
|
elseif data.go_back then
|
|
state.current_package = nil
|
|
show_content_menu()
|
|
else
|
|
for k, v in pairs(data) do
|
|
if k:sub(1, 4) == "page" then
|
|
state.content_page = tonumber(k:sub(5))
|
|
show_content_menu()
|
|
elseif k:sub(1, string.len("view_package_")) == "view_package_" then
|
|
state.current_package = tonumber(k:sub(string.len("view_package_>")))
|
|
show_content_menu()
|
|
end
|
|
end
|
|
end
|
|
elseif state.loc == "game" then
|
|
for k, v in pairs(data) do
|
|
if k == ".play" then
|
|
gamedata = {
|
|
playername = "singleplayer",
|
|
password = "",
|
|
address = nil,
|
|
port = nil,
|
|
selected_world = get_world_index(state.menu_vars.selected_world),
|
|
singleplayer = true
|
|
}
|
|
minetest.start()
|
|
elseif k:sub(1, string.len(".show_dialog_")) == ".show_dialog_" then
|
|
show_game_menu {
|
|
show_dialog = k:sub(string.len(".show_dialog_>"))
|
|
}
|
|
elseif k:sub(1, string.len(".overlay_dialog_")) == ".overlay_dialog_" then
|
|
show_game_menu {
|
|
overlay_dialog = k:sub(string.len(".overlay_dialog_>"))
|
|
}
|
|
elseif k:sub(1, string.len(".select_world_")) == ".select_world_" then
|
|
state.menu_vars.selected_world = k:sub(string.len(".select_world_>"))
|
|
state.menu_vars.selected_world_name = k:match "/([^/]+)$" or "<unknown world>"
|
|
local conf = Settings(state.menu_vars.selected_world.."/world.mt")
|
|
local de = conf:get("enable_damage")
|
|
if de == "true" then
|
|
state.menu_vars.damage_enabled = true
|
|
elseif de == "false" then
|
|
state.menu_vars.damage_enabled = false
|
|
else
|
|
state.menu_vars.creative_damage = not state.current_game.disabled_settings.damage
|
|
end
|
|
local cm = conf:get("creative_mode")
|
|
if cm == "true" then
|
|
state.menu_vars.creative_enabled = true
|
|
elseif cm == "false" then
|
|
state.menu_vars.creative_enabled = false
|
|
else
|
|
state.menu_vars.creative_enabled = not not state.current_game.disabled_settings.creative
|
|
end
|
|
show_game_menu()
|
|
elseif k == ".unoverlay_dialog" then
|
|
show_game_menu {
|
|
unoverlay_dialog = true
|
|
}
|
|
elseif k:sub(1, string.len(".set_")) == ".set_" then
|
|
local name, value = k:match "%.set_(.+)_to_(.*)"
|
|
state.menu_vars[name] = value == "" and "0" or value
|
|
show_game_menu()
|
|
end
|
|
end
|
|
elseif state.loc == "about" then
|
|
if data.docs_section then
|
|
show_about_menu()
|
|
end
|
|
elseif state.loc == "settings" then
|
|
if data.show_category then
|
|
state.settings_category = data.show_category
|
|
show_settings_menu()
|
|
else
|
|
for k, v in pairs(data) do
|
|
|
|
end
|
|
end
|
|
end
|
|
if data.to_meta_menu then
|
|
state.current_game = nil
|
|
state.loc = "games"
|
|
show_meta_menu()
|
|
elseif data.show_servers_view then
|
|
state.current_game = nil
|
|
state.loc = "servers"
|
|
show_servers_menu()
|
|
elseif data.show_content_view then
|
|
state.current_game = nil
|
|
state.loc = "content"
|
|
show_content_menu()
|
|
elseif data.show_about_view then
|
|
state.current_game = nil
|
|
state.loc = "about"
|
|
show_about_menu()
|
|
elseif data.show_settings_view then
|
|
state.current_game = nil
|
|
state.loc = "settings"
|
|
show_settings_menu()
|
|
end
|
|
end
|
|
|
|
function minetest.event_handler(ev)
|
|
-- When Esc is pressed, close the current dialog, or the game if we are on the meta menu.
|
|
if ev == "MenuQuit" then
|
|
if state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players then
|
|
state.joining_server = nil
|
|
state.connecting_to_server = nil
|
|
state.showing_server_mods = nil
|
|
state.showing_server_players = nil
|
|
show_servers_menu()
|
|
elseif state.loc ~= "games" then
|
|
show_meta_menu()
|
|
else
|
|
minetest.close()
|
|
end
|
|
end
|
|
end
|
|
|
|
state.theme = build_stylesheet("")
|
|
|
|
minetest.set_clouds(false)
|
|
|
|
if last_game then
|
|
state.current_game = get_game_info(last_game)
|
|
if state.current_game then
|
|
state.menu_current = {"main"}
|
|
state.loc = "game"
|
|
show_game_menu()
|
|
else
|
|
show_meta_menu()
|
|
end
|
|
else
|
|
show_meta_menu()
|
|
end
|
|
|
|
--]]
|