Update mainmenu/init.lua

This commit is contained in:
Signal 2025-06-28 15:16:16 +00:00
parent a577b070e9
commit e4bea7c082

View file

@ -12,18 +12,17 @@ size = window.max_formspec_size
default_textures = minetest.get_texturepath_share().."/base/pack/"
--FIXME: Assets
local assets = "/Users/iboettcher/eclipse-workspace/mods/mtmenu/"
local assets = os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"
local version = minetest.get_version()
-- Holds all the interface parts
-- This is where all view-specific state information goes.
state = {}
local fe = minetest.formspec_escape
local hte = minetest.hypertext_escape
--FIXME: Replace with /content because pause menu
dofile(minetest.get_builtin_path().."mainmenu/settings/settingtypes.lua")
dofile(minetest.get_builtin_path().."common/settings/settingtypes.lua")
---[[
@ -37,7 +36,6 @@ minetest.set_formspec_prepend("\
style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0]\
")
-- Prepended to the meta menu's formspec.
local meta_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
@ -47,7 +45,6 @@ local meta_header = "formspec_version[8]\
"
-- box[0,0;"..size.x..","..size.y..";]\
-- Prepended to game menus' formspecs.
local game_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
@ -65,16 +62,20 @@ local content_header = "formspec_version[8]\
bgcolor[#0000;true;#151618]\
"
-- The default main menu for games.
local default_game_menu = [[
<meta>
enable_clouds = true
</meta>
<main>
@set:test:Hello
label[2,2;Test]
scroll_container[1,6;4,2;worldscroll;vertical;;0,0]
@foreach:$WORLDS:worlds
@if:@selected_world:fi2
@set:list_width:@WIDTH * 0.3
@else:fi2
@set:list_width:@WIDTH * 0.8
@endif:fi2
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_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
style[.select_world_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
@ -84,9 +85,21 @@ local default_game_menu = [[
style[.select_world_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
style[.select_world_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
@endif:sel
button[0,${@i * 0.5};4,0.5;.select_world_${@name};${@test}]
button[0,${(@i +-1) * 0.5};${@list_width},0.5;.select_world_${@name};${@name}]
@endforeach:worlds
scroll_container_end[]
@if:@selected_world:fi
box[${@WIDTH * 0.4 +-0.05},${@HEIGHT * 0.1};0.1,${@HEIGHT * 0.8};#292d2fff]
scroll_container[${@WIDTH * 0.4},${@HEIGHT * 0.1};${@WIDTH * 0.8},${@HEIGHT * 0.8};worldmodsscroll;vertical;;0,0]
@foreach:@WORLDMODS:wm
label[0,${@i};${@name}]
@endforeach:wm
scroll_container_end[]
scrollbar[-800,6;0,2;vertical;worldmodsscroll;]
@else:fi
@endif:fi
scrollbaroptions[arrows=hide]
scrollbar[-800,6;0,2;vertical;worldscroll;]
</main>
@ -99,7 +112,7 @@ local default_game_menu = [[
@endforeach:myloop
</addworld>
]]
function core.on_before_close()
--minetest.settings:write()
end
@ -117,11 +130,6 @@ end
minetest.async_event_handler = handle_job
function minetest.handle_async(func, parameter, callback)
-- Serialize function
local serialized_func = string.dump(func)
assert(serialized_func ~= nil)
-- Serialize parameters
local serialized_param = minetest.serialize(parameter)
@ -129,7 +137,7 @@ function minetest.handle_async(func, parameter, callback)
return false
end
local jobid = minetest.do_async_callback(serialized_func, serialized_param)
local jobid = minetest.do_async_callback(func, serialized_param)
minetest.async_jobs[jobid] = callback
@ -173,13 +181,44 @@ local function get_worlds_for_game(id)
local out = {}
for _, x in ipairs(minetest.get_worlds()) do
if x.gameid == id then
if math.random() > 0.5 then x.selected = true end
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
function get_mods_for_world(world)
local config = minetest.check_mod_configuration(world)
for _, x in ipairs(config.unsatisfied_mods) do
x.unsatisfied = true
config.satisfied_mods[#config.satisfied_mods +1] = x
end
return config.satisfied_mods
end
-- Returns a list of content available for download.
function get_available_content()
local version = minetest.get_version()
@ -357,6 +396,10 @@ will define a dialog named 'main', with a label in it, and a dialog named 'other
with a button in it. Every menu file must have a dialog named 'main', which serves
as the entry point of the game's menu.
Note that to prevent dialog definitions from conflicting with the contents of
hypertext[] elements, they must not have leading whitespace before the opening
delimiter.
Being able to define these other dialogs would be pretty pointless if they couldn't
be used for anything. Accordingly, you can use standard formspec actions, e.g. buttons,
to segue to a different dialog. To do this, set the action name to
@ -393,9 +436,13 @@ file's top-level object will correspond to a variable. (Note that variable names
only contain letters or numbers, even if mapped from a JSON file.)
The menu-provided lists are:
- $WORLDS: The list of worlds for this game. Exposes @name (the world name),
- @WORLDS: The list of worlds for this game. Exposes @name (the world name),
@path (the world's full absolute path), @gameid (the ID of the current game),
and @selected (whether the world is currently selected).
- @MODS: The list of installed mods that are not incompatible with this game.
Exposes @name, @title, @description, @author, @path, @depends, and @optional_depends.
- @WORLDMODS: The list of mods installed on the current world. No-op if no world
is selected.
Expressions also support rudimentary mathematical operations, namely addition (+),
subtraction (+-), multiplication (*), division (/), and exponentiation (^). Trying
@ -436,6 +483,12 @@ Example:
@endif:<name>
```
Notes:
- The only uniqueness requirements for the name of a block is that it not be
the name of a statment of the same type contained in the body of that block.
This is so that the parser knows which `end` belongs to which block without
having to manage state.
Note: Because of the way the main menu works, image paths must be specified in full.
To make this non-painful, when referencing images use '$ASSET_PATH/<image name>'
instead of just the image name. $ASSET_PATH will be replaced with the actual path
@ -444,7 +497,7 @@ there. You can use $ASSET_PATH in any context. Additionally, $DEFAULT_ASSET_PATH
the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, in
case you need to stylistically unset a background image defined by a global style.
--]]
local function build_template_dialog(fs)
local function build_template_dialog(fs, depth)
if fs:trim() == "" then return end
local dialog = {}
local i = 0
@ -452,43 +505,44 @@ local function build_template_dialog(fs)
-- Extract foreach loops
local prev = 1
while i < 1000 do
local unfound = 0
local a, b, pattern, name, content = fs:find("@foreach:([^:]+):(%w*)\n(.-)\n%s-@endforeach:%2", prev)
if not a then break end
-- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, a -1))
dialog[#dialog +1] = {
foreach = pattern:trim(),
name = name,
content = build_template_dialog(content:trim())
}
prev = b
i = i +1
end
if i > 0 then
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1))
else
-- Extract conditionals
prev = 0
while i < 1000 do
local a, b, expr, name, content, else_content = fs:find("@if:([^:]+):(%w*)\n(.-)\n%s-@else:%2\n(.-\n?)@endif:%2", prev)
if not a then break end
-- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
dialog[#dialog +1] = fs:sub(prev +1, a -1)
-- print(string.rep("-", 20)..(depth or 0))
if a then
-- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
dialog[#dialog +1] = build_template_dialog(fs:sub(prev, a -1), (depth or 0) +1)
dialog[#dialog +1] = {
condition = expr:trim(),
foreach = pattern:trim(),
name = name,
content = build_template_dialog(content:trim()),
else_content = build_template_dialog(else_content:trim())
content = build_template_dialog(content:trim(), (depth or 0) +1)
}
prev = b
i = i +1
else
unfound = unfound +1
end
if i > 0 then
dialog[#dialog +1] = fs:sub(prev +1)
local a, b, expr, name, content, else_content = fs:find("@if:([^:]+):(%w*)\n(.-)\n%s-@else:%2\n(.-\n?)@endif:%2", prev)
if a then
-- print("if "..expr.." ("..name..")\n"..content.."\nend if ("..name..")")
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, a -1), (depth or 0) +1)
dialog[#dialog +1] = {
condition = expr:trim(),
name = name,
content = build_template_dialog(content:trim(), (depth or 0) +1),
else_content = build_template_dialog(else_content:trim(), (depth or 0) +1)
}
prev = b
i = i +1
else
unfound = unfound +1
end
if unfound > 1 then break end
end
if prev > 1 then
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1))
end
if #dialog == 1 then dialog = dialog[1] end
if i == 0 then dialog = fs end
-- minetest.log(dump(dialog, " "))
@ -497,19 +551,20 @@ end
local function build_game_menu(input)
-- MARK: Environment variables
input = input
input = "\n"..input
:gsub("%$ASSET_PATH", fe(state.current_game.path.."/menu"))
:gsub("%$DEFAULT_ASSET_PATH", fe(assets:sub(1, #assets -1)))
:gsub("%$NO_IMAGE", fe(default_textures.."blank.png"))
:gsub("/%*.-%*/", "")
local menu = {}
for name, fs in input:gmatch("<(%l+)>(.-)</%1>") do
for name, fs in input:gmatch("%f[^\n]<(%l+)>(.-)%f[^\n]</%1>") do
if name == "meta" then
menu[name] = parse_conf_text(fs)
else
menu[name] = build_template_dialog(fs)
end
end
-- print(dump(menu, " "))
return menu
end
@ -629,13 +684,25 @@ local function evaluate_template_expression(expr, vars, depth)
-- Expand all variables so we can deal with a constexpr.
local offset = 1
while offset < 100000 do
local a, b, name = expr:find("@([%a]+)", offset)
local a, b, name = expr:find("@([%a_]+)", offset)
if not a then break end
-- If referencing an undefined variable, default to 0 because it's safest that way.
local result = minetest.formspec_escape(tostring(vars[name] or "0"))
expr = expr:sub(1, a -1)..result..expr:sub(b +1)
offset = a +#result
end
-- Condense sub-expressions.
local offset = 1
while offset < 100000 do
local a, b, se = expr:find("(%b())", offset)
if not a then break end
se = se:gsub("^%((.*)%)$", "%1")
local result = evaluate_template_expression(se, vars, depth +1)
expr = expr:sub(1, a -1)..result..expr:sub(b +1)
offset = a +#result
end
-- If there are no operators, this is a constant expression and we can just return.
if not expr:find("%p") then return expr end
@ -649,7 +716,7 @@ local function evaluate_template_block(fs, vars)
-- Assignment statements
local offset = 1
while offset < #fs do
local a, b, name, expr = fs:find("@set:(%w+):(.-)\n", offset)
local a, b, name, expr = fs:find("@set:([%a_]+):([^\n]+)", offset)
if not a then break end
vars[name] = evaluate_template_expression(expr, vars)
fs = fs:sub(1, a -1)..fs:sub(b +1)
@ -673,8 +740,17 @@ local function evaluate_template_foreach(loop, vars)
local list = {}
if loop.foreach:sub(1,1) == "[" then
list = loop.foreach:gsub("^%[(.*)%]$", "%1"):split(",")
elseif loop.foreach == "$WORLDS" then
list = get_worlds_for_game(state.current_game.id)
elseif loop.foreach:sub(1, 1) == "@" then
local var = loop.foreach:sub(2)
if var == "WORLDS" then
list = get_worlds_for_game(state.current_game.id)
elseif var == "MODS" then
list = get_mods_for_game(state.current_game.id)
elseif var == "WORLDMODS" then
list = state.current_world and get_mods_for_world(state.current_world) or {}
else
list = vars[var]
end
end
for i, x in ipairs(list) do
local vars2 = {}
@ -704,21 +780,19 @@ local function evaluate_template_conditional(cond, vars)
return out
end
-- Game dialogs might contain e.g. foreach loops, so build those if needed.
-- Process the syntax tree for a game dialog and output the resulting string.
function evaluate_game_dialog(dialog, vars)
local out = ""
if type(dialog) == "string" then return evaluate_template_block(dialog, vars) end
for _, x in ipairs(dialog) do
if type(x) == "string" then
out = out..evaluate_template_block(x, vars)
elseif x.condition then
out = out..evaluate_template_conditional(x, vars)
elseif x.foreach then
out = out..evaluate_template_foreach(x, vars)
else
for _, c in ipairs(x) do
out = out..evaluate_game_dialog(c, vars)
end
if not dialog then return out end
if type(dialog) == "string" then
return evaluate_template_block(dialog, vars)
elseif dialog.condition then
out = out..evaluate_template_conditional(dialog, vars)
elseif dialog.foreach then
out = out..evaluate_template_foreach(dialog, vars)
else
for _, c in ipairs(dialog) do
out = out..evaluate_game_dialog(c, vars)
end
end
return out
@ -763,12 +837,9 @@ function show_meta_menu(v)
local dist = i -idx
local scale = 4 /(math.abs(dist) +1)
if scale > 0.1 then
--This looks cool, but is useless for UI purposes: center +10^(1 /math.abs(dist)) *math.sign(dist)
--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)
local test = io.open(x.menuicon_path)
if test then
test:close()
else
if x.menuicon_path == "" then
x.menuicon_path = assets.."games.png"
end
--TODO: Do these need some kind of background?
@ -852,7 +923,7 @@ function show_game_menu(args)
end
end
end
if #backgrounds > 0 then
local path = game.path.."/menu/"..backgrounds[math.random(#backgrounds)]
if minetest.set_background("background", path) then
@ -898,6 +969,7 @@ function show_game_menu(args)
for i, x in ipairs(state.menu_current) do
fs = fs..evaluate_game_dialog(game.menu[x], setmetatable({WIDTH = size.x, HEIGHT = size.y}, {__index = state.menu_vars}))
end
-- print(fs)
minetest.update_formspec(game_header..fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
@ -912,6 +984,23 @@ function add_favorite_server(address, port)
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)
@ -934,6 +1023,9 @@ function refresh_server_list()
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
@ -1363,15 +1455,18 @@ function show_content_menu()
fs = fs.."\
box[0,0;"..size.x..",1;#403e39ff]\
box[0,1;"..size.x..",0.1;#292d2fff]\
style[content_search,content_cancel;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
style[content_search,content_cancel;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 -7)..",0;4,1;test;Browse Online Content...]\
button["..(w -3)..",0;3,1;test;Add Content...]\
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)
@ -1392,7 +1487,8 @@ function show_content_menu()
end
function minetest.button_handler(data)
state.menu_vars = 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
@ -1422,11 +1518,16 @@ function minetest.button_handler(data)
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
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()
@ -1449,23 +1550,23 @@ function minetest.button_handler(data)
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)
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
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:lower()
port = state.current_server.port
end
local is_favorite = false
for _, x in ipairs(state.favorite_servers) do
@ -1523,10 +1624,17 @@ function minetest.button_handler(data)
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_>"))
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
end