minetest-i4/src/fields.lua
Jaidyn Ann 623b14bfe8 Implement new API for adding custom footer-buttons
This allows mods to place their own buttons
alongside the trash, sort, and settings buttons in
the inventory’s footer. The API mimics the custom
tab API.
2024-01-12 00:30:08 -06:00

504 lines
12 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local set_fs = i3.set_fs
IMPORT("min", "max", "vec_round")
IMPORT("reg_items", "reg_aliases")
IMPORT("sort", "copy", "insert", "remove", "indexof")
IMPORT("S", "random", "translate", "compressible", "ItemStack")
IMPORT("fmt", "find", "match", "sub", "lower", "split", "toupper")
IMPORT("valid_item", "get_stack", "craft_stack", "clean_name", "check_privs", "safe_teleport")
IMPORT("msg", "is_fav", "pos_to_str", "str_to_pos", "add_hud_waypoint", "play_sound", "reset_data")
IMPORT("search", "sort_inventory", "sort_by_category", "get_recipes", "get_detached_inv", "update_inv_size")
-- Fields-handler for any tab displaying the inventory & footer (that is, with def.slots true).
local function inv_fields(player, data, fields)
local name = data.player_name
local inv = player:get_inventory()
if fields.dd_sorting_method then
data.sort = tonumber(fields.dd_sorting_method)
elseif fields.sb_font_size then
data.font_size = tonumber(fields.sb_font_size:match"-?%d+$")
end
local footer = data.footer_button and i3.footer_buttons[data.footer_button]
if footer and footer.fields then
if not footer.fields(player, data, fields) then
data.footer_button = nil
end
end
for field in pairs(fields) do
if sub(field, 1, 4) == "btn_" then
data.subcat = indexof(i3.categories, sub(field, 5))
break
elseif sub(field, 1, 3) == "cb_" then
local str = sub(field, 4)
data[str] = false
if fields[field] == "true" then
data[str] = true
end
if str == "legacy_inventory" then
update_inv_size(player, data)
elseif str == "collapse" then
search(data)
end
elseif sub(field, 1, 8) == "setting_" then
data.show_setting = match(field, "_(%w+)$")
elseif sub(field, 1, 9) == "skin_btn_" then
local id = tonumber(field:match("%d+"))
local _skins = skins.get_skinlist_for_player(name)
play_sound(name, "i3_skin_change", 0.6)
skins.set_player_skin(player, _skins[id])
elseif find(field, "waypoint_%d+") then
local id, action = match(field, "_(%d+)_(%w+)$")
id = tonumber(id)
local waypoint = data.waypoints[id]
if not waypoint then return end
if action == "see" then
if data.waypoint_see and data.waypoint_see == id then
data.waypoint_see = nil
else
data.waypoint_see = id
end
elseif action == "delete" then
player:hud_remove(waypoint.id)
remove(data.waypoints, id)
elseif action == "teleport" then
local pos = str_to_pos(waypoint.pos)
safe_teleport(player, pos)
msg(name, S("Teleported to: @1", waypoint.name))
elseif action == "refresh" then
local color = random(0xffffff)
waypoint.color = color
player:hud_change(waypoint.id, "number", color)
elseif action == "hide" then
if waypoint.hide then
local new_id = add_hud_waypoint(
player, waypoint.name, str_to_pos(waypoint.pos), waypoint.color)
waypoint.id = new_id
waypoint.hide = nil
else
player:hud_remove(waypoint.id)
waypoint.hide = true
end
end
break
end
end
if fields.quit then
data.footer_button = nil
data.waypoint_see = nil
data.bag_rename = nil
data.goto_page = nil
if data.filter == "" then
data.enable_search = nil
end
elseif fields.close_preview then
data.waypoint_see = nil
elseif fields.bag_rename then
data.bag_rename = true
elseif fields.confirm_rename then
local bag = get_detached_inv("bag", name)
local bagstack = bag:get_stack("main", 1)
local meta = bagstack:get_meta()
local desc = translate(data.lang_code, bagstack:get_description())
local fill = split(desc, "(")[2]
local newname = fields.bag_newname:gsub("([%(%)])", "")
newname = toupper(newname:trim())
if fill then
newname = fmt("%s (%s", newname, fill)
end
meta:set_string("description", newname)
bag:set_stack("main", 1, bagstack)
data.bag = bagstack:to_string()
data.bag_rename = nil
elseif fields.waypoint_add then
local max_waypoints = i3.settings.max_waypoints
if #data.waypoints >= max_waypoints then
play_sound(name, "i3_cannot", 0.8)
return msg(name, fmt("Waypoints limit reached (%u)", max_waypoints))
end
local pos = player:get_pos()
for _, v in ipairs(data.waypoints) do
if vec_round(pos) == vec_round(str_to_pos(v.pos)) then
play_sound(name, "i3_cannot", 0.8)
return msg(name, S"You already have set a waypoint at this position")
end
end
local waypoint = fields.waypoint_name
if fields.waypoint_name == "" then
waypoint = "Waypoint"
end
local color = random(0xffffff)
local id = add_hud_waypoint(player, waypoint, pos, color)
insert(data.waypoints, {
name = waypoint,
pos = pos_to_str(pos, 1),
color = color,
id = id,
})
data.scrbar_inv += 1000
elseif fields.hide_debug_grid then
data.hide_debug_grid = not data.hide_debug_grid
end
end
local function select_item(player, data, fields)
local item
for field in pairs(fields) do
if find(field, ":") then
item = field
break
end
end
if not item then return end
if compressible(item, data) then
local idx
for i = 1, #data.items do
local it = data.items[i]
if it == item then
idx = i
break
end
end
if data.expand ~= "" then
data.alt_items = nil
if item == data.expand then
data.expand = nil
return
end
end
if idx and item ~= data.expand then
data.alt_items = copy(data.items)
data.expand = item
if i3.compress_groups[item] then
local items = copy(i3.compress_groups[item])
insert(items, fmt("_%s", item))
sort(items, function(a, b)
if a:sub(1, 1) == "_" then
a = a:sub(2)
end
return a < b
end)
local i = 1
for _, v in ipairs(items) do
if valid_item(reg_items[clean_name(v)]) then
insert(data.alt_items, idx + i, v)
i++
end
end
end
end
else
if sub(item, 1, 1) == "_" then
item = sub(item, 2)
elseif sub(item, 1, 6) == "group!" then
item = match(item, "([%w:_]+)$")
end
item = reg_aliases[item] or item
if not reg_items[item] then return end
if core.is_creative_enabled(data.player_name) then
local stack = ItemStack(item)
local stackmax = stack:get_stack_max()
stack = fmt("%s %s", item, stackmax)
return get_stack(player, stack)
end
if item == data.query_item then return end
local recipes, usages = get_recipes(player, item)
data.query_item = item
data.recipes = recipes
data.usages = usages
data.rnum = 1
data.unum = 1
data.scrbar_rcp = 1
data.scrbar_usg = 1
data.crafting_rcp = nil
data.crafting_usg = nil
end
end
local function rcp_fields(player, data, fields)
local sb_rcp, sb_usg = fields.scrbar_rcp, fields.scrbar_usg
if not data.hide_tabs and fields.filter and fields.filter == "" then
data.enable_search = nil
end
if fields.cancel then
reset_data(data)
elseif fields.exit then
data.query_item = nil
elseif fields.enable_search then
if data.hide_tabs then
data.enable_search = not data.enable_search
else
data.enable_search = true
end
elseif fields.filter and (fields.key_enter_field == "filter" or fields.search) then
if fields.filter == "" then
reset_data(data)
return set_fs(player)
end
local str = lower(fields.filter)
if data.filter == str then return end
data.filter = str
data.pagenum = 1
search(data)
if data.itab > 1 then
sort_by_category(data)
end
elseif fields.pagenum then
data.goto_page = not data.goto_page
elseif fields.goto_page then
local pagenum = tonumber(fields.goto_page)
data.pagenum = max(1, min(data.pagemax, pagenum or data.pagenum))
data.goto_page = nil
elseif fields.prev_page or fields.next_page then
if data.pagemax == 1 then return end
data.pagenum -= (fields.prev_page and 1 or -1)
if data.pagenum > data.pagemax then
data.pagenum = 1
elseif data.pagenum == 0 then
data.pagenum = data.pagemax
end
elseif fields.prev_skin or fields.next_skin then
if data.skin_pagemax == 1 then return end
data.skin_pagenum -= (fields.prev_skin and 1 or -1)
if data.skin_pagenum > data.skin_pagemax then
data.skin_pagenum = 1
elseif data.skin_pagenum == 0 then
data.skin_pagenum = data.skin_pagemax
end
elseif fields.prev_recipe or fields.next_recipe then
local num = data.rnum + (fields.prev_recipe and -1 or 1)
data.rnum = data.recipes[num] and num or (fields.prev_recipe and #data.recipes or 1)
data.crafting_rcp = nil
data.scrbar_rcp = 1
elseif fields.prev_usage or fields.next_usage then
local num = data.unum + (fields.prev_usage and -1 or 1)
data.unum = data.usages[num] and num or (fields.prev_usage and #data.usages or 1)
data.crafting_usg = nil
data.scrbar_usg = 1
elseif fields.fav then
local fav = is_fav(data)
if #data.favs < i3.settings.max_favs and not fav then
insert(data.favs, data.query_item)
elseif fav then
remove(data.favs, fav)
end
elseif fields.crafting_rcp or fields.crafting_usg then
if fields.crafting_rcp then
data.crafting_rcp = not data.crafting_rcp
if not data.crafting_rcp then
data.scrbar_rcp = 1
end
else
data.crafting_usg = not data.crafting_usg
if not data.crafting_usg then
data.scrbar_usg = 1
end
end
elseif (sb_rcp and sub(sb_rcp, 1, 3) == "CHG") or (sb_usg and sub(sb_usg, 1, 3) == "CHG") then
data.scrbar_rcp = sb_rcp and tonumber(match(sb_rcp, "%d+"))
data.scrbar_usg = sb_usg and tonumber(match(sb_usg, "%d+"))
elseif fields.craft_rcp or fields.craft_usg then
craft_stack(player, data, fields.craft_rcp)
if fields.craft_rcp then
data.crafting_rcp = nil
data.scrbar_rcp = 1
else
data.crafting_usg = nil
data.scrbar_usg = 1
end
else
select_item(player, data, fields)
end
end
-- Fields input-handler for the Home footer-button.
local function home_footer_fields(player, data, fields)
local name = player:get_player_name()
-- Use minetest_games sethome API, if available.
if sethome then
if not sethome.go(name) then
msg(name, "No home set")
else
msg(name, S"Welcome back home!")
end
-- Otherwise, use i3s home.
elseif not data.home then
msg(name, "No home set")
elseif name then
safe_teleport(player, str_to_pos(data.home))
msg(name, S"Welcome back home!")
end
-- Immediately disable this footer-dialogue, since nothing is displayed.
return false
end
-- Fields input-handler for the “Confirm trash” footer-dialogue.
local function confirm_trash_footer_fields(player, data, fields)
local inv = player:get_inventory()
if fields.confirm_trash_yes then
inv:set_list("main", {})
inv:set_list("craft", {})
end
-- In any case, disable this footer-dialogue.
return not (fields.confirm_trash_yes or fields.confirm_trash_no)
end
-- Fields input-handler for the settings footer-dialogue.
local function settings_footer_fields(player, data, fields)
if fields.set_home and not check_privs(name, {home = true}) then
return msg(name, "'home' privilege missing")
elseif fields.set_home then
if sethome then
sethome.set(player:get_player_name(), player:get_pos())
else
data.home = pos_to_str(player:get_pos(), 1)
end
end
return not fields.close_settings
end
core.register_on_player_receive_fields(function(player, formname, fields)
local name = player:get_player_name()
if formname == "i3_outdated" then
return false, core.kick_player(name, S"Your Minetest client needs updating (www.minetest.net)")
elseif formname ~= "" then
return false
end
-- No-op buttons
if fields.player_name or fields.awards or fields.home_pos or fields.no_item or
fields.no_rcp or fields.select_sorting or fields.sort_method or fields.bg_content or
fields.quick_crafting then
return false
end
-- print(dump(fields))
local data = i3.data[name]
if not data then return end
local sb_inv = fields.scrbar_inv
if sb_inv and sub(sb_inv, 1, 3) == "CHG" then
data.scrbar_inv = tonumber(match(sb_inv, "%d+"))
return
end
for f in pairs(fields) do
if sub(f, 1, 4) == "tab_" then
local tabname = sub(f, 5)
i3.set_tab(player, tabname)
break
elseif sub(f, 1, 5) == "itab_" then
data.pagenum = 1
data.itab = tonumber(f:sub(-1))
sort_by_category(data)
break
elseif sub(f, 1, 7) == "footer_" then
local footer_name = sub(f, 8)
for i, footer in ipairs(i3.footer_buttons) do
if footer.name == footer_name then
data.footer_button = i
break
end
end
end
end
rcp_fields(player, data, fields)
local tab = i3.tabs[data.tab]
if tab then
if tab.slots then
inv_fields(player, data, fields)
end
if tab.fields then
local ret = tab.fields(player, data, fields)
if ret == false then return end
end
end
return true, set_fs(player)
end)
return home_footer_fields, confirm_trash_footer_fields, settings_footer_fields