diff --git a/mainmenu/init.lua b/mainmenu/init.lua
index 43bcf6d..c1d8bf1 100644
--- a/mainmenu/init.lua
+++ b/mainmenu/init.lua
@@ -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 = [[
enable_clouds = true
-@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;]
@@ -99,7 +112,7 @@ local default_game_menu = [[
@endforeach:myloop
]]
-
+
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:
```
+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/'
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