Refactor builtin HUD (#14346)

This commit is contained in:
cx384 2024-04-10 11:43:15 +02:00 committed by GitHub
parent 284f6d3682
commit 8a5e49c856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 262 additions and 220 deletions

261
builtin/game/hud.lua Normal file
View File

@ -0,0 +1,261 @@
--[[
Register function to easily register new builtin hud elements
`def` is a table and contains the following fields:
elem_def the HUD element definition which can be changed with hud_replace_builtin
events (optional) additional event names on which the element will be updated
("hud_changed" will always be used.)
show_elem(player, flags, id)
(optional) a function to decide if the element should be shown to a player
It is called before the element gets updated.
update_def(player, elem_def)
(optional) a function to change the elem_def before it will be used.
(elem_def can be changed, since the table which got set by using
hud_replace_builtin isn't exposed to the API.)
update_elem(player, id)
(optional) a function to change the element after it has been updated
(Is not called when the element is first set or recreated.)
]]--
local registered_elements = {}
local update_events = {}
local function register_builtin_hud_element(name, def)
registered_elements[name] = def
for _, event in ipairs(def.events or {}) do
update_events[event] = update_events[event] or {}
table.insert(update_events[event], name)
end
end
-- Stores HUD ids for all players
local hud_ids = {}
-- Updates one element
-- In case the element is already added, it only calls the update_elem function from
-- registered_elements. (To recreate the element remove it first.)
local function update_element(player, player_hud_ids, elem_name, flags)
local def = registered_elements[elem_name]
local id = player_hud_ids[elem_name]
if def.show_elem and not def.show_elem(player, flags, id) then
if id then
player:hud_remove(id)
player_hud_ids[elem_name] = nil
end
return
end
if not id then
if def.update_def then
def.update_def(player, def.elem_def)
end
id = player:hud_add(def.elem_def)
player_hud_ids[elem_name] = id
return
end
if def.update_elem then
def.update_elem(player, id)
end
end
-- Updates all elements
-- If to_update is specified it will only update those elements.
local function update_hud(player, to_update)
local flags = player:hud_get_flags()
local playername = player:get_player_name()
hud_ids[playername] = hud_ids[playername] or {}
local player_hud_ids = hud_ids[playername]
if to_update then
for _, elem_name in ipairs(to_update) do
update_element(player, player_hud_ids, elem_name, flags)
end
else
for elem_name, _ in pairs(registered_elements) do
update_element(player, player_hud_ids, elem_name, flags)
end
end
end
local function player_event_handler(player, eventname)
assert(player:is_player())
if eventname == "hud_changed" then
update_hud(player)
return
end
-- Custom events
local to_update = update_events[eventname]
if to_update then
update_hud(player, to_update)
end
end
-- Returns true if successful, otherwise false,
-- but currently the return value is not documented in the Lua API.
function core.hud_replace_builtin(elem_name, elem_def)
assert(type(elem_def) == "table")
local registered = registered_elements[elem_name]
if not registered then
return false
end
registered.elem_def = table.copy(elem_def)
for playername, player_hud_ids in pairs(hud_ids) do
local player = core.get_player_by_name(playername)
local id = player_hud_ids[elem_name]
if player and id then
player:hud_remove(id)
player_hud_ids[elem_name] = nil
update_element(player, player_hud_ids, elem_name, player:hud_get_flags())
end
end
return true
end
local function cleanup_builtin_hud(player)
hud_ids[player:get_player_name()] = nil
end
-- Append "update_hud" as late as possible
-- This ensures that the HUD is hidden when the flags are updated in this callback
core.register_on_mods_loaded(function()
core.register_on_joinplayer(function(player)
update_hud(player)
end)
end)
core.register_on_leaveplayer(cleanup_builtin_hud)
core.register_playerevent(player_event_handler)
---- Builtin HUD Elements
--- Healthbar
-- Cache setting
local enable_damage = core.settings:get_bool("enable_damage")
local function scale_to_hud_max(player, field)
-- Scale "hp" or "breath" to the hud maximum dimensions
local current = player["get_" .. field](player)
local nominal
if field == "hp" then -- HUD is called health but field is hp
nominal = registered_elements.health.elem_def.item
else
nominal = registered_elements[field].elem_def.item
end
local max_display = math.max(player:get_properties()[field .. "_max"], current)
return math.ceil(current / max_display * nominal)
end
register_builtin_hud_element("health", {
elem_def = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "heart.png",
text2 = "heart_gone.png",
number = core.PLAYER_MAX_HP_DEFAULT,
item = core.PLAYER_MAX_HP_DEFAULT,
direction = 0,
size = {x = 24, y = 24},
offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)},
},
events = {"properties_changed", "health_changed"},
show_elem = function(player, flags)
return flags.healthbar and enable_damage and
player:get_armor_groups().immortal ~= 1
end,
update_def = function(player, elem_def)
elem_def.item = elem_def.item or elem_def.number or core.PLAYER_MAX_HP_DEFAULT
elem_def.number = scale_to_hud_max(player, "hp")
end,
update_elem = function(player, id)
player:hud_change(id, "number", scale_to_hud_max(player, "hp"))
end,
})
--- Breathbar
-- Stores core.after calls for every player
local breathbar_removal_jobs = {}
register_builtin_hud_element("breath", {
elem_def = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "bubble.png",
text2 = "bubble_gone.png",
number = core.PLAYER_MAX_BREATH_DEFAULT * 2,
item = core.PLAYER_MAX_BREATH_DEFAULT * 2,
direction = 0,
size = {x = 24, y = 24},
offset = {x = 25, y= -(48 + 24 + 16)},
},
events = {"properties_changed", "breath_changed"},
show_elem = function(player, flags, id)
local show_breathbar = flags.breathbar and enable_damage and
player:get_armor_groups().immortal ~= 1
if id then
-- The element will not prematurely be removed by update_element
-- (but may still be instantly removed if the flag changed)
return show_breathbar
end
-- Don't add the element if the breath is full
local breath_relevant = player:get_breath() < player:get_properties().breath_max
return show_breathbar and breath_relevant
end,
update_def = function(player, elem_def)
elem_def.item = elem_def.item or elem_def.number or core.PLAYER_MAX_BREATH_DEFAULT
elem_def.number = scale_to_hud_max(player, "breath")
end,
update_elem = function(player, id)
player:hud_change(id, "number", scale_to_hud_max(player, "breath"))
local player_name = player:get_player_name()
local breath_relevant = player:get_breath() < player:get_properties().breath_max
if not breath_relevant then
if not breathbar_removal_jobs[player_name] then
-- The breathbar stays for some time and then gets removed.
breathbar_removal_jobs[player_name] = core.after(1, function()
local player = core.get_player_by_name(player_name)
local id = hud_ids[player_name].breath
if player and id then
player:hud_remove(id)
hud_ids[player_name].breath = nil
end
breathbar_removal_jobs[player_name] = nil
end)
end
else
-- Cancel removal
local job = breathbar_removal_jobs[player_name]
if job then
job:cancel()
breathbar_removal_jobs[player_name] = nil
end
end
end,
})
--- Minimap
register_builtin_hud_element("minimap", {
elem_def = {
type = "minimap",
position = {x = 1, y = 0},
alignment = {x = -1, y = 1},
offset = {x = -10, y = 10},
size = {x = 256, y = 256},
},
show_elem = function(player, flags)
-- Don't add a minimap for clients which already have it hardcoded in C++.
return flags.minimap and
core.get_player_information(player:get_player_name()).protocol_version >= 44
end,
})

