Initial commit.

This commit is contained in:
Signal 2026-02-10 21:46:25 -05:00
commit ce6cb893bf
58 changed files with 3866 additions and 0 deletions

87
views/about.lua Normal file
View file

@ -0,0 +1,87 @@
local function get_credits()
local f = assert(io.open(core.get_mainmenu_path() .. "/credits.json"))
local json = core.parse_json(f:read("*all"))
f:close()
return json
end
local function get_renderer_info()
local out = {}
local renderer = core.get_active_renderer()
if renderer:sub(1, 7) == "OpenGL " then
renderer = renderer:sub(8)
end
local m = renderer:match("^[%d.]+")
if not m then
m = renderer:match("^ES [%d.]+")
end
out[#out + 1] = m or renderer
out[#out + 1] = core.get_active_driver():lower()
out[#out + 1] = core.get_active_irrlicht_device():upper()
return table.concat(out, " / ")
end
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor(theme.styles.bg_color, true)
if not state.credits then
local credits = get_credits()
state.credits = table.concat({
"<global halign=center color=", theme.styles.text_color, ">",
"<tag name=muted color=", theme.styles.text_color_muted, ">",
"<tag name=link color=", theme.styles.link_color, ">",
"<tag name=heading color=", theme.styles.heading_color, " size=24>",
"<heading>", fgettext_ne("Core Developers"), "</heading>\n",
core.hypertext_escape(table.concat(credits.core_developers, "\n")),
"\n\n<heading>", fgettext_ne("Core Team"), "</heading>\n",
core.hypertext_escape(table.concat(credits.core_team, "\n")),
"\n\n<heading>", fgettext_ne("Active Contributors"), "</heading>\n",
core.hypertext_escape(table.concat(credits.contributors, "\n")),
"\n\n<heading>", fgettext_ne("Previous Core Developers"), "</heading>\n",
core.hypertext_escape(table.concat(credits.previous_core_developers, "\n")),
"\n\n<heading>", fgettext_ne("Previous Contributors"), "</heading>\n",
core.hypertext_escape(table.concat(credits.previous_contributors, "\n")),
"\n\n",
}):gsub("%[.-%]", "<muted>%1</muted>"):gsub("\\<.-\\>", "<link>%1</link>")
end
local header_height = size.x * (72/672)
imfs.image(0, 0.2, size.x, header_height, default_textures.."menu_header.png")
imfs.hypertext(0, header_height + 0.3, "100%", 0.5, "<global halign=center valign=middle color="..theme.styles.text_color.."><b>Version "..version.string.."</b>")
imfs.hypertext(0, header_height + 0.9, "100%", "100% - "..(header_height + 0.9), state.credits)
imfs.button("100% - 2.2", 0.2, 2, 0.75, "Back")
:onclick(function()
show_meta_menu()
end)
imfs.button(0.2, 0.2, 3, 0.75, "API reference")
:onclick(function()
end)
imfs.button(0.2, 1.05, 3, 0.75, "Website")
:onclick(function()
core.open_url("https://luanti.org")
end)
imfs.button(0.2, 1.9, 3, 0.75, "Open data directory")
:onclick(function()
core.open_dir(core.get_user_path())
end)
imfs.box(0, "100% - 0.45", 6, 0.45, theme.styles.bg_color)
imfs.label(0.2, "100% - 0.225", fgettext_ne("Active renderer:").." "..get_renderer_info())
local offset = math.abs(size.x - size.y)
imfs.image(size.x > size.y and offset / 2 or 0, size.x < size.y and offset / 2 or 0, math.min(size.x, size.y), math.min(size.x, size.y), default_textures.."logo.png^[opacity:32")
return imfs.end_()
end

236
views/content.lua Normal file
View file

@ -0,0 +1,236 @@
local function search_content(state, search)
local out = {}
for i = 1, #state.content do
local pkg = state.content[i]
if pkg.name:find(search, 1, true) then
out[#out + 1] = pkg
end
end
return out
end
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor(theme.styles.bg_color, true)
if not state.content then
state.content = package.all()
state.content_shown._val = state.content
end
local current_package = state.current_package()
imfs.box(0, 0, "100%", current_package and 1 or 2, theme.styles.container.bg_color)
imfs.row(0.2, 0.125, "100% - 0.4", 0.75)
:gap(0.1)
theme.field(0, 0, "1x", "100%", state.search)
:onchange(function(value)
state.search = value
end)
:onenter(function()
state.page(1)
state.content_shown(search_content(state, state.search))
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("search"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Search content")
:onclick(function()
state.page(1)
state.content_shown(search_content(state, state.search))
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("cancel"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Clear search")
:onclick(function()
state.search = ""
state.page(1)
state.content_shown(state.content)
end)
imfs.button(0, 0, 3.5, 0.75, "Check for updates")
:onclick(function()
end)
imfs.button(0, 0, 2, 0.75, "Back")
:onclick(function()
if current_package then
state.current_package(false)
else
show_meta_menu()
end
end)
imfs.row_end()
imfs.box(0, 0.95, "100%", 0.1, theme.styles.container.border_color)
if current_package then
imfs.hypertext(0.25, 1.05, "100% - 0.5", "100% - 1.05", "\
<global color="..theme.styles.text_color..">\
<tag name=action color="..theme.styles.link_color.." hovercolor="..theme.styles.link_color_hovered..">\
"..markdown_to_hypertext(read_file(current_package.path.."/README.md") or ""))
else
imfs.row(0.2, 1.125, "100% - 0.4", 0.75)
:gap(0.1)
imfs.group(0, 0, "1x", 0.75)
imfs.image(0, 0, "100%", "100%", theme.get_background_image("button"), "8,8")
imfs.row(0.15, 0.05, "100% - 0.3", "100% - 0.13")
local show_type = state.show_type()
imfs.button(0, 0, "1x", "100%", "All")
:style(show_type == "all" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "all" then
state.show_type("all")
state.page(1)
state.content = package.all()
if state.search ~= "" then
state.content_shown(search_content(state, state.search))
else
state.content_shown(state.content)
end
end
end)
imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color)
imfs.button(0, 0, "1x", "100%", "Games")
:style(show_type == "games" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "games" then
state.show_type("games")
state.page(1)
state.content = package.games()
if state.search ~= "" then
state.content_shown(search_content(state, state.search))
else
state.content_shown(state.content)
end
end
end)
imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color)
imfs.button(0, 0, "1x", "100%", "Mods")
:style(show_type == "mods" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "mods" then
state.show_type("mods")
state.page(1)
state.content = package.mods()
if state.search ~= "" then
state.content_shown(search_content(state, state.search))
else
state.content_shown(state.content)
end
end
end)
imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color)
imfs.button(0, 0, "1x", "100%", "Texture Packs")
:style(show_type == "texturepacks" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "texturepacks" then
state.show_type("texturepacks")
state.page(1)
state.content = package.texturepacks()
if state.search ~= "" then
state.content_shown(search_content(state, state.search))
else
state.content_shown(state.content)
end
end
end)
imfs.row_end()
imfs.group_end()
imfs.button(0, 0, 3.5, 0.75, "Browse online content...")
:onclick(function()
show_online_content_menu()
end)
imfs.button(0, 0, 2.5, 0.75, "New package...")
:onclick(function()
end)
imfs.row_end()
imfs.box(0, 1.95, "100%", 0.1, theme.styles.container.border_color)
local content = state.content_shown()
if #content == 0 then
imfs.hypertext(0, 2.05, "100%", 2, [[
<global halign=center valign=middle color=#444>
<style size=36><b>No results.</b></style>
]])
else
local num_per_page = math.floor(size.x / 3) * math.floor((size.y - 2) / 3)
local pages = math.ceil(#content / num_per_page)
imfs.scroll_container(0, 2.05, "100%", "100% - 2.55", "horizontal", 0, "")
:scrollbar(function()
imfs.scrollbar(-800, -800, 0, 0, "horizontal", state.page)
:options({
min = 1,
max = pages,
smallstep = 1
})
:onchange(function(action, value)
if action == "CHG" then
state.page(value)
end
end)
end)
local x = 0
local y = 0
local start = (state.page() - 1) * num_per_page
for i = start + 1, start + num_per_page do
local pkg = 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 = theme.icon("menu_content")
end
end
imfs.button(x + 0.125, y, 2.5, 2.5)
:image(pkg.icon)
:tooltip(pkg.title ~= "" and pkg.title or pkg.name)
:onclick(function()
state.current_package(pkg)
end)
x = x + 3
if x + 2.5 > size.x then
x = 0
y = y + 3
end
end
imfs.scroll_container_end()
imfs.row(0, "100% - 0.375", "100%", 0.25)
:align("center")
for i = 1, pages do
imfs.button(0, 0, 0.25, 0.25)
:image(theme.icon(i == state.page() and "circle_active" or "circle"))
:onclick(function()
state.page(i)
end)
end
imfs.row_end()
end
end
return imfs.end_()
end

8
views/docs.lua Normal file
View file

@ -0,0 +1,8 @@
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor(theme.styles.bg_color, true)
return imfs.end_()
end

0
views/game.lua Normal file
View file

129
views/meta_menu.lua Normal file
View file

@ -0,0 +1,129 @@
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor("#151618", true)
imfs.image(0, size.y * 0.08, size.x, size.x * (72/672), default_textures.."menu_header.png")
local games = state.games
if #games < 1 then
imfs.hypertext(0, 0, size.x, size.y, [[
<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 imfs.end_()
end
imfs.scroll_container(0, 0, size.x, size.y, "horizontal", 0, "")
:scrollbar(function()
imfs.scrollbar(-800, -800, 0, 0, "horizontal", state.scroll_pos)
:options({
min = 1,
max = #games,
smallstep = 1,
})
:onchange(function(action, value)
if action == "CHG" then
state.scroll_pos(value)
end
end)
end)
local center = size.x / 2
for i, x in ipairs(games) do
imfs.button(center + (i - #games / 2) * 0.25, size.y - 0.5, 0.25, 0.25)
:image(theme.icon(i == state.scroll_pos() and "circle_active" or "circle"))
:onclick(function()
state.scroll_pos(i)
end)
local dist = i - state.scroll_pos()
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 = theme.icon("games")
end
--TODO: Do these need some kind of background?
imfs.button(lc - scale /2, size.y / 2 - scale / 2 + (4 - scale) / 2 + 2, scale, scale)
:image(x.menuicon_path)
:tooltip(x.title, "#444", "#aaa")
if dist == 0 then
imfs.button(lc - scale / 2, size.y / 2 + 4, 4, 0.5, x.title)
:styles(styles_no_background)
end
end
end
imfs.scroll_container_end()
imfs.button("5%", "30%", 4, 4)
:style({
border = false,
bgimg = theme.icon("menu_servers"),
bgimg_middle = 0,
})
:style("hovered", {
bgimg = theme.icon("menu_servers_hovered"),
bgimg_middle = 0,
})
:style("pressed", {
bgimg = theme.icon("menu_servers_hovered"),
bgimg_middle = 0,
})
:onclick(show_servers_menu)
imfs.button("5%", size.y * 0.3 + 4, 4, 0.5, "Servers")
:styles(styles_no_background)
imfs.button(size.x * 0.95 - 4, "30%", 4, 4)
:style({
border = false,
bgimg = theme.icon("menu_content"),
bgimg_middle = 0,
})
:style("hovered", {
bgimg = theme.icon("menu_content_hovered"),
bgimg_middle = 0,
})
:style("pressed", {
bgimg = theme.icon("menu_content_hovered"),
bgimg_middle = 0,
})
:onclick(show_content_menu)
imfs.button(size.x * 0.95 - 4, size.y * 0.3 + 4, 4, 0.5, "Content")
:styles(styles_no_background)
imfs.button(1, 0, 2, 0.5, "About")
:style({
bgimg = theme.get_background_image("tab")
})
:style("hovered", {
bgimg = theme.get_background_image("tab", "hovered")
})
:style("pressed", {
bgimg = theme.get_background_image("tab", "active"),
})
:onclick(show_about_menu)
imfs.button(size.x - 3, 0, 2, 0.5, "Settings")
:style({
bgimg = theme.get_background_image("tab"),
})
:style("hovered", {
bgimg = theme.get_background_image("tab", "hovered"),
})
:style("pressed", {
bgimg = theme.get_background_image("tab", "active"),
})
:onclick(show_settings_menu)
return imfs.end_()
end

231
views/online_content.lua Normal file
View file

@ -0,0 +1,231 @@
local function fetch_content()
local url = core.settings:get("contentdb_url")
.."/api/packages/?type=mod&type=game&type=txp&protocol_version="
..core.get_max_supp_proto().."&engine_version="
..core.urlencode(core.get_version().string)
for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
item = item:trim()
if item ~= "" then
url = url .. "&hide=" .. core.urlencode(item)
end
end
local http = core.get_http_api()
local response = http.fetch_sync({
url = url,
extra_headers = {
core.get_http_accept_languages()
},
})
return core.parse_json(response.data)
end
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor(theme.styles.bg_color, true)
imfs.box(0, 0, "100%", 1, theme.styles.container.bg_color)
imfs.row(0.2, 0.125, "100% - 0.4", 0.75)
:gap(0.1)
imfs.group(0, 0, "1.25x", 0.75)
imfs.image(0, 0, "100%", "100%", theme.get_background_image("button"))
imfs.row(0.15, 0.05, "100% - 0.3", "100% - 0.13")
local show_type = state.show_type()
imfs.button(0, 0, "1x", "100%", "All")
:style(show_type == "all" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "all" then
state.show_type("all")
state.page(1)
-- state.content = package.all()
-- if state.search ~= "" then
-- state.content_shown(search_content(state, state.search))
-- else
-- state.content_shown(state.content)
-- end
end
end)
imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color)
imfs.button(0, 0, "1x", "100%", "Games")
:style(show_type == "games" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "games" then
state.show_type("games")
state.page(1)
-- state.content = package.games()
-- if state.search ~= "" then
-- state.content_shown(search_content(state, state.search))
-- else
-- state.content_shown(state.content)
-- end
end
end)
imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color)
imfs.button(0, 0, "1x", "100%", "Mods")
:style(show_type == "mods" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "mods" then
state.show_type("mods")
state.page(1)
-- state.content = package.mods()
-- if state.search ~= "" then
-- state.content_shown(search_content(state, state.search))
-- else
-- state.content_shown(state.content)
-- end
end
end)
imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color)
imfs.button(0, 0, "1x", "100%", "Texture Packs")
:style(show_type == "texturepacks" and style_borderless_alt or style_borderless)
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
:onclick(function()
if show_type ~= "texturepacks" then
state.show_type("texturepacks")
state.page(1)
-- state.content = package.texturepacks()
-- if state.search ~= "" then
-- state.content_shown(search_content(state, state.search))
-- else
-- state.content_shown(state.content)
-- end
end
end)
imfs.row_end()
imfs.group_end()
theme.field(0, 0, "1x", "100%")
:onchange(function(value)
state.search = value
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("search"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Search content")
:onclick(function()
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("cancel"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Clear search")
:onclick(function()
end)
imfs.button(0, 0, 2, "100%", "Back")
:onclick(function()
show_content_menu()
end)
imfs.row_end()
imfs.box(0, 0.95, "100%", 0.1, theme.styles.container.border_color)
if not state.content_shown() then
imfs.row(0, "50% - 0.25", "100%", 0.5)
:align("center")
imfs.image(0, 0, 1, 0.5, theme.icon("loader"))
:animated(4, 200)
imfs.row_end()
if not state.loading then
core.handle_async(fetch_content, nil, function(content)
state.content = content
state.num_content = #content
state.content_shown(state.content)
state.loading = false
end)
state.loading = true
end
return imfs.end_()
end
local content = state.content_shown()
local num_per_page = math.floor((size.x + 0.5) / 5.5) * math.floor((size.y - 1.05) / 3.5)
local excess_x = (size.x + 0.5) % 5.5
local excess_y = (size.y - 1.05) % 3.5
imfs.scroll_container(0, 1.05, "100%", "100% - 1.55", "horizontal", 0, "")
:scrollbar(function()
imfs.scrollbar(0, size.y - 0.5, size.x, 0.5, "horizontal", state.page())
:options({
min = 1,
max = math.ceil(state.num_content / num_per_page),
smallstep = 1
})
:onchange(function(action, value)
if action == "CHG" then
state.page(value)
end
end)
end)
local x = 0
local y = 0
local start = (state.page() - 1) * num_per_page
local i = start
local idx = start
while idx < start + num_per_page do
i = i + 1
local pkg = content[i]
if not pkg then break end
if not package.is_installed(pkg.type, pkg.name) then
idx = idx + 1
imfs.group(x + excess_x / 2, y + excess_y / 2, 5, 3)
imfs.image(0, 0, "100%", "100%", package.screenshot(pkg))
imfs.box(0, 0, "100%", "100%", "#000a")
imfs.hypertext(0.1, 0.1, "100% - 0.2", "100% - 0.2", string.format(
[[<global color=#fff><style size=24><b>%s</b></style>
<b> by %s</b>
%s]],
core.hypertext_escape(pkg.title or pkg.name),
pkg.author,
pkg.short_description
))
:style(".scrollbar", {
size = "0"
})
imfs.button(0, 0, "100%", "100%")
:styles(styles_no_background)
:style("hovered", {
bgimg = "[fill:1x1:0,0:#fff",
bgcolor = "#0004"
})
:style("pressed", {
bgimg = "[fill:1x1:0,0:#fff",
bgcolor = "#0004"
})
:onclick(function()
end)
imfs.group_end()
x = x + 5.5
if x + 5 > size.x then
x = 0
y = y + 3.5
end
end
end
imfs.scroll_container_end()
return imfs.end_()
end

562
views/servers.lua Normal file
View file

@ -0,0 +1,562 @@
local 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
local 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
local function search_server_list(state, input)
if input:trim() == "" then
return nil
end
local search_str = ""
local words = {}
local mods = {}
local game
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
game = x:sub(6)
else
words[#words + 1] = x
end
end
local out = {}
for _, x in ipairs(state.servers) 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 game and x.gameid and game ~= x.gameid:lower():trim() then
passed = false
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():trim() == 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
local function refresh_server_list(state)
core.handle_async(
function(param)
local http = core.get_http_api()
local url = string.format("%s/list?proto_version_min=%d&proto_version_max=%d",
"https://servers.luanti.org" or core.settings:get("serverlist_url"),
core.get_min_supp_proto(),
core.get_max_supp_proto()
)
local response = http.fetch_sync({url = url})
if not response.succeeded then
return {}
end
local retval = core.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.servers = list
if state.search ~= "" then
state.servers_filtered = search_server_list(state, state.search)
list = state.servers_filtered
end
state.serverlist(list)
end
)
end
local function join_server(state)
core.settings:set("name", state.username())
local address
local port
if state.joining() == true then
if not tonumber(state.port()) then
state.join_error("Invalid port.")
return
end
address = state.address():lower()
port = state.port():lower()
else
address = state.joining().address:lower()
port = state.joining().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
core.settings:set("address", address)
core.settings:set("remote_port", port)
gamedata = {
playername = state.username(),
password = state.password(),
address = address,
port = port,
selected_world = 0,
singleplayer = false
}
core.start()
end
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor(theme.styles.bg_color, true)
local loading
if not state.serverlist() then
loading = true
local favorite_servers = core.parse_json(read_file(core.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._val = favorite_servers
refresh_server_list(state)
end
local enable_tooltips = not (state.joining() or state.detail_view())
local list = state.serverlist()
imfs.row("10%", size.y * 0.1 - 0.85, "80%", 0.75)
:gap(0.1)
theme.field(0, 0, "1x", "100%", state.search)
:onchange(function(value)
state.search = value
end)
:onenter(function(value)
state.search = value
state.servers_filtered = search_server_list(state, state.search)
state.serverlist(state.servers_filtered)
state.scroll_pos(1)
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("search"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Search server list")
:onclick(function()
state.servers_filtered = search_server_list(state, state.search)
state.serverlist(state.servers_filtered)
state.scroll_pos(1)
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("cancel"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Clear search")
:onclick(function()
state.search = ""
state.servers_filtered = nil
state.serverlist(state.servers)
state.scroll_pos(1)
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("refresh"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Refresh server list")
:onclick(function()
refresh_server_list(state)
end)
imfs.button(0, 0, 3, 0.75, "Direct Connection...")
:onclick(function()
state.joining(true)
end)
imfs.row_end()
local border_width = pixel_to_formspec(theme.styles.container.border_width)
imfs.image("10%", "10%", "80%", "80%", theme.get_background_image("container"))
imfs.group("10% + "..border_width, "10% + "..border_width, "80% - "..(border_width * 2), "80% - "..(border_width * 2))
imfs.scroll_container(0, 0, state.detail() and "65% - 0.05" or "100%", "100%", "vertical", 0, "")
:scrollbar(function()
imfs.scrollbar(state.detail() and "65% - 0.55" or "100% - 0.5", 0, 0.5, "100%", "vertical", state.scroll_pos)
:options({
min = 1,
max = #list,
smallstep = 1,
thumbsize = 0,
})
:onchange(function(action, value)
if action == "CHG" then
state.scroll_pos(value)
end
end)
end)
imfs.scope("servers")
local height = size.y * 0.8
local i = 0
local idx = state.scroll_pos()
while true do
-- We must use a while loop here because we skip incompatible servers,
-- and we have no way of knowing how many there will be until we iterate.
if i > math.ceil(height / 0.5) then break end
local x = list[idx]
-- End early if we've run out of list.
if not x then break end
-- 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 = core.colorize("#888", x.address..":"..x.port)
end
imfs.button(0, i * 0.5, "100%", 0.5)
:style({
border = false,
bgimg = "[fill:1x1:0,0:#fff",
bgimg_middle = 0,
bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt,
})
:style("hovered", {
border = false,
bgimg = "[fill:1x1:0,0:#fff",
bgimg_middle = 0,
})
:style("pressed", {
border = false,
bgimg = "[fill:1x1:0,0:#fff",
bgimg_middle = 0,
})
:onclick(function()
state.detail(x)
end)
imfs.row(0.1, i * 0.5, "100% - 0.65", 0.5)
if x.favorite then
imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_favorite"))
:tooltip("Favorite server", enable_tooltips)
end
local name = x.name and x.name:trim() or ""
if name == "" then
name = core.colorize("#888", x.address..":"..x.port)
end
-- We use an arealabel to ensure that the name will be clipped
-- if it is inordinately long, rather than overflowing onto
-- other thigs.
imfs.arealabel(0, 0.125, "1x", 0.375, name)
if x.pvp == false then
imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_peaceful"))
:tooltip("Peaceful", enable_tooltips)
end
if x.creative then
imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_creative"))
:tooltip("Creative", enable_tooltips)
end
if x.clients then
local clients = x.clients..(
x.clients_max and "/"..x.clients_max or ""
)
local color = "#aaa"
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
imfs.hypertext(0, 0, 1, 0.5, "<global valign=middle halign=center color="..color..">"..core.hypertext_escape(clients))
end
if x.ping or x.lag then
imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_icon_ping_"..ping_lvl))
:tooltip("Ping: "..math.floor(lag), enable_tooltips)
end
imfs.row_end()
i = i + 1
end
idx = idx + 1
end
imfs.scope_end()
if loading then
imfs.hypertext(0, i * 0.5, "100%", 0.5, "<global valign=middle halign=center color=#777>Loading...")
end
imfs.scroll_container_end()
local detail = state.detail()
if detail then
imfs.box("65% - 0.05", 0, 0.1, "100%", theme.styles.container.border_color)
imfs.column("65% + 0.1", border_width, "35% - "..(border_width + 0.1), "100% - "..(border_width * 2))
:gap(0.1)
imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa><b>"..core.hypertext_escape(detail.name or detail.address..":"..detail.port).."</b>")
if detail.mods or detail.clients_list or detail.favorites then
imfs.row(0.1, 0, "100% - 0.2", 0.5)
if detail.mods then
imfs.button(0, 0, 0.5, 0.5)
:image(theme.icon("menu_servers_mods"))
:tooltip("Show mod list")
:onclick(function()
state.detail_view("mod_list")
end)
end
if detail.clients_list then
imfs.button(0, 0, 0.5, 0.5)
:image(theme.icon("menu_servers_players"))
:tooltip("Show player list")
:onclick(function()
state.detail_view("player_list")
end)
end
if detail.favorite then
imfs.button(0, 0, 0.5, 0.5)
:image(theme.icon("menu_servers_unfavorite"))
:tooltip("Remove from favorites")
:onclick(function()
detail.favorite = nil
remove_favorite_server(detail.address, detail.port)
-- Bump the serverlist to notify that we've changed it.
state.serverlist(state.serverlist)
end)
end
imfs.row_end()
end
imfs.hypertext(0.1, 0, "100% - 0.2", "1x", "<global color=#aaa>"..core.hypertext_escape(detail.description or ""))
imfs.button(0, 0, "100%", 0.75, "Join server")
:onclick(function()
state.joining(detail)
end)
imfs.column_end()
end
imfs.group_end()
imfs.button("100% - 1", 0.25, 0.75, 0.75, "<")
:onclick(function()
show_meta_menu()
end)
local joining = state.joining()
if joining then
imfs.box(0, 0, "100%", "100%", "#0008")
imfs.button(0, 0, "100%", "100%")
:styles(styles_no_background)
:onclick(function()
state.joining(false)
end)
imfs.group("20%", "20%", "60%", "60%")
imfs.image(0, 0, "100%", "100%", theme.get_background_image("container"))
imfs.scroll_container(border_width + 0.1, border_width + 0.1, "100% - "..(border_width * 2 + 0.2), "100% - "..(border_width * 2 + 0.2))
imfs.column(0, 0, "100%", 100)
:gap(0.1)
if state.join_error() then
imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle color=#9d5b5b>"..core.hypertext_escape(state.server_connection_error.msg))
end
if joining == true then
imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa>Connecting to server...")
imfs.group(0, 0, "100%", 1.05)
imfs.image(0, 0.3, "100%", "100% - 0.3", theme.get_background_image("field"))
imfs.label(0.2, 0.125, "Address")
imfs.field(0.1, 0.3, "70% - 0.2", "100% - 0.3")
:onchange(function(value)
state.address(value)
end)
:onenter(function()
join_server(state)
end)
imfs.box("70% - 0.05", 0.3, 0.1, "100% - 0.3", theme.styles.field.border_color)
imfs.label("70% + 0.2", 0.125, "Port")
imfs.field("70% + 0.1", 0.3, "30% - 0.2", "100% - 0.3")
:onchange(function(value)
state.port(value)
end)
:onenter(function()
join_server(state)
end)
imfs.group_end()
else
imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa>Connecting to <b>"..core.hypertext_escape(joining.name or joining.address..":"..joining.port).."</b>...")
end
theme.field(0, 0, "100%", 0.75, "Username", state.username)
:onchange(function(value)
state.username(value)
end)
:onenter(function()
join_server(state)
end)
theme.field(0, 0, "100%", 0.75, "Password", "")
:password()
:onchange(function(value)
state.password(value)
end)
:onenter(function()
join_server(state)
end)
imfs.row(0, 0, "100%", 0.75)
:gap(0.1)
imfs.button(0, 0, "1x", "100%", "Cancel")
:onclick(function()
state.joining(false)
end)
imfs.button(0, 0, "1x", "100%", "Join")
:onclick(function()
join_server(state)
end)
imfs.row_end()
imfs.column_end()
imfs.scroll_container_end()
imfs.group_end()
elseif state.detail_view() then
imfs.box(0, 0, "100%", "100%", "#0008")
imfs.button(0, 0, "100%", "100%")
:styles(styles_no_background)
:onclick(function()
state.detail_view(false)
end)
imfs.image("20%", "20%", "60%", "60%", theme.get_background_image("container"))
imfs.group("20% + "..border_width, "20% + "..border_width, "60% - "..(border_width * 2), "60% - "..(border_width * 2))
imfs.scroll_container(0, 0, "100%", "100% - 0.8")
:scrollbar("100% - 0.5", 0, 0.5, "100% - 0.8", "vertical")
local list = state.detail_view() == "mod_list" and detail.mods or detail.clients_list
for i = 0, #list - 1 do
imfs.button(0, i * 0.5, "100%", 0.5)
:style({
border = false,
bgimg = "[fill:1x1:0,0:#fff",
bgimg_middle = 0,
bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt,
})
:style("hovered", style_borderless_hovered)
:style("pressed", style_borderless_hovered)
-- This isn't just the button label because we want left-alignment.
imfs.arealabel(0.1, i * 0.5 + 0.125, "100%", 0.375, list[i + 1])
end
imfs.scroll_container_end()
imfs.button(0, "100% - 0.75", "100%", 0.75, "Close")
:onclick(function()
state.detail_view(false)
end)
imfs.group_end()
end
return imfs.end_()
end

271
views/settings.lua Normal file
View file

@ -0,0 +1,271 @@
dofile(core.get_builtin_path().."common/settings/settingtypes.lua")
-- Some categories, like Accessibility and Theme, do not appear
-- in the builtin settingtypes.txt, so they are added to the state table here.
local function inject_settings(state)
-- Theme
table.insert(state.categories, 1, "Theme")
state.settings.Theme = {
{
comment = "The background color of containers.",
context = "client",
default = "#fff",
name = "theme.container_bg_color",
readable_name = "Container background color",
requires = {},
type = "string",
value = imfs.state("#fff")
}
}
-- Accessibility
table.insert(state.categories, 2, "Accessibility")
state.settings.Accessibility = {
{
comment = "Smooths rotation of camera, also called look or mouse smoothing. 0 to disable.",
context = "client",
default = "0.0",
max = 0.99,
min = 0,
name = "camera_smoothing",
readable_name = "Camera smoothing",
requires = {},
type = "float",
value = imfs.state("0.0")
}
}
end
local function search_settings(state, search)
if state.search == "" then
return state.settings, state.categories
end
local out = {}
for category, settings in pairs(state.settings) do
for i = 1, #settings do
if settings[i].name:find(search, 1, true) then
if not out[category] then
out[category] = {settings[i]}
else
table.insert(out[category], settings[i])
end
end
end
end
local categories = {}
for i = 1, #state.categories do
local category = state.categories[i]
if category.name then
for j = 1, #state.categories do
if out[category[j]] then
if not categories[category.name] then
table.insert(categories, {name = category.name})
end
table.insert(categories[#categories], category[j])
break
end
end
else
if out[category] then
table.insert(categories, category)
break
end
end
end
return out, categories
end
return function(state)
imfs.begin(size.x, size.y)
:padding(0, 0)
:bgcolor(theme.styles.bg_color, true)
if not state.settings then
state.settings = {}
state.categories = {}
local category
local settings = settingtypes.parse_config_file(false, false)
for _, x in ipairs(settings) do
if x.type == "category" then
if x.level == 0 then
state.categories[#state.categories + 1] = {name = x.name}
elseif x.level == 1 then
local group = state.categories[#state.categories]
group[#group + 1] = x.name
state.settings[x.name] = {}
category = x.name
end
else
local s = state.settings[category]
x.value = imfs.state()
if x.type == "string" or x.type == "float" or x.type == "int" or x.type == "enum" then
x.value._val = minetest.settings:get(x.name) or x.default
elseif x.type == "bool" then
x.value._val = minetest.settings:get_bool(x.name, x.default)
end
s[#s +1] = x
end
end
inject_settings(state)
state.settings_shown._val = state.settings
state.categories_shown._val = state.categories
end
imfs.box(0, 0, "100%", 1, theme.styles.container.bg_color)
imfs.row(0.2, 0.125, "100% - 0.4", 0.75)
:gap(0.1)
theme.field(0, 0, "1x", "100%", state.search)
:onchange(function(value)
state.search = value
end)
:onenter(function()
local settings, categories = search_settings(state, state.search)
if categories[1] then
local first = categories[1].name and categories[1][1] or categories[1]
state.category(first)
end
state.categories_shown(categories)
state.settings_shown(settings)
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("search"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Search settings")
:onclick(function()
local settings, categories = search_settings(state, state.search)
if categories[1] then
local first = categories[1].name and categories[1][1] or categories[1]
state.category(first)
end
state.categories_shown(categories)
state.settings_shown(settings)
end)
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("cancel"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Clear search")
:onclick(function()
state.search = ""
state.settings_shown(state.settings)
state.categories_shown(state.categories)
end)
imfs.button(0, 0, 2, "100%", "Back")
:onclick(function()
show_meta_menu()
end)
imfs.row_end()
imfs.box(0, 1, "20%", "100% - 1", theme.styles.container.bg_color)
imfs.box(0, 0.95, "100%", 0.1, theme.styles.container.border_color)
imfs.scroll_container(0.2, 1.05, "20% - 0.45", "100% - 1.05")
local categories = state.categories_shown()
if categories then
local y = 0.1
for i = 1, #categories do
local group = categories[i]
if not group then break end
-- Top-level categories.
if type(group) == "string" then
imfs.button(0, y, "100%", 0.5, group)
:onclick(function()
if state.category() ~= group then
state.category(group)
end
end)
y = y + 0.55
-- Grouped categories.
else
imfs.arealabel(0, y + 0.125, "100%", 0.375, group.name)
y = y + 0.425
local h = 0
for j = 1, #group do
local category = group[j]
imfs.box(0.12, y + 0.245, 0.25, 0.01, theme.styles.text_color)
imfs.button(0.25, y, "100% - 0.25", 0.5, category)
:onclick(function()
if state.category() ~= category then
state.category(category)
end
end)
h = h + 0.55
y = y + 0.55
end
imfs.box(0.12, y - h, 0.01, h - 0.3, theme.styles.text_color)
end
end
end
imfs.scroll_container_end()
imfs.box("20% - 0.05", 1, 0.1, "100% - 1", theme.styles.container.border_color)
imfs.scroll_container("20% + 0.25", 1.05, "80% - 0.45", "100% - 1.05")
imfs.column(0, 0, "100%", 1000)
:gap(0.1)
local settings = state.settings_shown()[state.category()]
if settings then
for i = 1, #settings do
local setting = settings[i]
if not setting then break end
if setting.type == "bool" then
local active = minetest.settings:get_bool(setting.name, setting.default)
theme.checkbox(0, 0, setting.readable_name, setting.value, setting.comment)
elseif setting.type == "float" or setting.type == "int" then
imfs.arealabel(0, 0, "100%", 0.325, setting.readable_name)
imfs.row(0, 0, "100%", 0.75)
:gap(0.1)
theme.number_field(0, 0, "1x", 0.75, setting.value)
:tooltip(setting.comment)
imfs.button(0, 0, 2, 0.75, "Set")
imfs.row_end()
elseif setting.type == "string" then
imfs.arealabel(0, 0, "100%", 0.325, setting.readable_name)
imfs.row(0, 0, "100%", 0.75)
:gap(0.1)
theme.field(0, 0, "1x", 0.75, setting.value)
:tooltip(setting.comment)
if setting.value._val ~= setting.default then
imfs.button(0, 0, 0.75, 0.75)
:image(theme.icon("refresh"))
:style({
bgimg = theme.get_background_image("button"),
bgimg_middle = theme.styles.button.border_width,
})
:tooltip("Reset to default")
end
imfs.button(0, 0, 2, 0.75, "Set")
imfs.row_end()
elseif setting.type == "enum" then
imfs.scope(setting.name)
imfs.arealabel(0, 0, "100%", 0.325, setting.readable_name)
imfs.row(0, 0, "100%", "fit")
:gap(0.1)
theme.dropdown(0, 0, "1x", 0.75, setting.values, setting.value)
:tooltip(setting.comment)
imfs.button(0, 0, 2, 0.75, "Set")
imfs.row_end()
imfs.scope_end()
else
print(dump(setting))
end
end
end
imfs.column_end()
imfs.scroll_container_end()
return imfs.end_()
end

0
views/texturepacks.lua Normal file
View file