View File

@ -35,7 +35,7 @@ assert(loadfile(gamepath .. "falling.lua"))(builtin_shared)
dofile(gamepath .. "features.lua") dofile(gamepath .. "features.lua")
dofile(gamepath .. "voxelarea.lua") dofile(gamepath .. "voxelarea.lua")
dofile(gamepath .. "forceloading.lua") dofile(gamepath .. "forceloading.lua")
dofile(gamepath .. "statbars.lua") dofile(gamepath .. "hud.lua")
dofile(gamepath .. "knockback.lua") dofile(gamepath .. "knockback.lua")
dofile(gamepath .. "async.lua") dofile(gamepath .. "async.lua")

View File

@ -1,217 +0,0 @@
-- cache setting
local enable_damage = core.settings:get_bool("enable_damage")
local bar_definitions = {
hp = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "heart.png",
text2 = "heart_gone.png",
number = core.PLAYER_MAX_HP_DEFAULT,
item = core.PLAYER_MAX_HP_DEFAULT,
direction = 0,
size = {x = 24, y = 24},
offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)},
},
breath = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "bubble.png",
text2 = "bubble_gone.png",
number = core.PLAYER_MAX_BREATH_DEFAULT * 2,
item = core.PLAYER_MAX_BREATH_DEFAULT * 2,
direction = 0,
size = {x = 24, y = 24},
offset = {x = 25, y= -(48 + 24 + 16)},
},
minimap = {
type = "minimap",
position = {x = 1, y = 0},
alignment = {x = -1, y = 1},
offset = {x = -10, y = 10},
size = {x = 256 , y = 256},
},
}
local hud_ids = {}
local function scaleToHudMax(player, field)
-- Scale "hp" or "breath" to the hud maximum dimensions
local current = player["get_" .. field](player)
local nominal = bar_definitions[field].item
local max_display = math.max(player:get_properties()[field .. "_max"], current)
return math.ceil(current / max_display * nominal)
end
local function update_builtin_statbars(player)
local name = player:get_player_name()
if name == "" then
return
end
local flags = player:hud_get_flags()
if not hud_ids[name] then
hud_ids[name] = {}
-- flags are not transmitted to client on connect, we need to make sure
-- our current flags are transmitted by sending them actively
player:hud_set_flags(flags)
end
local hud = hud_ids[name]
local immortal = player:get_armor_groups().immortal == 1
if flags.healthbar and enable_damage and not immortal then
local number = scaleToHudMax(player, "hp")
if hud.id_healthbar == nil then
local hud_def = table.copy(bar_definitions.hp)
hud_def.number = number
hud.id_healthbar = player:hud_add(hud_def)
else
player:hud_change(hud.id_healthbar, "number", number)
end
elseif hud.id_healthbar then
player:hud_remove(hud.id_healthbar)
hud.id_healthbar = nil
end
local show_breathbar = flags.breathbar and enable_damage and not immortal
local breath = player:get_breath()
local breath_max = player:get_properties().breath_max
if show_breathbar and breath <= breath_max then
local number = scaleToHudMax(player, "breath")
if not hud.id_breathbar and breath < breath_max then
local hud_def = table.copy(bar_definitions.breath)
hud_def.number = number
hud.id_breathbar = player:hud_add(hud_def)
elseif hud.id_breathbar then
player:hud_change(hud.id_breathbar, "number", number)
end
end
if hud.id_breathbar and (not show_breathbar or breath == breath_max) then
core.after(1, function(player_name, breath_bar)
local player = core.get_player_by_name(player_name)
if player then
player:hud_remove(breath_bar)
end
end, name, hud.id_breathbar)
hud.id_breathbar = nil
end
-- Don't add a minimap for clients which already have it hardcoded in C++.
local show_minimap = flags.minimap and
minetest.get_player_information(name).protocol_version >= 44
if show_minimap and not hud.id_minimap then
hud.id_minimap = player:hud_add(bar_definitions.minimap)
elseif not show_minimap and hud.id_minimap then
player:hud_remove(hud.id_minimap)
hud.id_minimap = nil
end
end
local function cleanup_builtin_statbars(player)
local name = player:get_player_name()
if name == "" then
return
end
hud_ids[name] = nil
end
local function player_event_handler(player,eventname)
assert(player:is_player())
local name = player:get_player_name()
if name == "" or not hud_ids[name] then
return
end
if eventname == "health_changed" then
update_builtin_statbars(player)
if hud_ids[name].id_healthbar then
return true
end
end
if eventname == "breath_changed" then
update_builtin_statbars(player)
if hud_ids[name].id_breathbar then
return true
end
end
if eventname == "hud_changed" or eventname == "properties_changed" then
update_builtin_statbars(player)
return true
end
return false
end
function core.hud_replace_builtin(hud_name, definition)
if type(definition) ~= "table" then
return false
end
definition = table.copy(definition)
if hud_name == "health" then
definition.item = definition.item or definition.number or core.PLAYER_MAX_HP_DEFAULT
bar_definitions.hp = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_healthbar then
player:hud_remove(ids.id_healthbar)
ids.id_healthbar = nil
update_builtin_statbars(player)
end
end
return true
end
if hud_name == "breath" then
definition.item = definition.item or definition.number or core.PLAYER_MAX_BREATH_DEFAULT
bar_definitions.breath = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_breathbar then
player:hud_remove(ids.id_breathbar)
ids.id_breathbar = nil
update_builtin_statbars(player)
end
end
return true
end
if hud_name == "minimap" then
bar_definitions.minimap = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_minimap then
player:hud_remove(ids.id_minimap)
ids.id_minimap = nil
update_builtin_statbars(player)
end
end
return true
end
return false
end
-- Append "update_builtin_statbars" as late as possible
-- This ensures that the HUD is hidden when the flags are updated in this callback
core.register_on_mods_loaded(function()
core.register_on_joinplayer(update_builtin_statbars)
end)
core.register_on_leaveplayer(cleanup_builtin_statbars)
core.register_playerevent(player_event_handler)

View File

@ -1784,8 +1784,6 @@ void Server::SendHUDSetFlags(session_t peer_id, u32 flags, u32 mask)
{ {
NetworkPacket pkt(TOCLIENT_HUD_SET_FLAGS, 4 + 4, peer_id); NetworkPacket pkt(TOCLIENT_HUD_SET_FLAGS, 4 + 4, peer_id);
flags &= ~(HUD_FLAG_HEALTHBAR_VISIBLE | HUD_FLAG_BREATHBAR_VISIBLE);
pkt << flags << mask; pkt << flags << mask;
Send(&pkt); Send(&pkt);