This commit is contained in:
Vincent Robinson 2024-04-15 20:36:58 +02:00 committed by GitHub
commit fecb6d1f16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 4153 additions and 59 deletions

View File

@ -20,7 +20,14 @@ read_globals = {
"PerlinNoise", "PerlinNoiseMap",
string = {fields = {"split", "trim"}},
table = {fields = {"copy", "getn", "indexof", "insert_all"}},
table = {fields = {
"copy",
"getn",
"indexof",
"insert_all",
"merge",
"shallow_copy",
}},
math = {fields = {"hypot", "round"}},
}

View File

@ -489,6 +489,15 @@ function table.copy(t, seen)
end
function table.shallow_copy(t)
local new = {}
for k, v in pairs(t) do
new[k] = v
end
return new
end
function table.insert_all(t, other)
if table.move then -- LuaJIT
return table.move(other, 1, #other, #t + 1, t)
@ -500,6 +509,15 @@ function table.insert_all(t, other)
end
function table.merge(...)
local new = {}
for _, t in ipairs{...} do
table.insert_all(new, t)
end
return new
end
function table.key_value_swap(t)
local ti = {}
for k,v in pairs(t) do
@ -763,3 +781,28 @@ function core.parse_coordinates(x, y, z, relative_to)
local rz = core.parse_relative_number(z, relative_to.z)
return rx and ry and rz and { x = rx, y = ry, z = rz }
end
local function call(class, ...)
local obj = core.class(class)
if obj.new then
obj:new(...)
end
return obj
end
function core.class(super)
super = super or {}
super.__index = super
super.__call = call
return setmetatable({}, super)
end
function core.is_instance(obj, class)
if type(obj) ~= "table" then
return false
end
local meta = getmetatable(obj)
return meta == class or core.is_instance(meta, class)
end

View File

@ -2,6 +2,7 @@
local scriptpath = core.get_builtin_path()
local commonpath = scriptpath .. "common" .. DIR_DELIM
local gamepath = scriptpath .. "game".. DIR_DELIM
local uipath = scriptpath .. "ui" .. DIR_DELIM
-- Shared between builtin files, but
-- not exposed to outer context
@ -38,6 +39,7 @@ dofile(gamepath .. "forceloading.lua")
dofile(gamepath .. "hud.lua")
dofile(gamepath .. "knockback.lua")
dofile(gamepath .. "async.lua")
dofile(uipath .. "init.lua")
core.after(0, builtin_shared.cache_content_ids)

130
builtin/ui/elem.lua Normal file
View File

@ -0,0 +1,130 @@
--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
ui._elem_types = {}
function ui._new_type(base, type, type_id, id_required)
local class = core.class(base)
class._type = type
class._type_id = type_id
class._id_required = id_required
ui._elem_types[type] = class
return class
end
function ui.derive_elem(base, type)
assert(not ui._elem_types[type], "Derived element name already used")
return ui._new_type(base, type, base._type_id, base._id_required)
end
ui.Elem = ui._new_type(nil, "elem", 0x00, false)
ui.Label = ui.derive_elem(ui.Elem, "label")
ui.Image = ui.derive_elem(ui.Elem, "image")
function ui.Elem:new(props)
if self._id_required then
assert(ui.is_id(props.id), "ID is required for '" .. self._type .. "'")
end
self._id = props.id or ui.new_id()
self._groups = {}
self._boxes = {main = true}
self._style = props.style or ui.Style{props = props}
self._children = table.merge(props.children or props)
-- Set by parent ui.Elem
self._parent = nil
self._index = nil
self._rindex = nil
-- Set by ui.Window
self._window = nil
assert(ui.is_id(self._id), "Element ID must be an ID string")
for _, group in ipairs(props.groups or {}) do
assert(ui.is_id(group), "Element group must be an ID string")
self._groups[group] = true
end
for i, child in ipairs(self._children) do
assert(child._parent == nil, "Element already has a parent")
assert(not core.is_instance(child, ui.Root),
"ui.Root can only be a root element")
child._parent = self
child._index = i
child._rindex = #self._children - i + 1
end
end
function ui.Elem:_get_flat()
local elems = {self}
for _, child in ipairs(self._children) do
table.insert_all(elems, child:_get_flat())
end
return elems
end
function ui.Elem:_encode()
return ui._encode("Bz S", self._type_id, self._id, self:_encode_fields())
end
function ui.Elem:_encode_fields()
local fl = ui._make_flags()
if ui._shift_flag(fl, #self._children > 0) then
local child_ids = {}
for i, child in ipairs(self._children) do
child_ids[i] = child._id
end
ui._encode_flag(fl, "Z", ui._encode_array("z", child_ids))
end
self:_encode_box(fl, self._boxes.main)
return ui._encode_flags(fl)
end
function ui.Elem:_encode_box(fl, box)
-- Element encoding always happens after styles are computed and boxes are
-- populated with style indices. So, if this box has any styles applied to
-- it, encode the relevant states.
if not ui._shift_flag(fl, box.n > 0) then
return
end
local box_fl = ui._make_flags()
-- For each state, check if there is any styling. If there is, add it
-- to the box's flags.
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
if ui._shift_flag(box_fl, box[i] ~= ui._NO_STYLE) then
ui._encode_flag(box_fl, "I", box[i])
end
end
ui._encode_flag(fl, "s", ui._encode_flags(box_fl))
end

34
builtin/ui/elem_defs.lua Normal file
View File

@ -0,0 +1,34 @@
--[[
Minetest
Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
ui.Root = ui._new_type(ui.Elem, "root", 0x01, false)
function ui.Root:new(props)
ui.Elem.new(self, props)
self._boxes.backdrop = true
end
function ui.Root:_encode_fields()
local fl = ui._make_flags()
self:_encode_box(fl, self._boxes.backdrop)
return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl))
end

29
builtin/ui/init.lua Normal file
View File

@ -0,0 +1,29 @@
--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
ui = {}
local UI_PATH = core.get_builtin_path() .. "ui" .. DIR_DELIM
dofile(UI_PATH .. "util.lua")
dofile(UI_PATH .. "selector.lua")
dofile(UI_PATH .. "style.lua")
dofile(UI_PATH .. "elem.lua")
dofile(UI_PATH .. "window.lua")
dofile(UI_PATH .. "elem_defs.lua")

497
builtin/ui/selector.lua Normal file
View File

@ -0,0 +1,497 @@
--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
ui._STATE_NONE = 0
ui._NUM_STATES = bit.lshift(1, 5)
ui._NO_STYLE = -1
--[[
Selector parsing functions return a function. When called with an element as
the solitary parameter, this function will return a boolean, indicating whether
the element is matched by the selector. If the boolean is true, a table of
tables {box=..., states=...} is also returned. If false, this is nil.
The keys of this table are hashes of the box, which serve to prevent duplicate
box/state combos from being generated. The values contain all the combinations
of boxes and states that the selector specifies. The box may be nil if the
selector specified no box, in which case it should default to "main". This list
may also be empty, which means that contradictory boxes were specified and no
box should be styled. The list will not contain duplicates.
--]]
-- By default, most selectors leave the box unspecified and don't select any
-- particular state, leaving the state at zero.
local function make_box(name, states)
return {name = name, states = states or ui._STATE_NONE}
end
-- Hash the box to string that represents that combination of box and states
-- uniquely to prevent duplicates in box tables.
local function hash_box(box)
return (box.name or "") .. "$" .. tostring(box.states)
end
local function make_hashed(name, states)
local box = make_box(name, states)
return {[hash_box(box)] = box}
end
local function result(matches, name, states)
if matches then
return true, make_hashed(name, states)
end
return false, nil
end
local simple_preds = {
["empty"] = function(elem)
return result(#elem._children == 0)
end,
["first_child"] = function(elem)
return result(elem._parent == nil or elem._index == 1)
end,
["last_child"] = function(elem)
return result(elem._parent == nil or elem._rindex == 1)
end,
["only_child"] = function(elem)
return result(elem._parent == nil or #elem._parent._children == 1)
end,
}
local sel_preds = {
["<"] = function(sel)
return function(elem)
return result(elem._parent and sel(elem._parent))
end
end,
[">"] = function(sel)
return function(elem)
for _, child in ipairs(elem._children) do
if sel(child) then
return result(true)
end
end
return result(false)
end
end,
["<<"] = function(sel)
return function(elem)
local ancestor = elem._parent
while ancestor ~= nil do
if sel(ancestor) then
return result(true)
end
ancestor = ancestor._parent
end
return result(false)
end
end,
[">>"] = function(sel)
return function(elem)
for _, descendant in ipairs(elem:_get_flat()) do
if descendant ~= elem and sel(descendant) then
return result(true)
end
end
return result(false)
end
end,
["<>"] = function(sel)
return function(elem)
if not elem._parent then
return result(false)
end
for _, sibling in ipairs(elem._parent._children) do
if sibling ~= elem and sel(sibling) then
return result(true)
end
end
return result(false)
end
end,
}
local special_preds = {
["nth_child"] = function(str)
local index = tonumber(str)
assert(index, "Expected number for ?nth_child")
return function(elem)
if not elem._parent then
return result(index == 1)
end
return result(elem._index == index)
end
end,
["nth_last_child"] = function(str)
local rindex = tonumber(str)
assert(rindex, "Expected number for ?nth_last_child")
return function(elem)
if not elem._parent then
return result(rindex == 1)
end
return result(elem._rindex == rindex)
end
end,
}
local states_by_name = {
focused = bit.lshift(1, 0),
selected = bit.lshift(1, 1),
hovered = bit.lshift(1, 2),
pressed = bit.lshift(1, 3),
disabled = bit.lshift(1, 4),
}
local function parse_term(str, pred)
str = str:trim()
assert(str ~= "", "Expected selector term")
-- We need to test the first character to see what sort of term we're
-- dealing with, and then usually parse from the rest of the string.
local prefix = str:sub(1, 1)
local suffix = str:sub(2)
if prefix == "*" then
-- Universal terms match everything and have no extra stuff to parse.
return suffix, function(elem)
return result(true)
end
elseif prefix == "#" then
-- Most selectors are similar to the ID selector, in that characters
-- for the ID string are parsed, and all the characters directly
-- afterwards are returned as the rest of the string after the term.
local id, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
assert(id, "Expected ID after '#'")
return rest, function(elem)
return result(elem._id == id)
end
elseif prefix == "." then
local group, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
assert(group, "Expected group after '.'")
return rest, function(elem)
return result(elem._groups[group] ~= nil)
end
elseif prefix == "@" then
--[[
It's possible to check if a box exists in a predicate, but that leads
to different behaviors inside and outside of predicates. @main@thumb
effectively matches nothing by returning an empty table of boxes, but
will return true for scrollbars, which a predicate will interpret as
matching something. So, prevent it altogether. This problem
fundamentally exists because we select elements, not boxes, since boxes
and states are very much tied to the client-side of things.
--]]
assert(not pred, "Box selectors are invalid for predicates")
local name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
assert(name, "Expected box after '@'")
return rest, function(elem)
if elem._boxes[name] then
-- If the box is in the element, return it.
return result(true, name, ui._STATE_NONE)
elseif name == "all" then
-- If we want all boxes, iterate over the boxes in the element
-- and add each of them to a full list of boxes.
local boxes = {}
for name in pairs(elem._boxes) do
local box = make_box(name, ui._STATE_NONE)
boxes[hash_box(box)] = box
end
return true, boxes
end
-- Otherwise, the selector doesn't match.
return result(false)
end
elseif prefix == "$" then
-- Unfortunately, we can't detect the state of boxes from the server,
-- so we can't use them in predicates.
assert(not pred, "Style selectors are invalid for predicates")
local name, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
assert(name, "Expected state after '$'")
local state = states_by_name[name]
assert(state, "Invalid state: '" .. name .. "'")
return rest, function(elem)
-- States unconditionally match every element. Specify the state
-- that this term indicates but leave the box undefined.
return result(true, nil, state)
end
elseif prefix == "/" then
local type, rest = suffix:match("^([" .. ui._ID_CHARS .. "]+)%/(.*)$")
assert(type, "Expected window type after '/'")
assert(ui._window_types[type], "Invalid window type: '" .. type .. "'")
return rest, function(elem)
return result(elem._window._type == type)
end
elseif prefix == "," then
-- Since we don't know which terms came directly behind us, we return
-- nil so that ui._parse_sel() can union the two selectors on either
-- side of the comma instead of returning a selector function.
return suffix, nil
elseif prefix == "(" then
-- Parse a matching set of parentheses, and recursively pass the
-- contents into ui._parse_sel().
local sub, rest = str:match("^(%b())(.*)$")
assert(sub, "Unmatched ')' for '('")
return rest, ui._parse_sel(sub:sub(2, -2), pred)
elseif prefix == "!" then
-- Parse a single predicate term (NOT an entire predicate selector) and
-- ensure that it's a valid selector term, not a comma.
local rest, term = parse_term(suffix, true)
assert(term, "Expected selector term after '!'")
return rest, function(elem)
return result(not term(elem))
end
elseif prefix == "?" then
-- Predicates may have different syntax depending on the name of the
-- predicate, so just parse the name initially.
local name, rest = suffix:match("^([" .. ui._ID_CHARS .. "%<%>%^]+)(.*)$")
assert(name, "Expected predicate after '?'")
-- If this is a simple predicate, return its predicate function without
-- doing any further parsing.
local func = simple_preds[name]
if func then
return rest, func
end
-- If this is a function predicate, we need to do more parsing.
func = sel_preds[name] or special_preds[name]
if func then
-- Parse a matching pair of parentheses and get the contents
-- between them.
assert(rest:sub(1, 1) == "(", "Expected '(' after '?" .. name .. "'")
local sub, rest = rest:match("^(%b())(.*)$")
assert(sub, "Unmatched ')' for '?" .. name .. "('")
local contents = sub:sub(2, -2)
-- If this is a function predicate that wants a selector, parse the
-- contents as a predicate selector and pass it on to the selector
-- creation function.
if sel_preds[name] then
return rest, func(ui._parse_sel(contents, true))
end
-- Otherwise, hand the string directly to the function for special
-- processing, which we automatically trim for convenience.
return rest, func(contents:trim())
end
-- Otherwise, there is no predicate by this name.
error("Invalid predicate: '?" .. name .. "'")
else
-- If we found no special character, it's either a type or it indicates
-- invalid characters in the selector string.
local type, rest = str:match("^([" .. ui._ID_CHARS .. "]+)(.*)$")
assert(type, "Unexpected character in selector: '" .. prefix .. "'")
assert(ui._elem_types[type], "Invalid element type: '" .. type .. "'")
return rest, function(elem)
return result(elem._type == type)
end
end
end
local function intersect_boxes(a_boxes, b_boxes)
local new_boxes = {}
for _, box_a in pairs(a_boxes) do
for _, box_b in pairs(b_boxes) do
-- Two boxes can only be merged if they're the same box or if one
-- or both selectors hasn't specified a box yet.
if box_a.name == nil or box_b.name == nil or box_a.name == box_b.name then
-- Create the new box by taking the specified box (if there is
-- one) and ORing the states together (making them more refer
-- to a more specific state).
local new_box = make_box(
box_a.name or box_b.name,
bit.bor(box_a.states, box_b.states)
)
-- Hash this box and add it into the table. This will be
-- effectively a no-op if there's already an identical box
-- hashed in the table.
new_boxes[hash_box(new_box)] = new_box
end
end
end
return new_boxes
end
function ui._intersect_sels(sels)
return function(elem)
-- We start with the default box, and intersect the box and states from
-- every selector with it.
local all_boxes = make_hashed()
-- Loop through all of the selectors. All of them need to match for the
-- intersected selector to match.
for _, sel in ipairs(sels) do
local matches, boxes = sel(elem)
if not matches then
-- This selector doesn't match, so fail immediately.
return false, nil
end
-- Since the selector matched, intersect the boxes and states with
-- those of the other selectors. If two selectors both match an
-- element but specify different boxes, then this selector will
-- return true, but the boxes will be cancelled out in the
-- intersection, leaving an empty list of boxes.
if boxes then
all_boxes = intersect_boxes(all_boxes, boxes)
end
end
return true, all_boxes
end
end
local function union_sels(sels)
return function(elem)
-- We initially have no boxes, and have to add them in as matching
-- selectors are unioned in.
local all_boxes = {}
local found_match = false
-- Loop through all of the selectors. If any of them match, this entire
-- unioned selector matches.
for _, sel in ipairs(sels) do
local matches, boxes = sel(elem)
if matches then
-- We found a match. However, we can't return true just yet
-- because we need to union the boxes and states from every
-- selector, not just this one.
found_match = true
if boxes then
-- Add the boxes from this selector into the table of all
-- the boxes. The hashing of boxes will automatically weed
-- out any duplicates.
for hash, box in pairs(boxes) do
all_boxes[hash] = box
end
end
end
end
if found_match then
return true, all_boxes
end
return false, nil
end
end
function ui._parse_sel(str, pred)
str = str:trim()
assert(str ~= "", "Empty style selector")
-- Since selectors consisting of a single universal selector are very
-- common (as a blank ui.Style selector defaults to that), give it a
-- dedicated fast-path and skip all the parsing.
if str == "*" then
return function()
return result(true)
end
end
local sub_sels = {}
local terms = {}
-- Loop until we've read every term from the input string.
repeat
-- Parse the next term from the input string.
local term
str, term = parse_term(str, pred)
if term ~= nil then
-- If we didn't read a comma, insert this term into the list of
-- terms for the current sub-selector.
table.insert(terms, term)
else
-- If we read a comma, make sure that we have terms before and
-- after it so it's not dangling.
assert(#terms > 0, "Expected selector term before ','")
assert(str ~= "", "Expected selector term after ','")
end
-- If we read a comma or ran out of terms, we need to commit the terms
-- we've read so far.
if term == nil or str == "" then
-- If there's only one term, commit it directly. Otherwise,
-- intersect all the terms together.
if #terms == 1 then
table.insert(sub_sels, terms[1])
else
table.insert(sub_sels, ui._intersect_sels(terms))
end
-- Clear out the list of terms for the next sub-selector.
terms = {}
end
until str == ""
-- Now that we've read all the sub-selectors between the commas, we need to
-- commit them. We only need to union the terms if there's more than one.
if #sub_sels == 1 then
return sub_sels[1]
end
return union_sels(sub_sels)
end

200
builtin/ui/style.lua Normal file
View File

@ -0,0 +1,200 @@
--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
ui.Style = core.class()
function ui.Style:new(props)
self._sel = ui._parse_sel(props.sel or "*")
self._props = ui._cascade_props(props.props or props, {})
self._nested = table.merge(props.nested or props)
self._reset = props.reset
end
function ui.Style:_get_flat()
local flat_styles = {}
self:_get_flat_impl(flat_styles, ui._parse_sel("*"))
return flat_styles
end
function ui.Style:_get_flat_impl(flat_styles, parent_sel)
-- Intersect our selector with our parent selector, resulting in a fully
-- qualified selector.
local full_sel = ui._intersect_sels({parent_sel, self._sel})
-- Copy this style's properties into a new style with the full selector.
local flat = ui.Style{
reset = self._reset,
props = self._props,
}
flat._sel = full_sel
table.insert(flat_styles, flat)
-- For each sub-style of this style, cascade it with our full selector and
-- add it to the list of flat styles.
for _, nested in ipairs(self._nested) do
nested:_get_flat_impl(flat_styles, full_sel)
end
end
local function cascade_layer(new, add, props, p)
new[p.."_image"] = add[p.."_image"] or props[p.."_image"]
new[p.."_fill"] = add[p.."_fill"] or props[p.."_fill"]
new[p.."_tint"] = add[p.."_tint"] or props[p.."_tint"]
new[p.."_source"] = add[p.."_source"] or props[p.."_source"]
new[p.."_middle"] = add[p.."_middle"] or props[p.."_middle"]
new[p.."_middle_scale"] = add[p.."_middle_scale"] or props[p.."_middle_scale"]
new[p.."_frames"] = add[p.."_frames"] or props[p.."_frames"]
new[p.."_frame_time"] = add[p.."_frame_time"] or props[p.."_frame_time"]
end
function ui._cascade_props(add, props)
local new = {}
new.size = add.size or props.size
new.rel_pos = add.rel_pos or props.rel_pos
new.rel_anchor = add.rel_anchor or props.rel_anchor
new.rel_size = add.rel_size or props.rel_size
new.margin = add.margin or props.margin
new.padding = add.padding or props.padding
cascade_layer(new, add, props, "bg")
cascade_layer(new, add, props, "fg")
new.fg_scale = add.fg_scale or props.fg_scale
new.fg_halign = add.fg_halign or props.fg_halign
new.fg_valign = add.fg_valign or props.fg_valign
new.visible = ui._apply_bool(add.visible, props.visible)
new.noclip = ui._apply_bool(add.noclip, props.noclip)
return new
end
local halign_map = {left = 0, center = 1, right = 2}
local valign_map = {top = 0, center = 1, bottom = 2}
local spacing_map = {
before = 0,
after = 1,
outside = 2,
around = 3,
between = 4,
evenly = 5,
remove = 6,
}
local function encode_layer(props, p)
local fl = ui._make_flags()
if ui._shift_flag(fl, props[p.."_image"]) then
ui._encode_flag(fl, "z", props[p.."_image"])
end
if ui._shift_flag(fl, props[p.."_fill"]) then
ui._encode_flag(fl, "I", core.colorspec_to_colorint(props[p.."_fill"]))
end
if ui._shift_flag(fl, props[p.."_tint"]) then
ui._encode_flag(fl, "I", core.colorspec_to_colorint(props[p.."_tint"]))
end
if ui._shift_flag(fl, props[p.."_source"]) then
ui._encode_flag(fl, "ffff", unpack(props[p.."_source"]))
end
if ui._shift_flag(fl, props[p.."_middle"]) then
ui._encode_flag(fl, "ffff", unpack(props[p.."_middle"]))
end
if ui._shift_flag(fl, props[p.."_middle_scale"]) then
ui._encode_flag(fl, "f", props[p.."_middle_scale"])
end
if ui._shift_flag(fl, props[p.."_frames"]) then
ui._encode_flag(fl, "I", props[p.."_frames"])
end
if ui._shift_flag(fl, props[p.."_frame_time"]) then
ui._encode_flag(fl, "I", props[p.."_frame_time"])
end
return fl
end
function ui._encode_props(props)
local fl = ui._make_flags()
if ui._shift_flag(fl, props.size) then
ui._encode_flag(fl, "ff", unpack(props.size))
end
if ui._shift_flag(fl, props.rel_pos) then
ui._encode_flag(fl, "ff", unpack(props.rel_pos))
end
if ui._shift_flag(fl, props.rel_anchor) then
ui._encode_flag(fl, "ff", unpack(props.rel_anchor))
end
if ui._shift_flag(fl, props.rel_size) then
ui._encode_flag(fl, "ff", unpack(props.rel_size))
end
if ui._shift_flag(fl, props.margin) then
ui._encode_flag(fl, "ffff", unpack(props.margin))
end
if ui._shift_flag(fl, props.padding) then
ui._encode_flag(fl, "ffff", unpack(props.padding))
end
local bg_fl = encode_layer(props, "bg")
if ui._shift_flag(fl, bg_fl.flags ~= 0) then
ui._encode_flag(fl, "s", ui._encode_flags(bg_fl))
end
local fg_fl = encode_layer(props, "fg")
if ui._shift_flag(fl, fg_fl.flags ~= 0) then
ui._encode_flag(fl, "s", ui._encode_flags(fg_fl))
end
if ui._shift_flag(fl, props.fg_scale) then
ui._encode_flag(fl, "f", props.fg_scale)
end
if ui._shift_flag(fl, props.fg_halign) then
ui._encode_flag(fl, "B", halign_map[props.fg_halign])
end
if ui._shift_flag(fl, props.fg_valign) then
ui._encode_flag(fl, "B", valign_map[props.fg_valign])
end
if ui._shift_flag(fl, props.visible ~= nil) then
ui._shift_flag(fl, props.visible)
end
if ui._shift_flag(fl, props.noclip ~= nil) then
ui._shift_flag(fl, props.noclip)
end
return ui._encode("s", ui._encode_flags(fl))
end
local default_theme = ui.Style{}
function ui.get_default_theme()
return default_theme
end
function ui.set_default_theme(theme)
default_theme = theme
end

91
builtin/ui/util.lua Normal file
View File

@ -0,0 +1,91 @@
--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
local next_id = 0
function ui.new_id()
-- Just increment a monotonic counter and return it as hex. Even at
-- unreasonably fast ID generation rates, it would take years for this
-- counter to hit the 2^53 limit and start generating duplicates.
next_id = next_id + 1
return string.format("_%X", next_id)
end
ui._ID_CHARS = "a-zA-Z0-9_%-%:"
function ui.is_id(str)
return type(str) == "string" and str == str:match("^[" .. ui._ID_CHARS .. "]+$")
end
-- This coordinate size calculation copies the one for fixed-size formspec
-- coordinates in guiFormSpecMenu.cpp.
function ui.get_coord_size()
return math.floor(0.5555 * 96)
end
function ui._apply_bool(add, prop)
if add ~= nil then
return add
end
return prop
end
ui._encode = core.encode_network
ui._decode = core.decode_network
function ui._encode_array(format, arr)
local formatted = {}
for _, val in ipairs(arr) do
table.insert(formatted, ui._encode(format, val))
end
return ui._encode("IZ", #formatted, table.concat(formatted))
end
function ui._pack_flags(...)
local flags = 0
for _, flag in ipairs({...}) do
flags = bit.bor(bit.lshift(flags, 1), flag and 1 or 0)
end
return flags
end
function ui._make_flags()
return {flags = 0, num_flags = 0, data = {}}
end
function ui._shift_flag(fl, flag)
-- OR the LSB with the condition, and then right rotate it to the MSB.
fl.flags = bit.ror(bit.bor(fl.flags, flag and 1 or 0), 1)
fl.num_flags = fl.num_flags + 1
return flag
end
function ui._encode_flag(fl, ...)
table.insert(fl.data, ui._encode(...))
end
function ui._encode_flags(fl)
-- We've been shifting into the right the entire time, so flags are in the
-- upper bits; however, the protocol expects them to be in the lower bits.
-- So, shift them the appropriate amount into the lower bits.
local adjusted = bit.rshift(fl.flags, 32 - fl.num_flags)
return ui._encode("I", adjusted) .. table.concat(fl.data)
end

308
builtin/ui/window.lua Normal file
View File

@ -0,0 +1,308 @@
--[[
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--]]
local open_windows = {}
local function build_window(id, param)
local info = open_windows[id]
if not info then
return nil, nil
end
local window = info.builder(id, info.player, info.context, param or {})
assert(core.is_instance(window, ui.Window),
"Expected ui.Window to be returned from builder function")
assert(not window._id, "Window object has already been returned")
window._id = id
info.window = window
return window, info.player
end
ui.Window = core.class()
ui._window_types = {
bg = 0,
mask = 1,
hud = 2,
message = 3,
gui = 4,
fg = 5,
}
function ui.Window:new(props)
self._id = nil -- Set by build_window()
self._type = props.type
self._theme = props.theme or ui.get_default_theme()
self._style = props.style or ui.Style{}
self._root = props.root
assert(ui._window_types[self._type], "Invalid window type")
assert(core.is_instance(self._root, ui.Root),
"Expected root of window to be ui.Root")
self._elems = self._root:_get_flat()
self._elems_by_id = {}
for _, elem in ipairs(self._elems) do
local id = elem._id
assert(not self._elems_by_id[id], "Element has duplicate ID: '" .. id .. "'")
self._elems_by_id[id] = elem
assert(elem._window == nil, "Element already has window")
elem._window = self
end
end
function ui.Window:_encode(player, opening)
local enc_styles = self:_encode_styles()
local enc_elems = self:_encode_elems()
local data = ui._encode("ZzZ", enc_elems, self._root._id, enc_styles)
if opening then
data = ui._encode("ZB", data, ui._window_types[self._type])
end
return data
end
function ui.Window:_encode_styles()
-- Clear out all the boxes in every element.
for _, elem in ipairs(self._elems) do
for box in pairs(elem._boxes) do
elem._boxes[box] = {n = 0}
end
end
-- Get a cascaded and flattened list of all the styles for this window.
local styles = self:_get_full_style():_get_flat()
-- Take each style and apply its properties to every box and state matched
-- by its selector.
self:_apply_styles(styles)
-- Take the styled boxes and encode their styles into a single table,
-- replacing the boxes' style property tables with indices into this table.
local enc_styles = self:_index_styles()
return ui._encode_array("Z", enc_styles)
end
function ui.Window:_get_full_style()
-- The full style contains the theme, global style, and inline element
-- styles as sub-styles, in that order, to ensure the correct precedence.
local styles = {self._theme, self._style}
for _, elem in ipairs(self._elems) do
-- Cascade the inline style with the element's ID, ensuring that the
-- inline style globally refers to this element only.
table.insert(styles, ui.Style{
sel = "#" .. elem._id,
nested = {elem._style},
})
end
-- Return all these styles wrapped up into a single style.
return ui.Style{
nested = styles,
}
end
local function apply_style(elem, boxes, style)
-- Loop through each box, applying the styles accordingly. The table of
-- boxes may be empty, in which case nothing happens.
for _, box in pairs(boxes) do
local name = box.name or "main"
-- If this style resets all properties, find all states that are a
-- subset of the state being styled and clear their property tables.
if style._reset then
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
if bit.band(box.states, i) == box.states then
elem._boxes[name][i] = nil
end
end
end
-- Get the existing style property table for this box if it exists.
local props = elem._boxes[name][box.states] or {}
-- Cascade the properties from this style onto the box.
elem._boxes[name][box.states] = ui._cascade_props(style._props, props)
end
end
function ui.Window:_apply_styles(styles)
-- Loop through each style and element and see if the style properties can
-- be applied to any boxes.
for _, style in ipairs(styles) do
for _, elem in ipairs(self._elems) do
-- Check if the selector for this style. If it matches, apply the
-- style to each of the applicable boxes.
local matches, boxes = style._sel(elem)
if matches then
apply_style(elem, boxes, style)
end
end
end
end
local function index_style(box, i, style_indices, enc_styles)
-- If we have a style for this state, serialize it to a string. Identical
-- styles have identical strings, so we use this to our advantage.
local enc = ui._encode_props(box[i])
-- If we haven't serialized a style identical to this one before, store
-- this as the latest index in the list of style strings.
if not style_indices[enc] then
style_indices[enc] = #enc_styles
table.insert(enc_styles, enc)
end
-- Set the index of our state to the index of its style string, and keep
-- count of how many states with valid indices we have for this box so far.
box[i] = style_indices[enc]
box.n = box.n + 1
end
function ui.Window:_index_styles()
local style_indices = {}
local enc_styles = {}
for _, elem in ipairs(self._elems) do
for _, box in pairs(elem._boxes) do
for i = ui._STATE_NONE, ui._NUM_STATES - 1 do
if box[i] then
-- If this box has a style, encode and index it.
index_style(box, i, style_indices, enc_styles)
else
-- Otherwise, this state has no style, so set it as such.
box[i] = ui._NO_STYLE
end
end
end
end
return enc_styles
end
function ui.Window:_encode_elems()
local enc_elems = {}
for _, elem in ipairs(self._elems) do
table.insert(enc_elems, elem:_encode())
end
return ui._encode_array("Z", enc_elems)
end
local OPEN_WINDOW = 0x00
local REOPEN_WINDOW = 0x01
local UPDATE_WINDOW = 0x02
local CLOSE_WINDOW = 0x03
local last_id = 0
function ui.open(builder, player, context, param)
local id = last_id
last_id = last_id + 1
open_windows[id] = {
builder = builder,
player = player,
context = context or {},
window = nil, -- Set by build_window()
}
local window = build_window(id, param)
local data = ui._encode("BL Z", OPEN_WINDOW, id,
window:_encode(player, true))
core.send_ui_message(player, data)
return id
end
function ui.reopen(close_id, param)
local new_id = last_id
last_id = last_id + 1
open_windows[new_id] = open_windows[close_id]
open_windows[close_id] = nil
local window, player = build_window(new_id, param)
if not window then
return nil
end
local data = ui._encode("BLL Z", REOPEN_WINDOW, new_id, close_id,
window:_encode(player, true))
core.send_ui_message(player, data)
return new_id
end
function ui.update(id, param)
local window, player = build_window(id, param)
if not window then
return
end
local data = ui._encode("BL Z", UPDATE_WINDOW, id,
window:_encode(player, false))
core.send_ui_message(player, data)
end
function ui.close(id)
local info = open_windows[id]
if not info then
return
end
local data = ui._encode("BL", CLOSE_WINDOW, id)
core.send_ui_message(info.player, data)
open_windows[id] = nil
end
function ui.get_window_info(id)
local info = open_windows[id]
if not info then
return nil
end
-- Only return a subset of the fields that are relevant for the caller.
return {
builder = info.builder,
player = info.player,
context = info.context,
}
end
function ui.get_open_windows()
local ids = {}
for id in pairs(open_windows) do
table.insert(ids, id)
end
return ids
end

View File

@ -4015,7 +4015,11 @@ Helper functions
* `minetest.get_us_time()`
* returns time with microsecond precision. May not return wall time.
* `table.copy(table)`: returns a table
* returns a deep copy of `table`
* Returns a deep copy of `table`, i.e. a copy of the table and all its
nested tables.
* `table.shallow_copy(table)`:
* Returns a shallow copy of `table`, i.e. only a copy of the table itself,
but not any of the nested tables.
* `table.indexof(list, val)`: returns the smallest numerical index containing
the value `val` in the table `list`. Non-numerical indices are ignored.
If `val` could not be found, `-1` is returned. `list` must not have
@ -4023,6 +4027,9 @@ Helper functions
* `table.insert_all(table, other_table)`:
* Appends all values in `other_table` to `table` - uses `#table + 1` to
find new indices.
* `table.merge(...)`:
* Merges multiple tables together into a new single table using
`table.insert_all()`.
* `table.key_value_swap(t)`: returns a table with keys and values swapped
* If multiple keys in `t` map to the same value, it is unspecified which
value maps to that key.
@ -5565,6 +5572,9 @@ Utilities
* `minetest.colorspec_to_colorstring(colorspec)`: Converts a ColorSpec to a
ColorString. If the ColorSpec is invalid, returns `nil`.
* `colorspec`: The ColorSpec to convert
* `minetest.colorspec_to_colorint(colorspec)`: Converts a ColorSpec to integer
form. If the ColorSpec is invalid, returns `nil`.
* `colorspec`: The ColorSpec to convert
* `minetest.colorspec_to_bytes(colorspec)`: Converts a ColorSpec to a raw
string of four bytes in an RGBA layout, returned as a string.
* `colorspec`: The ColorSpec to convert
@ -5573,8 +5583,8 @@ Utilities
* `width`: Width of the image
* `height`: Height of the image
* `data`: Image data, one of:
* array table of ColorSpec, length must be width*height
* string with raw RGBA pixels, length must be width*height*4
* array table of ColorSpec, length must be `width * height`
* string with raw RGBA pixels, length must be `width * height * 4`
* `compression`: Optional zlib compression level, number in range 0 to 9.
The data is one-dimensional, starting in the upper left corner of the image
and laid out in scanlines going from left to right, then top to bottom.
@ -5584,6 +5594,43 @@ Utilities
* `minetest.urlencode(str)`: Encodes reserved URI characters by a
percent sign followed by two hex digits. See
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
* `minetest.class([super])`: Creates a new metatable-based class.
* `super` (optional): The superclass (i.e. the metatable) of the newly
created class. If nil, an empty table will be used.
* Lua metamethods may be added to the class, but they are not automatically
inherited. Note that `__index` and `__call` metafields are automatically
added to the metatable.
* When a new object is constructed, the `new()` method, if present, will be
called.
* Example: The following code, demonstrating a simple example of classes
and inheritance, will print `area=6, filled=true`:
```lua
local Shape = minetest.class()
function Shape:new(filled)
self.filled = filled
end
function Shape:describe()
return "area=" .. self:get_area() .. ", filled=" .. tostring(self.filled)
end
local Rectangle = minetest.class(Shape)
function Rectangle:new(filled, width, height)
Shape.new(self, filled)
self.width = width
self.height = height
end
function Rectangle:get_area()
return self.width * self.height
end
local shape = Rectangle(true, 2, 3)
print(shape:describe())
```
* `minetest.is_instance(obj, class)`: Returns true if and only if `obj` is an
instance of `class` or any of its subclasses.
Logging
-------
@ -7121,6 +7168,50 @@ Misc.
* Example: `deserialize('print("foo")')`, returns `nil`
(function call fails), returns
`error:[string "print("foo")"]:1: attempt to call global 'print' (a nil value)`
* `minetest.encode_network(format, ...)`: Encodes numbers and strings in binary
format suitable for network transfer according to a format string.
* Each character in the format string corresponds to an argument to the
function. Possible format characters:
* `b`: Signed 8-bit integer
* `h`: Signed 16-bit integer
* `i`: Signed 32-bit integer
* `l`: Signed 64-bit integer
* `B`: Unsigned 8-bit integer
* `H`: Unsigned 16-bit integer
* `I`: Unsigned 32-bit integer
* `L`: Unsigned 64-bit integer
* `f`: Single-precision floating point number
* `s`: 16-bit size-prefixed string. Max 64 KB in size
* `S`: 32-bit size-prefixed string. Max 64 MB in size
* `z`: Null-terminated string. Cannot have embedded null characters
* `Z`: Verbatim string with no size or terminator
* ` `: Spaces are ignored
* Integers are encoded in big-endian format, and floating point numbers are
encoded in IEEE-754 format. Note that the full range of 64-bit integers
cannot be represented in Lua's doubles.
* If integers outside of the range of the corresponding type are encoded,
integer wraparound will occur.
* If a string that is too long for a size-prefixed string is encoded, it
will be truncated.
* If a string with an embedded null character is encoded as a null
terminated string, it is truncated to the first null character.
* Verbatim strings are added directly to the output as-is and can therefore
have any size or contents, but the code on the decoding end cannot
automatically detect its length.
* `minetest.decode_network(format, data, ...)`: Decodes numbers and strings
from a binary format created by `minetest.encode_network()` according to a
format string.
* The format string follows the same rules as `minetest.encode_network()`.
The decoded values are returned as individual values from the function.
* `Z` has special behavior; an extra argument has to be passed to the
function for every `Z` specifier denoting how many characters to read.
To read all remaining characters, use a size of `-1`.
* If the end of the data is encountered while still reading values from the
string, values of the correct type will still be returned, but strings of
variable length will be truncated, and numbers and verbatim strings will
use zeros for the missing bytes.
* If a size-prefixed string has a size that is greater than the maximum, it
will be truncated and the rest of the characters skipped.
* `minetest.compress(data, method, ...)`: returns `compressed_data`
* Compress a string of data.
* `method` is a string identifying the compression method to be used.

View File

@ -254,3 +254,202 @@ local function test_gennotify_api()
assert(#custom == 0, "custom ids not empty")
end
unittests.register("test_gennotify_api", test_gennotify_api)
unittests.register("test_encode_network", function()
-- 8-bit integers
assert(minetest.encode_network("bbbbbbb", 0, 1, -1, -128, 127, 255, 256) ==
"\x00\x01\xFF\x80\x7F\xFF\x00")
assert(minetest.encode_network("BBBBBBB", 0, 1, -1, -128, 127, 255, 256) ==
"\x00\x01\xFF\x80\x7F\xFF\x00")
-- 16-bit integers
assert(minetest.encode_network("hhhhhhhh",
0, 1, 257, -1,
-32768, 32767, 65535, 65536) ==
"\x00\x00".."\x00\x01".."\x01\x01".."\xFF\xFF"..
"\x80\x00".."\x7F\xFF".."\xFF\xFF".."\x00\x00")
assert(minetest.encode_network("HHHHHHHH",
0, 1, 257, -1,
-32768, 32767, 65535, 65536) ==
"\x00\x00".."\x00\x01".."\x01\x01".."\xFF\xFF"..
"\x80\x00".."\x7F\xFF".."\xFF\xFF".."\x00\x00")
-- 32-bit integers
assert(minetest.encode_network("iiiiiiii",
0, 257, 2^24-1, -1,
-2^31, 2^31-1, 2^32-1, 2^32) ==
"\x00\x00\x00\x00".."\x00\x00\x01\x01".."\x00\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF"..
"\x80\x00\x00\x00".."\x7F\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF".."\x00\x00\x00\x00")
assert(minetest.encode_network("IIIIIIII",
0, 257, 2^24-1, -1,
-2^31, 2^31-1, 2^32-1, 2^32) ==
"\x00\x00\x00\x00".."\x00\x00\x01\x01".."\x00\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF"..
"\x80\x00\x00\x00".."\x7F\xFF\xFF\xFF".."\xFF\xFF\xFF\xFF".."\x00\x00\x00\x00")
-- 64-bit integers
assert(minetest.encode_network("llllll",
0, 1,
511, -1,
2^53-1, -2^53) ==
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")
assert(minetest.encode_network("LLLLLL",
0, 1,
511, -1,
2^53-1, -2^53) ==
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")
-- Strings
local max_16 = string.rep("*", 2^16 - 1)
local max_32 = string.rep("*", 2^26)
assert(minetest.encode_network("ssss",
"", "hello",
max_16, max_16.."too long") ==
"\x00\x00".. "\x00\x05hello"..
"\xFF\xFF"..max_16.."\xFF\xFF"..max_16)
assert(minetest.encode_network("SSSS",
"", "hello",
max_32, max_32.."too long") ==
"\x00\x00\x00\x00".. "\x00\x00\x00\x05hello"..
"\x04\x00\x00\x00"..max_32.."\x04\x00\x00\x00"..max_32)
assert(minetest.encode_network("zzzz",
"", "hello", "hello\0embedded", max_16.."longer") ==
"\0".."hello\0".."hello\0".. max_16.."longer\0")
assert(minetest.encode_network("ZZZZ",
"", "hello", "hello\0embedded", max_16.."longer") ==
"".."hello".."hello\0embedded"..max_16.."longer")
-- Spaces
assert(minetest.encode_network("B I", 255, 2^31) == "\xFF\x80\x00\x00\x00")
assert(minetest.encode_network(" B Zz ", 15, "abc", "xyz") == "\x0Fabcxyz\0")
-- Empty format strings
assert(minetest.encode_network("") == "")
assert(minetest.encode_network(" ", 5, "extra args") == "")
end)
unittests.register("test_decode_network", function()
local d
-- 8-bit integers
d = {minetest.decode_network("bbbbb", "\x00\x01\x7F\x80\xFF")}
assert(#d == 5)
assert(d[1] == 0 and d[2] == 1 and d[3] == 127 and d[4] == -128 and d[5] == -1)
d = {minetest.decode_network("BBBBB", "\x00\x01\x7F\x80\xFF")}
assert(#d == 5)
assert(d[1] == 0 and d[2] == 1 and d[3] == 127 and d[4] == 128 and d[5] == 255)
-- 16-bit integers
d = {minetest.decode_network("hhhhhh",
"\x00\x00".."\x00\x01".."\x01\x01"..
"\x7F\xFF".."\x80\x00".."\xFF\xFF")}
assert(#d == 6)
assert(d[1] == 0 and d[2] == 1 and d[3] == 257 and
d[4] == 32767 and d[5] == -32768 and d[6] == -1)
d = {minetest.decode_network("HHHHHH",
"\x00\x00".."\x00\x01".."\x01\x01"..
"\x7F\xFF".."\x80\x00".."\xFF\xFF")}
assert(#d == 6)
assert(d[1] == 0 and d[2] == 1 and d[3] == 257 and
d[4] == 32767 and d[5] == 32768 and d[6] == 65535)
-- 32-bit integers
d = {minetest.decode_network("iiiiii",
"\x00\x00\x00\x00".."\x00\x00\x00\x01".."\x00\xFF\xFF\xFF"..
"\x7F\xFF\xFF\xFF".."\x80\x00\x00\x00".."\xFF\xFF\xFF\xFF")}
assert(#d == 6)
assert(d[1] == 0 and d[2] == 1 and d[3] == 2^24-1 and
d[4] == 2^31-1 and d[5] == -2^31 and d[6] == -1)
d = {minetest.decode_network("IIIIII",
"\x00\x00\x00\x00".."\x00\x00\x00\x01".."\x00\xFF\xFF\xFF"..
"\x7F\xFF\xFF\xFF".."\x80\x00\x00\x00".."\xFF\xFF\xFF\xFF")}
assert(#d == 6)
assert(d[1] == 0 and d[2] == 1 and d[3] == 2^24-1 and
d[4] == 2^31-1 and d[5] == 2^31 and d[6] == 2^32-1)
-- 64-bit integers
d = {minetest.decode_network("llllll",
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")}
assert(#d == 6)
assert(d[1] == 0 and d[2] == 1 and d[3] == 511 and
d[4] == -1 and d[5] == 2^53-1 and d[6] == -2^53)
d = {minetest.decode_network("LLLLLL",
"\x00\x00\x00\x00\x00\x00\x00\x00".."\x00\x00\x00\x00\x00\x00\x00\x01"..
"\x00\x00\x00\x00\x00\x00\x01\xFF".."\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"..
"\x00\x1F\xFF\xFF\xFF\xFF\xFF\xFF".."\xFF\xE0\x00\x00\x00\x00\x00\x00")}
assert(#d == 6)
assert(d[1] == 0 and d[2] == 1 and d[3] == 511 and
d[4] == 2^64-1 and d[5] == 2^53-1 and d[6] == 2^64 - 2^53)
-- Floating point numbers
local enc = minetest.encode_network("fff",
0.0, 123.456, -987.654)
assert(#enc == 3 * 4)
d = {minetest.decode_network("fff", enc)}
assert(#d == 3)
assert(d[1] == 0.0 and d[2] > 123.45 and d[2] < 123.46 and
d[3] > -987.66 and d[3] < -987.65)
-- Strings
local max_16 = string.rep("*", 2^16 - 1)
local max_32 = string.rep("*", 2^26)
d = {minetest.decode_network("ssss",
"\x00\x00".."\x00\x05hello".."\xFF\xFF"..max_16.."\x00\xFFtoo short")}
assert(#d == 4)
assert(d[1] == "" and d[2] == "hello" and d[3] == max_16 and d[4] == "too short")
d = {minetest.decode_network("SSSSS",
"\x00\x00\x00\x00".."\x00\x00\x00\x05hello"..
"\x04\x00\x00\x00"..max_32.."\x04\x00\x00\x08"..max_32.."too long"..
"\x00\x00\x00\xFFtoo short")}
assert(#d == 5)
assert(d[1] == "" and d[2] == "hello" and
d[3] == max_32 and d[4] == max_32 and d[5] == "too short")
d = {minetest.decode_network("zzzz", "\0".."hello\0".."missing end")}
assert(#d == 4)
assert(d[1] == "" and d[2] == "hello" and d[3] == "missing end" and d[4] == "")
-- Verbatim strings
d = {minetest.decode_network("ZZZZ", "xxxyyyyyzzz", 3, 0, 5, -1)}
assert(#d == 4)
assert(d[1] == "xxx" and d[2] == "" and d[3] == "yyyyy" and d[4] == "zzz")
-- Read past end
d = {minetest.decode_network("bhilBHILf", "")}
assert(#d == 9)
assert(d[1] == 0 and d[2] == 0 and d[3] == 0 and d[4] == 0 and
d[5] == 0 and d[6] == 0 and d[7] == 0 and d[8] == 0 and d[9] == 0.0)
d = {minetest.decode_network("ZsSzZ", "xx", 4, 4)}
assert(#d == 5)
assert(d[1] == "xx\0\0" and d[2] == "" and d[3] == "" and
d[4] == "" and d[5] == "\0\0\0\0")
-- Spaces
d = {minetest.decode_network("B I", "\xFF\x80\x00\x00\x00")}
assert(#d == 2)
assert(d[1] == 255 and d[2] == 2^31)
d = {minetest.decode_network(" B Zz ", "\x0Fabcxyz\0", 3)}
assert(#d == 3)
assert(d[1] == 15 and d[2] == "abc" and d[3] == "xyz")
-- Empty format strings
d = {minetest.decode_network("", "some random data")}
assert(#d == 0)
d = {minetest.decode_network(" ", "some random data", 3, 5)}
assert(#d == 0)
end)

View File

@ -207,6 +207,7 @@ public:
void handleCommand_InventoryFormSpec(NetworkPacket* pkt);
void handleCommand_DetachedInventory(NetworkPacket* pkt);
void handleCommand_ShowFormSpec(NetworkPacket* pkt);
void handleCommand_UiMessage(NetworkPacket* pkt);
void handleCommand_SpawnParticle(NetworkPacket* pkt);
void handleCommand_AddParticleSpawner(NetworkPacket* pkt);
void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt);

View File

@ -37,6 +37,7 @@ enum ClientEventType : u8
CE_DEATHSCREEN,
CE_SHOW_FORMSPEC,
CE_SHOW_LOCAL_FORMSPEC,
CE_UI_MESSAGE,
CE_SPAWN_PARTICLE,
CE_ADD_PARTICLESPAWNER,
CE_DELETE_PARTICLESPAWNER,
@ -106,6 +107,10 @@ struct ClientEvent
std::string *formspec;
std::string *formname;
} show_formspec;
struct
{
std::string *data;
} ui_message;
// struct{
//} textures_updated;
ParticleParameters *spawn_particle;

View File

@ -53,6 +53,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gui/guiOpenURL.h"
#include "gui/guiVolumeChange.h"
#include "gui/mainmenumanager.h"
#include "gui/manager.h"
#include "gui/profilergraph.h"
#include "mapblock.h"
#include "minimap.h"
@ -835,6 +836,7 @@ private:
void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
void handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam);
void handleClientEvent_HandleParticleEvent(ClientEvent *event,
CameraOrientation *cam);
void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
@ -883,6 +885,7 @@ private:
std::unique_ptr<GameUI> m_game_ui;
GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
ui::GUIManagerElem *gui_manager_elem = nullptr; // Free using ->Drop()
MapDrawControl *draw_control = nullptr;
Camera *camera = nullptr;
Clouds *clouds = nullptr; // Free using ->Drop()
@ -1224,6 +1227,8 @@ void Game::shutdown()
if (formspec)
formspec->quitMenu();
ui::g_manager.reset();
// Clear text when exiting.
m_game_ui->clearText();
@ -1237,6 +1242,8 @@ void Game::shutdown()
if (gui_chat_console)
gui_chat_console->drop();
if (gui_manager_elem)
gui_manager_elem->drop();
if (sky)
sky->drop();
@ -1538,6 +1545,8 @@ bool Game::createClient(const GameStartData &start_data)
if (mapper && client->modsLoaded())
client->getScript()->on_minimap_ready(mapper);
ui::g_manager.setClient(client);
return true;
}
@ -1555,6 +1564,9 @@ bool Game::initGui()
gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
-1, chat_backend, client, &g_menumgr);
// Thingy to draw UI manager after chat but before formspecs.
gui_manager_elem = new ui::GUIManagerElem(guienv, guiroot, -1);
if (g_touchscreengui)
g_touchscreengui->init(texture_src);
@ -2793,6 +2805,7 @@ const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
{&Game::handleClientEvent_Deathscreen},
{&Game::handleClientEvent_ShowFormSpec},
{&Game::handleClientEvent_ShowLocalFormSpec},
{&Game::handleClientEvent_UiMessage},
{&Game::handleClientEvent_HandleParticleEvent},
{&Game::handleClientEvent_HandleParticleEvent},
{&Game::handleClientEvent_HandleParticleEvent},
@ -2898,6 +2911,12 @@ void Game::handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrienta
delete event->show_formspec.formname;
}
void Game::handleClientEvent_UiMessage(ClientEvent *event, CameraOrientation *cam)
{
ui::g_manager.receiveMessage(*event->ui_message.data);
delete event->ui_message.data;
}
void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event,
CameraOrientation *cam)
{
@ -4310,7 +4329,7 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats)
draw_crosshair = false;
this->m_rendering_engine->draw_scene(sky_color, this->m_game_ui->m_flags.show_hud,
draw_wield_tool, draw_crosshair);
this->m_game_ui->m_flags.show_chat, draw_wield_tool, draw_crosshair);
/*
Profiler graph

View File

@ -36,7 +36,7 @@ RenderingCore::~RenderingCore()
delete shadow_renderer;
}
void RenderingCore::draw(video::SColor _skycolor, bool _show_hud,
void RenderingCore::draw(video::SColor _skycolor, bool _show_hud, bool _show_chat,
bool _draw_wield_tool, bool _draw_crosshair)
{
v2u32 screensize = device->getVideoDriver()->getScreenSize();
@ -46,6 +46,7 @@ void RenderingCore::draw(video::SColor _skycolor, bool _show_hud,
context.draw_crosshair = _draw_crosshair;
context.draw_wield_tool = _draw_wield_tool;
context.show_hud = _show_hud;
context.show_chat = _show_chat;
pipeline->reset(context);
pipeline->run(context);

View File

@ -53,7 +53,7 @@ public:
RenderingCore &operator=(const RenderingCore &) = delete;
RenderingCore &operator=(RenderingCore &&) = delete;
void draw(video::SColor _skycolor, bool _show_hud,
void draw(video::SColor _skycolor, bool _show_hud, bool _show_chat,
bool _draw_wield_tool, bool _draw_crosshair);
v2u32 getVirtualSize() const;

View File

@ -46,6 +46,7 @@ struct PipelineContext
v2u32 target_size;
bool show_hud {true};
bool show_chat {true};
bool draw_wield_tool {true};
bool draw_crosshair {true};
};

View File

@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "client/hud.h"
#include "client/minimap.h"
#include "client/shadows/dynamicshadowsrender.h"
#include "gui/manager.h"
/// Draw3D pipeline step
void Draw3D::run(PipelineContext &context)
@ -43,6 +44,9 @@ void Draw3D::run(PipelineContext &context)
void DrawWield::run(PipelineContext &context)
{
ui::g_manager.preDraw();
ui::g_manager.drawType(ui::WindowType::BG);
if (m_target)
m_target->activate(context);
@ -60,12 +64,24 @@ void DrawHUD::run(PipelineContext &context)
if (context.draw_crosshair)
context.hud->drawCrosshair();
}
ui::g_manager.drawType(ui::WindowType::MASK);
if (context.show_hud) {
context.hud->drawHotbar(context.client->getEnv().getLocalPlayer()->getWieldIndex());
context.hud->drawLuaElements(context.client->getCamera()->getOffset());
ui::g_manager.drawType(ui::WindowType::HUD);
context.client->getCamera()->drawNametags();
}
if (context.show_chat)
ui::g_manager.drawType(ui::WindowType::MESSAGE);
context.device->getGUIEnvironment()->drawAll();
ui::g_manager.drawType(ui::WindowType::FG);
}

View File

@ -411,10 +411,10 @@ void RenderingEngine::finalize()
core.reset();
}
void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud,
void RenderingEngine::draw_scene(video::SColor skycolor, bool show_hud, bool show_chat,
bool draw_wield_tool, bool draw_crosshair)
{
core->draw(skycolor, show_hud, draw_wield_tool, draw_crosshair);
core->draw(skycolor, show_hud, show_chat, draw_wield_tool, draw_crosshair);
}
const VideoDriverInfo &RenderingEngine::getVideoDriverInfo(irr::video::E_DRIVER_TYPE type)

View File

@ -141,7 +141,7 @@ public:
gui::IGUIEnvironment *guienv, ITextureSource *tsrc,
float dtime = 0, int percent = 0, bool sky = true);
void draw_scene(video::SColor skycolor, bool show_hud,
void draw_scene(video::SColor skycolor, bool show_hud, bool show_chat,
bool draw_wield_tool, bool draw_crosshair);
void initialize(Client *client, Hud *hud);

View File

@ -1,4 +1,7 @@
set(gui_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/box.cpp
${CMAKE_CURRENT_SOURCE_DIR}/elem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/generic_elems.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiAnimatedImage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiBackgroundImage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiBox.cpp
@ -23,8 +26,11 @@ set(gui_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiHyperText.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp
${CMAKE_CURRENT_SOURCE_DIR}/manager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp
${CMAKE_CURRENT_SOURCE_DIR}/texture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/touchscreengui.cpp
${CMAKE_CURRENT_SOURCE_DIR}/window.cpp
PARENT_SCOPE
)

452
src/gui/box.cpp Normal file
View File

@ -0,0 +1,452 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui/box.h"
#include "debug.h"
#include "log.h"
#include "porting.h"
#include "gui/elem.h"
#include "gui/manager.h"
#include "gui/window.h"
#include "util/serialize.h"
namespace ui
{
Align toAlign(u8 align)
{
if (align >= (u8)Align::MAX_ALIGN) {
return Align::CENTER;
}
return (Align)align;
}
void Layer::reset()
{
image = Texture();
fill = BLANK;
tint = WHITE;
source = rf32(0.0f, 0.0f, 1.0f, 1.0f);
middle = rf32(0.0f, 0.0f, 0.0f, 0.0f);
middle_scale = 1.0f;
num_frames = 1;
frame_time = 1000;
}
void Layer::read(std::istream &full_is)
{
auto is = newIs(readStr16(full_is));
u32 set_mask = readU32(is);
if (testShift(set_mask))
image = g_manager.getTexture(readNullStr(is));
if (testShift(set_mask))
fill = readARGB8(is);
if (testShift(set_mask))
tint = readARGB8(is);
if (testShift(set_mask)) {
source.UpperLeftCorner = readV2F32(is);
source.LowerRightCorner = readV2F32(is);
}
if (testShift(set_mask)) {
middle.UpperLeftCorner = clamp_vec(readV2F32(is));
middle.LowerRightCorner = clamp_vec(readV2F32(is));
}
if (testShift(set_mask))
middle_scale = std::max(readF32(is), 0.0f);
if (testShift(set_mask))
num_frames = std::max(readU32(is), 1U);
if (testShift(set_mask))
frame_time = std::max(readU32(is), 1U);
}
void Style::reset()
{
size = d2f32(0.0f, 0.0f);
rel_pos = v2f32(0.0f, 0.0f);
rel_anchor = v2f32(0.0f, 0.0f);
rel_size = d2s32(1.0f, 1.0f);
margin = rf32(0.0f, 0.0f, 0.0f, 0.0f);
padding = rf32(0.0f, 0.0f, 0.0f, 0.0f);
bg.reset();
fg.reset();
fg_scale = 1.0f;
fg_halign = Align::CENTER;
fg_valign = Align::CENTER;
visible = true;
noclip = false;
}
void Style::read(std::istream &is)
{
// No need to read a size prefix; styles are already read in as size-
// prefixed strings in Window.
u32 set_mask = readU32(is);
if (testShift(set_mask))
size = clamp_vec(readV2F32(is));
if (testShift(set_mask))
rel_pos = readV2F32(is);
if (testShift(set_mask))
rel_anchor = readV2F32(is);
if (testShift(set_mask))
rel_size = clamp_vec(readV2F32(is));
if (testShift(set_mask)) {
margin.UpperLeftCorner = readV2F32(is);
margin.LowerRightCorner = readV2F32(is);
}
if (testShift(set_mask)) {
padding.UpperLeftCorner = readV2F32(is);
padding.LowerRightCorner = readV2F32(is);
}
if (testShift(set_mask))
bg.read(is);
if (testShift(set_mask))
fg.read(is);
if (testShift(set_mask))
fg_scale = std::max(readF32(is), 0.0f);
if (testShift(set_mask))
fg_halign = toAlign(readU8(is));
if (testShift(set_mask))
fg_valign = toAlign(readU8(is));
if (testShift(set_mask))
visible = testShift(set_mask);
if (testShift(set_mask))
noclip = testShift(set_mask);
}
Window &Box::getWindow()
{
return m_elem.getWindow();
}
const Window &Box::getWindow() const
{
return m_elem.getWindow();
}
void Box::reset()
{
m_style.reset();
for (State i = 0; i < m_style_refs.size(); i++) {
m_style_refs[i] = NO_STYLE;
}
m_draw_rect = rf32(0.0f, 0.0f, 0.0f, 0.0f);
m_child_rect = m_draw_rect;
m_clip_rect = m_draw_rect;
}
void Box::read(std::istream &full_is)
{
auto is = newIs(readStr16(full_is));
u32 style_mask = readU32(is);
for (State i = 0; i < m_style_refs.size(); i++) {
// If we have a style for this state in the mask, add it to the
// list of styles.
if (!testShift(style_mask)) {
continue;
}
u32 style = readU32(is);
if (getWindow().getStyleStr(style) != nullptr) {
m_style_refs[i] = style;
} else {
errorstream << "Style " << style << " does not exist" << std::endl;
}
}
}
void Box::layout(const rf32 &parent_rect, const rf32 &parent_clip)
{
// Before we layout the box, we need to recompute the style so we have
// fully updated style properties.
computeStyle();
// First, calculate the size of the box in absolute coordinates based
// on the normalized size.
d2f32 origin_size(
(m_style.rel_size.Width * parent_rect.getWidth()),
(m_style.rel_size.Height * parent_rect.getHeight())
);
// Ensure that the normalized size of the box isn't smaller than the
// minimum size.
origin_size.Width = std::max(origin_size.Width, m_style.size.Width);
origin_size.Height = std::max(origin_size.Height, m_style.size.Height);
// Then, create the rect of the box relative to the origin by
// converting the normalized position absolute coordinates, while
// accounting for the anchor based on the previously calculated size.
v2f32 origin_pos(
(m_style.rel_pos.X * parent_rect.getWidth()) -
(m_style.rel_anchor.X * origin_size.Width),
(m_style.rel_pos.Y * parent_rect.getHeight()) -
(m_style.rel_anchor.Y * origin_size.Height)
);
rf32 origin_rect(origin_pos, origin_size);
// The absolute rect of the box is made by shifting the origin to the
// top left of the parent rect.
rf32 abs_rect = origin_rect + parent_rect.UpperLeftCorner;
// The rect we draw to is the absolute rect adjusted for the margins.
// Since this is the final rect, we ensure that it doesn't have a
// negative size.
m_draw_rect = clamp_rect(rf32(
abs_rect.UpperLeftCorner + m_style.margin.UpperLeftCorner,
abs_rect.LowerRightCorner - m_style.margin.LowerRightCorner
));
// The rect that children and the foreground layer are drawn relative
// to is the draw rect adjusted for padding. Make sure this rect is
// never negative as well.
m_child_rect = clamp_rect(rf32(
m_draw_rect.UpperLeftCorner + m_style.padding.UpperLeftCorner,
m_draw_rect.LowerRightCorner - m_style.padding.LowerRightCorner
));
// If we are set to noclip, we clip to the same rect we draw to.
// Otherwise, the clip rect is the drawing rect clipped against the
// parent clip rect.
m_clip_rect = m_style.noclip ? m_draw_rect : clip_rect(m_draw_rect, parent_clip);
}
void Box::draw(Canvas &parent)
{
// Since layout() is always called before draw(), we already have fully
// updated style properties.
// Don't draw anything if we aren't visible.
if (!m_style.visible) {
return;
}
// Create a new canvas relative to our parent to draw to.
Canvas canvas(parent, m_style.noclip ? nullptr : &m_clip_rect);
// Draw our background and foreground layers.
drawLayer(canvas, m_style.bg, m_draw_rect);
drawForeground(canvas);
}
void Box::drawForeground(Canvas &canvas)
{
// It makes no sense to draw a foreground when there's no image, since
// it would otherwise take no room.
if (!m_style.fg.image.isTexture()) {
return;
}
// The foreground layer is aligned and scaled in a particular area of
// the box. First, get the size of the foreground layer.
d2f32 src_size = m_style.fg.source.getSize();
src_size.Height /= m_style.fg.num_frames;
d2s32 tex_size = m_style.fg.image.getSize();
src_size.Width *= tex_size.Width;
src_size.Height *= tex_size.Height;
// Then, compute the scale that we should use. A scale of zero means
// the image should take up as much room as possible while still
// preserving the aspect ratio of the image.
float scale = m_style.fg_scale;
if (scale == 0.0f) {
scale = std::min(
m_child_rect.getWidth() / src_size.Width,
m_child_rect.getHeight() / src_size.Height
);
}
d2f32 fg_size(src_size.Width * scale, src_size.Height * scale);
// Now, using the alignment options, position the foreground image
// inside the remaining space.
v2f32 fg_pos = m_child_rect.UpperLeftCorner;
if (m_style.fg_halign == Align::CENTER) {
fg_pos.X += (m_child_rect.getWidth() - fg_size.Width) / 2.0f;
} else if (m_style.fg_halign == Align::END) {
fg_pos.X += m_child_rect.getWidth() - fg_size.Width;
}
if (m_style.fg_valign == Align::CENTER) {
fg_pos.Y += (m_child_rect.getHeight() - fg_size.Height) / 2.0f;
} else if (m_style.fg_valign == Align::END) {
fg_pos.Y += m_child_rect.getHeight() - fg_size.Height;
}
// We have our position and size, so now we can draw the layer.
drawLayer(canvas, m_style.fg, rf32(fg_pos, fg_size));
}
void Box::drawLayer(Canvas &canvas, const Layer &layer, const rf32 &dst)
{
// Draw the fill color if it's not totally transparent.
if (layer.fill.getAlpha() != 0x0) {
canvas.drawRect(dst, layer.fill);
}
// If there's no image, there's nothing else for us to do.
if (!layer.image.isTexture()) {
return;
}
// If we have animations, we need to adjust the source rect by the
// frame offset in accordance with the current frame.
rf32 src = layer.source;
if (layer.num_frames > 1) {
float frame_height = src.getHeight() / layer.num_frames;
src.LowerRightCorner.Y = src.UpperLeftCorner.Y + frame_height;
float frame_offset = frame_height *
((porting::getTimeMs() / layer.frame_time) % layer.num_frames);
src.UpperLeftCorner.Y += frame_offset;
src.LowerRightCorner.Y += frame_offset;
}
// If the source rect for this image is flipped, we need to flip the
// sign of our middle rect as well to get the right adjustments.
rf32 src_middle = layer.middle;
if (src.getWidth() < 0.0f) {
src_middle.UpperLeftCorner.X = -src_middle.UpperLeftCorner.X;
src_middle.LowerRightCorner.X = -src_middle.LowerRightCorner.X;
}
if (src.getHeight() < 0.0f) {
src_middle.UpperLeftCorner.Y = -src_middle.UpperLeftCorner.Y;
src_middle.LowerRightCorner.Y = -src_middle.LowerRightCorner.Y;
}
// Now we need to draw the texture as a nine-slice image. But first,
// since the middle rect uses normalized coordinates, we need to
// de-normalize it into actual pixels for the destination rect and
// scale it by the middle rect scaling parameter.
rf32 scaled_middle(
layer.middle.UpperLeftCorner.X * layer.middle_scale * layer.image.getWidth(),
layer.middle.UpperLeftCorner.Y * layer.middle_scale * layer.image.getHeight(),
layer.middle.LowerRightCorner.X * layer.middle_scale * layer.image.getWidth(),
layer.middle.LowerRightCorner.Y * layer.middle_scale * layer.image.getHeight()
);
// Now draw each slice of the nine-slice image. If the middle rect
// equals the whole source rect, this will automatically act like a
// normal image.
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
rf32 slice_src = src;
rf32 slice_dst = dst;
switch (x) {
case 0:
slice_dst.LowerRightCorner.X =
dst.UpperLeftCorner.X + scaled_middle.UpperLeftCorner.X;
slice_src.LowerRightCorner.X =
src.UpperLeftCorner.X + src_middle.UpperLeftCorner.X;
break;
case 1:
slice_dst.UpperLeftCorner.X += scaled_middle.UpperLeftCorner.X;
slice_dst.LowerRightCorner.X -= scaled_middle.LowerRightCorner.X;
slice_src.UpperLeftCorner.X += src_middle.UpperLeftCorner.X;
slice_src.LowerRightCorner.X -= src_middle.LowerRightCorner.X;
break;
case 2:
slice_dst.UpperLeftCorner.X =
dst.LowerRightCorner.X - scaled_middle.LowerRightCorner.X;
slice_src.UpperLeftCorner.X =
src.LowerRightCorner.X - src_middle.LowerRightCorner.X;
break;
}
switch (y) {
case 0:
slice_dst.LowerRightCorner.Y =
dst.UpperLeftCorner.Y + scaled_middle.UpperLeftCorner.Y;
slice_src.LowerRightCorner.Y =
src.UpperLeftCorner.Y + src_middle.UpperLeftCorner.Y;
break;
case 1:
slice_dst.UpperLeftCorner.Y += scaled_middle.UpperLeftCorner.Y;
slice_dst.LowerRightCorner.Y -= scaled_middle.LowerRightCorner.Y;
slice_src.UpperLeftCorner.Y += src_middle.UpperLeftCorner.Y;
slice_src.LowerRightCorner.Y -= src_middle.LowerRightCorner.Y;
break;
case 2:
slice_dst.UpperLeftCorner.Y =
dst.LowerRightCorner.Y - scaled_middle.LowerRightCorner.Y;
slice_src.UpperLeftCorner.Y =
src.LowerRightCorner.Y - src_middle.LowerRightCorner.Y;
break;
}
// Draw this slice of the texture with the proper tint.
canvas.drawTexture(slice_dst, layer.image, slice_src, layer.tint);
}
}
}
void Box::computeStyle()
{
// First, clear our current style and compute what state we're in.
m_style.reset();
State state = STATE_NONE;
// Loop over each style state from lowest precedence to highest since
// they should be applied in that order.
for (State i = 0; i < m_style_refs.size(); i++) {
// If this state we're looking at is a subset of the current state,
// then it's a match for styling.
if ((state & i) != i) {
continue;
}
u32 index = m_style_refs[i];
// If the index for this state has an associated style string,
// apply it to our current style.
if (index != NO_STYLE) {
auto is = newIs(*getWindow().getStyleStr(index));
m_style.read(is);
}
}
}
}

163
src/gui/box.h Normal file
View File

@ -0,0 +1,163 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irrlichttypes_extrabloated.h"
#include "gui/texture.h"
#include "util/basic_macros.h"
#include <array>
#include <iostream>
#include <string>
namespace ui
{
// Serialized enum; do not change order of entries.
enum class Align
{
START,
CENTER,
END,
MAX_ALIGN,
};
Align toAlign(u8 align);
struct Layer
{
Texture image;
video::SColor fill;
video::SColor tint;
rf32 source;
rf32 middle;
float middle_scale;
u32 num_frames;
u32 frame_time;
Layer()
{
reset();
}
void reset();
void read(std::istream &is);
};
struct Style
{
d2f32 size;
v2f32 rel_pos;
v2f32 rel_anchor;
d2f32 rel_size;
rf32 margin;
rf32 padding;
Layer bg;
Layer fg;
float fg_scale;
Align fg_halign;
Align fg_valign;
bool visible;
bool noclip;
Style()
{
reset();
}
void reset();
void read(std::istream &is);
};
class Elem;
class Window;
class Box
{
public:
using State = u32;
// These states are organized in order of precedence. States with a
// larger value will override the styles of states with a lower value.
static constexpr State STATE_NONE = 0;
static constexpr State STATE_FOCUSED = 1 << 0;
static constexpr State STATE_SELECTED = 1 << 1;
static constexpr State STATE_HOVERED = 1 << 2;
static constexpr State STATE_PRESSED = 1 << 3;
static constexpr State STATE_DISABLED = 1 << 4;
static constexpr State NUM_STATES = 1 << 5;
private:
// Indicates that there is no style string for this state combination.
static constexpr u32 NO_STYLE = -1;
Elem &m_elem;
Style m_style;
std::array<u32, NUM_STATES> m_style_refs;
rf32 m_draw_rect;
rf32 m_child_rect;
rf32 m_clip_rect;
public:
Box(Elem &elem) :
m_elem(elem)
{
reset();
}
DISABLE_CLASS_COPY(Box)
ALLOW_CLASS_MOVE(Box)
Elem &getElem() { return m_elem; }
const Elem &getElem() const { return m_elem; }
Window &getWindow();
const Window &getWindow() const;
const Style &getStyle() const { return m_style; }
const rf32 &getDrawRect() const { return m_draw_rect; }
const rf32 &getChildRect() const { return m_child_rect; }
const rf32 &getChildClip() const { return m_clip_rect; }
void reset();
void read(std::istream &is);
void layout(const rf32 &parent_rect, const rf32 &parent_clip);
void draw(Canvas &parent);
private:
void drawForeground(Canvas &canvas);
void drawLayer(Canvas &canvas, const Layer &layer, const rf32 &dst);
void computeStyle();
};
}

147
src/gui/elem.cpp Normal file
View File

@ -0,0 +1,147 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui/elem.h"
#include "debug.h"
#include "log.h"
#include "gui/manager.h"
#include "gui/window.h"
#include "util/serialize.h"
// Include every element header for Elem::create()
#include "gui/generic_elems.h"
namespace ui
{
std::unique_ptr<Elem> Elem::create(Type type, Window &window, std::string id)
{
std::unique_ptr<Elem> elem = nullptr;
#define CREATE(name, type) \
case name: \
elem = std::make_unique<type>(window, std::move(id)); \
break
switch (type) {
CREATE(ELEM, Elem);
CREATE(ROOT, Root);
default:
return nullptr;
}
#undef CREATE
// It's a pain to call reset() in the constructor of every single
// element due to how virtual functions work in C++, so we reset
// elements after creating them here.
elem->reset();
return elem;
}
Elem::Elem(Window &window, std::string id) :
m_window(window),
m_id(std::move(id)),
m_main_box(*this)
{}
void Elem::reset()
{
m_order = (size_t)-1;
m_parent = nullptr;
m_children.clear();
m_main_box.reset();
}
void Elem::read(std::istream &is)
{
u32 set_mask = readU32(is);
if (testShift(set_mask))
readChildren(is);
if (testShift(set_mask))
m_main_box.read(is);
}
void Elem::layout(const rf32 &parent_rect, const rf32 &parent_clip)
{
layoutBoxes(parent_rect, parent_clip);
layoutChildren();
}
void Elem::drawAll(Canvas &canvas)
{
draw(canvas);
for (Elem *child : m_children) {
child->drawAll(canvas);
}
}
void Elem::layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip)
{
m_main_box.layout(parent_rect, parent_clip);
}
void Elem::layoutChildren()
{
for (Elem *child : m_children) {
child->layout(m_main_box.getChildRect(), m_main_box.getChildClip());
}
}
void Elem::draw(Canvas &canvas)
{
m_main_box.draw(canvas);
}
void Elem::readChildren(std::istream &is)
{
u32 num_children = readU32(is);
for (size_t i = 0; i < num_children; i++) {
std::string id = readNullStr(is);
Elem *child = m_window.getElem(id, true);
if (child == nullptr) {
continue;
}
/* Check if this child already has a parent before adding it as a
* child. Elements are deserialized in unspecified order rather
* than a prefix order of parents before their children, so
* isolated circular element refrences are still possible. However,
* cycles including the root are impossible, so recursion starting
* with the root element is safe and will always terminate.
*/
if (child->m_parent != nullptr) {
errorstream << "Element \"" << id << "\" already has parent \"" <<
child->m_parent->m_id << "\"" << std::endl;
} else if (child == m_window.getRoot()) {
errorstream << "Element \"" << id <<
"\" is the root element and cannot have a parent" << std::endl;
} else {
m_children.push_back(child);
child->m_parent = this;
}
}
}
}

98
src/gui/elem.h Normal file
View File

@ -0,0 +1,98 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irrlichttypes_extrabloated.h"
#include "gui/box.h"
#include "util/basic_macros.h"
#include <iostream>
#include <memory>
#include <string>
#include <vector>
namespace ui
{
class Window;
class Elem
{
public:
// Serialized enum; do not change values of entries.
enum Type
{
ELEM = 0x00,
ROOT = 0x01,
};
private:
// The window and ID are intrinsic to the element's identity, so they
// are set by the constructor and aren't cleared in reset() or changed
// in read().
Window &m_window;
std::string m_id;
size_t m_order;
Elem *m_parent;
std::vector<Elem *> m_children;
Box m_main_box;
public:
static std::unique_ptr<Elem> create(Type type, Window &window, std::string id);
Elem(Window &window, std::string id);
DISABLE_CLASS_COPY(Elem)
ALLOW_CLASS_MOVE(Elem)
virtual ~Elem() = default;
Window &getWindow() { return m_window; }
const Window &getWindow() const { return m_window; }
const std::string &getId() const { return m_id; }
virtual Type getType() const { return ELEM; }
size_t getOrder() { return m_order; }
void setOrder(size_t order) { m_order = order; }
Elem *getParent() { return m_parent; }
const std::vector<Elem *> &getChildren() { return m_children; }
Box &getMainBox() { return m_main_box; }
virtual void reset();
virtual void read(std::istream &is);
void layout(const rf32 &parent_rect, const rf32 &parent_clip);
void drawAll(Canvas &canvas);
protected:
virtual void layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip);
virtual void layoutChildren();
virtual void draw(Canvas &canvas);
private:
void readChildren(std::istream &is);
};
}

58
src/gui/generic_elems.cpp Normal file
View File

@ -0,0 +1,58 @@
/*
Minetest
Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui/generic_elems.h"
#include "debug.h"
#include "log.h"
#include "gui/manager.h"
#include "util/serialize.h"
namespace ui
{
void Root::reset()
{
Elem::reset();
m_backdrop_box.reset();
}
void Root::read(std::istream &is)
{
auto super = newIs(readStr32(is));
Elem::read(super);
u32 set_mask = readU32(is);
if (testShift(set_mask))
m_backdrop_box.read(is);
}
void Root::layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip)
{
m_backdrop_box.layout(parent_rect, parent_clip);
Elem::layoutBoxes(m_backdrop_box.getChildRect(), m_backdrop_box.getChildClip());
}
void Root::draw(Canvas &canvas)
{
m_backdrop_box.draw(canvas);
Elem::draw(canvas);
}
}

52
src/gui/generic_elems.h Normal file
View File

@ -0,0 +1,52 @@
/*
Minetest
Copyright (C) 2024 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irrlichttypes_extrabloated.h"
#include "gui/box.h"
#include "gui/elem.h"
#include <iostream>
#include <string>
namespace ui
{
class Root : public Elem
{
private:
Box m_backdrop_box;
public:
Root(Window &window, std::string id) :
Elem(window, std::move(id)),
m_backdrop_box(*this)
{}
virtual Type getType() const override { return ROOT; }
virtual void reset() override;
virtual void read(std::istream &is) override;
protected:
virtual void layoutBoxes(const rf32 &parent_rect, const rf32 &parent_clip);
virtual void draw(Canvas &canvas);
};
}

192
src/gui/manager.cpp Normal file
View File

@ -0,0 +1,192 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui/manager.h"
#include "debug.h"
#include "log.h"
#include "settings.h"
#include "client/client.h"
#include "client/renderingengine.h"
#include "client/tile.h"
#include "util/serialize.h"
namespace ui
{
bool testShift(u32 &bits)
{
bool test = bits & 1;
bits >>= 1;
return test;
}
std::string readStr16(std::istream &is)
{
return deSerializeString16(is, true);
}
std::string readStr32(std::istream &is)
{
return deSerializeString32(is, true);
}
std::string readNullStr(std::istream &is)
{
std::string str;
std::getline(is, str, '\0');
return str;
}
void writeStr16(std::ostream &os, const std::string &str)
{
os << serializeString16(str, true);
}
void writeStr32(std::ostream &os, const std::string &str)
{
os << serializeString32(str, true);
}
void writeNullStr(std::ostream &os, const std::string &str)
{
os << str.substr(0, strlen(str.c_str())) << '\0';
}
std::istringstream newIs(std::string str)
{
return std::istringstream(std::move(str), std::ios_base::binary);
}
std::ostringstream newOs()
{
return std::ostringstream(std::ios_base::binary);
}
Texture Manager::getTexture(const std::string &name) const
{
return Texture(m_client->tsrc()->getTexture(name));
}
float Manager::getPixelSize(WindowType type) const
{
if (type == WindowType::GUI || type == WindowType::MESSAGE) {
return m_gui_pixel_size;
}
return m_hud_pixel_size;
}
d2f32 Manager::getScreenSize(WindowType type) const
{
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
d2u32 screen_size = driver->getScreenSize();
float pixel_size = getPixelSize(type);
return d2f32(
screen_size.Width / pixel_size,
screen_size.Height / pixel_size
);
}
void Manager::reset()
{
m_client = nullptr;
m_windows.clear();
}
void Manager::removeWindow(u64 id)
{
auto it = m_windows.find(id);
if (it == m_windows.end()) {
infostream << "Window " << id << " is already closed" << std::endl;
return;
}
m_windows.erase(it);
}
void Manager::receiveMessage(const std::string &data)
{
auto is = newIs(data);
u32 action = readU8(is);
u64 id = readU64(is);
switch (action) {
case REOPEN_WINDOW: {
u64 close_id = readU64(is);
removeWindow(close_id);
}
// fallthrough
case OPEN_WINDOW: {
auto it = m_windows.find(id);
if (it != m_windows.end()) {
infostream << "Window " << id << " is already open" << std::endl;
break;
}
it = m_windows.emplace(id, Window(id)).first;
it->second.read(is, true);
break;
}
case UPDATE_WINDOW: {
auto it = m_windows.find(id);
if (it != m_windows.end()) {
it->second.read(is, false);
} else {
infostream << "Window " << id << " does not exist" << std::endl;
}
break;
}
case CLOSE_WINDOW:
removeWindow(id);
break;
default:
errorstream << "Invalid manager action: " << action << std::endl;
break;
}
}
void Manager::preDraw()
{
float base_size = RenderingEngine::getDisplayDensity();
m_gui_pixel_size = base_size * g_settings->getFloat("gui_scaling");
m_hud_pixel_size = base_size * g_settings->getFloat("hud_scaling");
}
void Manager::drawType(WindowType type)
{
Texture::begin();
for (auto &it : m_windows) {
if (it.second.getType() == type) {
it.second.drawAll();
}
}
Texture::end();
}
Manager g_manager;
}

120
src/gui/manager.h Normal file
View File

@ -0,0 +1,120 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irrlichttypes_extrabloated.h"
#include "gui/texture.h"
#include "gui/window.h"
#include "util/basic_macros.h"
#include <iostream>
#include <map>
#include <sstream>
#include <string>
class Client;
namespace ui
{
// Define a few functions that are particularly useful for UI serialization
// and deserialization.
bool testShift(u32 &bits);
// The UI purposefully avoids dealing with SerializationError, so it uses
// always uses truncating or null-terminated string functions. Hence, we
// make convenience wrappers around the string functions in "serialize.h".
std::string readStr16(std::istream &is);
std::string readStr32(std::istream &is);
std::string readNullStr(std::istream &is);
void writeStr16(std::ostream &os, const std::string &str);
void writeStr32(std::ostream &os, const std::string &str);
void writeNullStr(std::ostream &os, const std::string &str);
// Convenience functions to create new binary string streams.
std::istringstream newIs(std::string str);
std::ostringstream newOs();
class Manager
{
public:
// Serialized enum; do not change values of entries.
enum ReceiveAction
{
OPEN_WINDOW = 0x00,
REOPEN_WINDOW = 0x01,
UPDATE_WINDOW = 0x02,
CLOSE_WINDOW = 0x03,
};
private:
Client *m_client;
float m_gui_pixel_size = 0.0f;
float m_hud_pixel_size = 0.0f;
// Use map rather than unordered_map so that windows are always sorted
// by window ID to make sure that they are drawn in order of creation.
std::map<u64, Window> m_windows;
public:
Manager()
{
reset();
}
DISABLE_CLASS_COPY(Manager)
Client *getClient() const { return m_client; }
void setClient(Client *client) { m_client = client; }
Texture getTexture(const std::string &name) const;
float getPixelSize(WindowType type) const;
d2f32 getScreenSize(WindowType type) const;
void reset();
void removeWindow(u64 id);
void receiveMessage(const std::string &data);
void preDraw();
void drawType(WindowType type);
};
extern Manager g_manager;
// Inconveniently, we need a way to draw the "gui" window types after the
// chat console but before other GUIs like the key change menu, formspecs,
// etc. So, we inject our own mini Irrlicht element in between.
class GUIManagerElem : public gui::IGUIElement
{
public:
GUIManagerElem(gui::IGUIEnvironment* env, gui::IGUIElement* parent, s32 id) :
gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rs32())
{}
virtual void draw() override
{
g_manager.drawType(ui::WindowType::GUI);
gui::IGUIElement::draw();
}
};
}

255
src/gui/texture.cpp Normal file
View File

@ -0,0 +1,255 @@
/*
Minetest
Copyright (C) 2022 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui/texture.h"
#include "client/renderingengine.h"
#include "debug.h"
#include "log.h"
namespace ui
{
static video::IVideoDriver *driver() {
return RenderingEngine::get_video_driver();
}
// The current render target. We keep track of this in set_as_target() so
// as to only change render targets when necessary. It is only valid
// between start_drawing() and end_drawing(); otherwise, it's nullptr.
video::ITexture *s_current_target = nullptr;
static void force_screen_target()
{
// Force-set the render target to the screen, regardless of the current
// value of s_current_target.
driver()->setRenderTarget(nullptr, false, 0);
s_current_target = nullptr;
}
static void set_as_target(video::ITexture *target, bool clear = false,
video::SColor color = BLANK)
{
// Don't change the render target if it's already set.
if (s_current_target == target) {
return;
}
// If we want to clear it to a certain color, then we go ahead and
// clear the depth buffer as well.
u16 to_clear = clear ? video::ECBF_ALL : video::ECBF_NONE;
if (driver()->setRenderTarget(target, to_clear, color)) {
// The call succeeded, so update the current target variable.
s_current_target = target;
} else {
// The call failed, so we probably don't support render targets.
errorstream << "Unable to set render target" << std::endl;
force_screen_target();
}
}
void Texture::begin()
{
// Force set the target since we don't know what the target was before,
// so s_current_target could possibly be invalid.
force_screen_target();
}
void Texture::end()
{
set_as_target(nullptr);
}
Texture Texture::screen = {};
Texture::Texture(const d2s32 &size)
{
if (driver()->queryFeature(video::EVDF_RENDER_TO_TARGET)) {
m_texture.grab(driver()->addRenderTargetTexture(d2u32(size)));
if (!isTexture()) {
errorstream << "Unable to create render target" << std::endl;
}
// The default contents of a texture are appear to be unspecified,
// even though some it's usually transparent by default. So, we
// explicitly make it transparent.
drawFill(BLANK);
}
}
Texture::Texture(video::ITexture *texture)
{
m_texture.grab(texture);
}
Texture::~Texture()
{
// If the reference count is one, only Irrlicht still holds a reference
// to the texture, so we can remove it from the driver.
if (isTexture() && m_texture.get()->getReferenceCount() == 1) {
driver()->removeTexture(m_texture.get());
}
}
d2s32 Texture::getSize() const
{
if (isTexture()) {
return d2s32(m_texture->getOriginalSize());
}
return d2s32(driver()->getScreenSize());
}
void Texture::drawPixel(
const v2s32 &pos,
video::SColor color,
const rs32 *clip)
{
drawRect(rs32(pos, d2s32(0, 0)), color, clip);
}
void Texture::drawRect(
const rs32 &rect,
video::SColor color,
const rs32 *clip)
{
set_as_target(m_texture.get());
driver()->draw2DRectangle(color, rect, clip);
}
void Texture::drawTexture(
const v2s32 &pos,
const Texture &texture,
const rs32 *src,
const rs32 *clip,
video::SColor tint)
{
d2s32 size = (src != nullptr) ? src->getSize() : texture.getSize();
drawTexture(rs32(pos, size), texture, src, clip, tint);
}
void Texture::drawTexture(
const rs32 &rect,
const Texture &texture,
const rs32 *src,
const rs32 *clip,
video::SColor tint)
{
if (!texture.isTexture()) {
errorstream << "Can't draw the screen to a texture" << std::endl;
return;
}
if (m_texture.get() == texture.m_texture.get()) {
errorstream << "Can't draw a texture to itself" << std::endl;
return;
}
set_as_target(m_texture.get());
// If we don't have a source rectangle, make it encompass the entire
// texture.
rs32 texture_rect(texture.getSize());
if (src == nullptr) {
src = &texture_rect;
}
// All the corners should have the same tint.
video::SColor tints[] = {tint, tint, tint, tint};
driver()->draw2DImage(
texture.m_texture.get(), rect, *src, clip, tints, true);
}
void Texture::drawFill(video::SColor color)
{
if (isTexture()) {
/* There's no normal way to fill a texture with a color; drawing a
* rect will add alpha, not replace it. So, use setRenderTarget()
* to clear it instead. Irrlicht will ignore the call if this
* texture is already the current render target, so we set it to
* the screen first before attempting to clear the texture.
*/
set_as_target(nullptr);
set_as_target(m_texture.get(), true, color);
} else {
// The screen can't have transparency, so make the color opaque and
// draw a rectangle across the entire screen.
color.setAlpha(0xFF);
driver()->draw2DRectangle(color, rs32(getSize()));
}
}
Canvas::Canvas(Texture &texture, float scale, const rf32 *clip) :
m_texture(texture),
m_scale(scale)
{
if (clip == nullptr) {
m_clip_ptr = nullptr;
} else {
m_clip = rs32(
clip->UpperLeftCorner.X * m_scale,
clip->UpperLeftCorner.Y * m_scale,
clip->LowerRightCorner.X * m_scale,
clip->LowerRightCorner.Y * m_scale
);
m_clip_ptr = &m_clip;
}
}
void Canvas::drawRect(
const rf32 &rect,
video::SColor color)
{
m_texture.drawRect(getDrawRect(rect), color, m_clip_ptr);
}
void Canvas::drawTexture(
const v2f32 &pos,
const Texture &texture,
const rf32 &src,
video::SColor tint)
{
drawTexture(rf32(pos, d2f32(texture.getSize())), texture, src, tint);
}
void Canvas::drawTexture(
const rf32 &rect,
const Texture &texture,
const rf32 &src,
video::SColor tint)
{
rs32 draw_src(
src.UpperLeftCorner.X * texture.getWidth(),
src.UpperLeftCorner.Y * texture.getHeight(),
src.LowerRightCorner.X * texture.getWidth(),
src.LowerRightCorner.Y * texture.getHeight()
);
m_texture.drawTexture(
getDrawRect(rect), texture, &draw_src, m_clip_ptr, tint);
}
rs32 Canvas::getDrawRect(const rf32 &rect) const
{
return rs32(
rect.UpperLeftCorner.X * m_scale,
rect.UpperLeftCorner.Y * m_scale,
rect.LowerRightCorner.X * m_scale,
rect.LowerRightCorner.Y * m_scale
);
}
}

185
src/gui/texture.h Normal file
View File

@ -0,0 +1,185 @@
/*
Minetest
Copyright (C) 2022 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irr_ptr.h"
#include "irrlichttypes_extrabloated.h"
namespace ui
{
using d2s32 = core::dimension2di;
using d2u32 = core::dimension2du;
using d2f32 = core::dimension2df;
using rs32 = core::recti;
using rf32 = core::rectf;
const video::SColor BLANK = 0x00000000;
const video::SColor BLACK = 0xFF000000;
const video::SColor WHITE = 0xFFFFFFFF;
template<typename T>
core::vector2d<T> clamp_vec(core::vector2d<T> vec)
{
if (vec.X < 0)
vec.X = 0;
if (vec.Y < 0)
vec.Y = 0;
return vec;
}
template<typename T>
core::rect<T> clamp_rect(core::rect<T> rect)
{
if (rect.getWidth() < 0)
rect.LowerRightCorner.X = rect.UpperLeftCorner.X;
if (rect.getHeight() < 0)
rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y;
return rect;
}
template<typename T>
core::rect<T> clip_rect(core::rect<T> first, const core::rect<T> &second)
{
first.clipAgainst(second);
return first;
}
template<typename T>
T &dim_at(core::dimension2d<T> &dim, size_t index)
{
return index == 0 ? dim.Width : dim.Height;
}
template<typename T>
const T &dim_at(const core::dimension2d<T> &dim, size_t index)
{
return index == 0 ? dim.Width : dim.Height;
}
class Texture
{
private:
irr_ptr<video::ITexture> m_texture = nullptr;
public:
// These functions must surround any drawing calls to Texture instances
// to ensure proper tracking of render targets. Raw Irrlicht draw calls
// should be used with caution to avoid messing up render targets.
static void begin();
static void end();
static Texture screen;
Texture() = default;
Texture(video::ITexture *texture);
Texture(const d2s32 &size);
~Texture();
d2s32 getSize() const;
s32 getWidth() const { return getSize().Width; }
s32 getHeight() const { return getSize().Height; }
bool isTexture() const
{
return m_texture.get() != nullptr;
}
void drawPixel(
const v2s32 &pos,
video::SColor color,
const rs32 *clip = nullptr);
void drawRect(
const rs32 &rect,
video::SColor color,
const rs32 *clip = nullptr);
void drawTexture(
const v2s32 &pos,
const Texture &texture,
const rs32 *src = nullptr,
const rs32 *clip = nullptr,
video::SColor tint = WHITE);
void drawTexture(
const rs32 &rect,
const Texture &texture,
const rs32 *src = nullptr,
const rs32 *clip = nullptr,
video::SColor tint = WHITE);
void drawFill(video::SColor color);
friend bool operator==(const Texture &left, const Texture &right)
{
return left.m_texture == right.m_texture;
}
friend bool operator!=(const Texture &left, const Texture &right)
{
return !(left == right);
}
};
class Canvas
{
private:
Texture &m_texture;
float m_scale;
rs32 m_clip;
rs32 *m_clip_ptr;
public:
Canvas(Texture &texture, float scale, const rf32 *clip = nullptr);
Canvas(Canvas &canvas, const rf32 *clip = nullptr) :
Canvas(canvas.getTexture(), canvas.getScale(), clip)
{}
Texture &getTexture() { return m_texture; }
const Texture &getTexture() const { return m_texture; }
float getScale() const { return m_scale; }
void drawRect(
const rf32 &rect,
video::SColor color);
void drawTexture(
const v2f32 &pos,
const Texture &texture,
const rf32 &src = rf32(0.0f, 0.0f, 1.0f, 1.0f),
video::SColor tint = WHITE);
void drawTexture(
const rf32 &rect,
const Texture &texture,
const rf32 &src = rf32(0.0f, 0.0f, 1.0f, 1.0f),
video::SColor tint = WHITE);
private:
rs32 getDrawRect(const rf32 &rect) const;
};
}

284
src/gui/window.cpp Normal file
View File

@ -0,0 +1,284 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "gui/window.h"
#include "debug.h"
#include "log.h"
#include "settings.h"
#include "client/client.h"
#include "client/renderingengine.h"
#include "client/tile.h"
#include "gui/manager.h"
#include "gui/texture.h"
#include "util/serialize.h"
namespace ui
{
WindowType toWindowType(u8 type)
{
if (type >= (u8)WindowType::MAX_TYPE) {
return WindowType::HUD;
}
return (WindowType)type;
}
Elem *Window::getElem(const std::string &id, bool required)
{
// Empty IDs may be valid values if the element is optional.
if (id.empty() && !required) {
return nullptr;
}
// If the ID is not empty, then we need to search for an actual
// element. Not finding one means that an error occurred.
auto it = m_elems.find(id);
if (it != m_elems.end()) {
return it->second.get();
}
errorstream << "Element \"" << id << "\" does not exist" << std::endl;
return nullptr;
}
const std::string *Window::getStyleStr(u32 index) const
{
if (index < m_style_strs.size()) {
return &m_style_strs[index];
}
return nullptr;
}
void Window::reset()
{
m_elems.clear();
m_ordered_elems.clear();
m_root_elem = nullptr;
m_style_strs.clear();
}
void Window::read(std::istream &is, bool opening)
{
std::unordered_map<Elem *, std::string> elem_contents;
readElems(is, elem_contents);
readRootElem(is);
readStyles(is);
if (opening)
m_type = toWindowType(readU8(is));
// Assuming no earlier step failed, we can proceed to read in all the
// properties. Otherwise, reset the window entirely.
if (m_root_elem != nullptr) {
updateElems(elem_contents);
} else {
reset();
}
}
float Window::getPixelSize() const
{
return g_manager.getPixelSize(m_type);
}
d2f32 Window::getScreenSize() const
{
return g_manager.getScreenSize(m_type);
}
void Window::drawAll()
{
if (m_root_elem == nullptr) {
return;
}
rf32 parent_rect(getScreenSize());
m_root_elem->layout(parent_rect, parent_rect);
Canvas canvas(Texture::screen, getPixelSize());
m_root_elem->drawAll(canvas);
}
void Window::readElems(std::istream &is,
std::unordered_map<Elem *, std::string> &elem_contents)
{
// Read in all the new elements and updates to existing elements.
u32 num_elems = readU32(is);
std::unordered_map<std::string, std::unique_ptr<Elem>> new_elems;
for (size_t i = 0; i < num_elems; i++) {
u32 type = readU8(is);
std::string id = readNullStr(is);
// Make sure that elements have valid IDs. If the string has non-ID
// characters in it, though, we don't particularly care.
if (id.empty()) {
errorstream << "Element has empty ID" << std::endl;
continue;
}
// Each element has a size prefix stating how big the element is.
// This allows new fields to be added to elements without breaking
// compatibility. So, read it in as a string and save it for later.
std::string contents = readStr32(is);
// If this is a duplicate element, skip it right away.
if (new_elems.find(id) != new_elems.end()) {
errorstream << "Duplicate element \"" << id << "\"" << std::endl;
continue;
}
/* Now we need to decide whether to create a new element or to
* modify the state of an already existing one. This allows
* changing attributes of an element (like the style or the
* element's children) while leaving leaving persistent state
* intact (such as the position of a scrollbar or the contents of a
* text field).
*/
std::unique_ptr<Elem> elem = nullptr;
// Search for a pre-existing element.
auto it = m_elems.find(id);
if (it == m_elems.end() || it->second->getType() != type) {
// If the element was not found or the existing element has the
// wrong type, create a new element.
elem = Elem::create((Elem::Type)type, *this, id);
// If we couldn't create the element, the type was invalid.
// Skip this element entirely.
if (elem == nullptr) {
errorstream << "Element \"" << id << "\" has an invalid type: " <<
type << std::endl;
continue;
}
} else {
// Otherwise, use the existing element.
elem = std::move(it->second);
}
// Now that we've gotten our element, reset its contents.
elem->reset();
// We need to read in all elements before updating each element, so
// save the element's contents for later.
elem_contents[elem.get()] = contents;
new_elems.emplace(id, std::move(elem));
}
// Set these elements as our list of new elements.
m_elems = std::move(new_elems);
// Clear the ordered elements for now. They will be regenerated later.
m_ordered_elems.clear();
}
void Window::readRootElem(std::istream &is)
{
// Get the root element of the window and make sure it's valid.
m_root_elem = getElem(readNullStr(is), true);
if (m_root_elem == nullptr) {
errorstream << "Window " << m_id << " has no root element" << std::endl;
reset();
} else if (m_root_elem->getType() != Elem::ROOT) {
errorstream << "Window " << m_id <<
" has wrong type for root element" << std::endl;
reset();
}
}
void Window::readStyles(std::istream &is)
{
// Styles are stored in their raw binary form; every time a style needs
// to be recalculated, these binary strings can be applied one over the
// other, resulting in automatic cascading styles.
u32 num_styles = readU32(is);
m_style_strs.clear();
for (size_t i = 0; i < num_styles; i++) {
m_style_strs.push_back(readStr16(is));
}
}
void Window::updateElems(std::unordered_map<Elem *, std::string> &elem_contents)
{
// Now that we have a fully updated window, we can update each element
// with its contents. We couldn't do this before because elements need
// to be able to call getElem() and getStyleStr().
for (auto &contents : elem_contents) {
auto is = newIs(std::move(contents.second));
contents.first->read(is);
}
// Check the depth of the element tree; if it's too deep, there's
// potential for stack overflow.
if (!checkTree(m_root_elem, 1)) {
reset();
return;
}
// Update the ordering of the elements so we can do iteration rather
// than recursion when searching through the elements in order.
updateElemOrdering(m_root_elem, 0);
}
bool Window::checkTree(Elem *elem, size_t depth) const
{
if (depth > MAX_TREE_DEPTH) {
errorstream << "Window " << m_id <<
" exceeds max tree depth: " << MAX_TREE_DEPTH << std::endl;
return false;
}
for (Elem *child : elem->getChildren()) {
if (child->getType() == Elem::ROOT) {
errorstream << "Element of root type \"" << child->getId() <<
"\" is not root of window" << std::endl;
return false;
}
if (!checkTree(child, depth + 1)) {
return false;
}
}
return true;
}
size_t Window::updateElemOrdering(Elem *elem, size_t order)
{
// The parent gets ordered before its children since the ordering of
// elements follows draw order.
elem->setOrder(order);
m_ordered_elems.push_back(elem);
for (Elem *child : elem->getChildren()) {
// Order this element's children using the next index after the
// parent, returning the index of the last child element.
order = updateElemOrdering(child, order + 1);
}
return order;
}
}

102
src/gui/window.h Normal file
View File

@ -0,0 +1,102 @@
/*
Minetest
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irrlichttypes_extrabloated.h"
#include "gui/elem.h"
#include "util/basic_macros.h"
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace ui
{
// Serialized enum; do not change order of entries.
enum class WindowType
{
BG,
MASK,
HUD,
MESSAGE,
GUI,
FG,
MAX_TYPE,
};
WindowType toWindowType(u8 type);
class Window
{
private:
static constexpr size_t MAX_TREE_DEPTH = 64;
u64 m_id;
WindowType m_type = WindowType::GUI;
std::unordered_map<std::string, std::unique_ptr<Elem>> m_elems;
std::vector<Elem *> m_ordered_elems;
Elem *m_root_elem;
std::vector<std::string> m_style_strs;
public:
Window(u64 id) :
m_id(id)
{
reset();
}
DISABLE_CLASS_COPY(Window)
ALLOW_CLASS_MOVE(Window)
u64 getId() const { return m_id; }
WindowType getType() const { return m_type; }
const std::vector<Elem *> &getElems() { return m_ordered_elems; }
Elem *getElem(const std::string &id, bool required);
Elem *getRoot() { return m_root_elem; }
const std::string *getStyleStr(u32 index) const;
void reset();
void read(std::istream &is, bool opening);
float getPixelSize() const;
d2f32 getScreenSize() const;
void drawAll();
private:
void readElems(std::istream &is,
std::unordered_map<Elem *, std::string> &elem_contents);
void readRootElem(std::istream &is);
void readStyles(std::istream &is);
void updateElems(std::unordered_map<Elem *, std::string> &elem_contents);
bool checkTree(Elem *elem, size_t depth) const;
size_t updateElemOrdering(Elem *elem, size_t order);
};
}

View File

@ -120,7 +120,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] =
{ "TOCLIENT_SET_MOON", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetMoon }, // 0x5b
{ "TOCLIENT_SET_STARS", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_HudSetStars }, // 0x5c
{ "TOCLIENT_MOVE_PLAYER_REL", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MovePlayerRel }, // 0x5d,
null_command_handler,
{ "TOCLIENT_UI_MESSAGE", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_UiMessage }, // 0x5e,
null_command_handler,
{ "TOCLIENT_SRP_BYTES_S_B", TOCLIENT_STATE_NOT_CONNECTED, &Client::handleCommand_SrpBytesSandB }, // 0x60
{ "TOCLIENT_FORMSPEC_PREPEND", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_FormspecPrepend }, // 0x61,

View File

@ -999,6 +999,17 @@ void Client::handleCommand_ShowFormSpec(NetworkPacket* pkt)
m_client_event_queue.push(event);
}
void Client::handleCommand_UiMessage(NetworkPacket* pkt)
{
std::string *data = new std::string(pkt->getString(0), pkt->getSize());
ClientEvent *event = new ClientEvent();
event->type = CE_UI_MESSAGE;
event->ui_message.data = data;
m_client_event_queue.push(event);
}
void Client::handleCommand_SpawnParticle(NetworkPacket* pkt)
{
std::string datastring(pkt->getString(0), pkt->getSize());

View File

@ -223,6 +223,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
AO_CMD_SET_BONE_POSITION extended
Add TOCLIENT_MOVE_PLAYER_REL
Move default minimap from client-side C++ to server-side builtin Lua
Add TOCLIENT_UI_MESSAGE
[scheduled bump for 5.9.0]
*/
@ -811,6 +812,12 @@ enum ToClientCommand : u16
v3f added_pos
*/
TOCLIENT_UI_MESSAGE = 0x5e,
/*
Complicated variable-length structure with many optional fields and
length-prefixed data for future compatibility.
*/
TOCLIENT_SRP_BYTES_S_B = 0x60,
/*
Belonging to AUTH_MECHANISM_SRP.

View File

@ -220,7 +220,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] =
{ "TOCLIENT_SET_MOON", 0, true }, // 0x5b
{ "TOCLIENT_SET_STARS", 0, true }, // 0x5c
{ "TOCLIENT_MOVE_PLAYER_REL", 0, true }, // 0x5d
null_command_factory, // 0x5e
{ "TOCLIENT_UI_MESSAGE", 0, true }, // 0x5e
null_command_factory, // 0x5f
{ "TOCLIENT_SRP_BYTES_S_B", 0, true }, // 0x60
{ "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61

View File

@ -437,6 +437,19 @@ int ModApiServer::l_show_formspec(lua_State *L)
return 1;
}
// send_ui_message(player, data)
int ModApiServer::l_send_ui_message(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
size_t len;
const char *player = luaL_checkstring(L, 1);
const char *data = luaL_checklstring(L, 2, &len);
getServer(L)->sendUiMessage(player, data, len);
return 0;
}
// get_current_modname()
int ModApiServer::l_get_current_modname(lua_State *L)
{
@ -749,6 +762,7 @@ void ModApiServer::Initialize(lua_State *L, int top)
API_FCT(chat_send_all);
API_FCT(chat_send_player);
API_FCT(show_formspec);
API_FCT(send_ui_message);
API_FCT(sound_play);
API_FCT(sound_stop);
API_FCT(sound_fade);

View File

@ -70,6 +70,9 @@ private:
// show_formspec(playername,formname,formspec)
static int l_show_formspec(lua_State *L);
// send_ui_message(player, data)
static int l_send_ui_message(lua_State *L);
// sound_play(spec, parameters)
static int l_sound_play(lua_State *L);

View File

@ -605,6 +605,20 @@ int ModApiUtil::l_colorspec_to_colorstring(lua_State *L)
return 0;
}
// colorspec_to_colorint(colorspec)
int ModApiUtil::l_colorspec_to_colorint(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
video::SColor color(0);
if (read_color(L, 1, &color)) {
lua_pushnumber(L, color.color);
return 1;
}
return 0;
}
// colorspec_to_bytes(colorspec)
int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
{
@ -625,6 +639,183 @@ int ModApiUtil::l_colorspec_to_bytes(lua_State *L)
return 0;
}
// encode_network(format, ...)
int ModApiUtil::l_encode_network(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
std::string format = readParam<std::string>(L, 1);
std::ostringstream os(std::ios_base::binary);
int arg = 2;
for (size_t i = 0; i < format.size(); i++) {
switch (format[i]) {
case 'b':
// Casting the double to a signed integer larger than the target
// integer results in proper integer wraparound behavior.
writeS8(os, (s64)luaL_checknumber(L, arg));
break;
case 'h':
writeS16(os, (s64)luaL_checknumber(L, arg));
break;
case 'i':
writeS32(os, (s64)luaL_checknumber(L, arg));
break;
case 'l':
writeS64(os, (s64)luaL_checknumber(L, arg));
break;
case 'B':
// Casting to an unsigned integer doesn't result in the proper
// integer conversions being applied, so we still use signed.
writeU8(os, (s64)luaL_checknumber(L, arg));
break;
case 'H':
writeU16(os, (s64)luaL_checknumber(L, arg));
break;
case 'I':
writeU32(os, (s64)luaL_checknumber(L, arg));
break;
case 'L':
// For the 64-bit integers, we can never experience integer
// overflow due to the limited range of Lua's doubles, but we can
// have underflow, hence why we cast to s64 first.
writeU64(os, (s64)luaL_checknumber(L, arg));
break;
case 'f':
writeF32(os, luaL_checknumber(L, arg));
break;
case 's': {
std::string str = readParam<std::string>(L, arg);
os << serializeString16(str, true);
break;
}
case 'S': {
std::string str = readParam<std::string>(L, arg);
os << serializeString32(str, true);
break;
}
case 'z': {
std::string str = readParam<std::string>(L, arg);
os << str.substr(0, strlen(str.c_str())) << '\0';
break;
}
case 'Z':
os << readParam<std::string>(L, arg);
break;
case ' ':
// Continue because we don't want to increment arg.
continue;
default:
throw LuaError("Invalid format string");
}
arg++;
}
std::string data = os.str();
lua_pushlstring(L, data.c_str(), data.size());
return 1;
}
// decode_network(format, data)
int ModApiUtil::l_decode_network(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
std::string format = readParam<std::string>(L, 1);
std::string data = readParam<std::string>(L, 2);
std::istringstream is(data, std::ios_base::binary);
// Make sure we have space for all our returned arguments.
lua_checkstack(L, format.size());
// Set up tracking for verbatim strings and the number of return values.
int num_args = lua_gettop(L);
int arg = 3;
int ret = 0;
for (size_t i = 0; i < format.size(); i++) {
switch (format[i]) {
case 'b':
lua_pushnumber(L, readS8(is));
break;
case 'h':
lua_pushnumber(L, readS16(is));
break;
case 'i':
lua_pushnumber(L, readS32(is));
break;
case 'l':
lua_pushnumber(L, readS64(is));
break;
case 'B':
lua_pushnumber(L, readU8(is));
break;
case 'H':
lua_pushnumber(L, readU16(is));
break;
case 'I':
lua_pushnumber(L, readU32(is));
break;
case 'L':
lua_pushnumber(L, readU64(is));
break;
case 'f':
lua_pushnumber(L, readF32(is));
break;
case 's': {
std::string str = deSerializeString16(is, true);
lua_pushlstring(L, str.c_str(), str.size());
break;
}
case 'S': {
std::string str = deSerializeString32(is, true);
lua_pushlstring(L, str.c_str(), str.size());
break;
}
case 'z': {
std::string str;
std::getline(is, str, '\0');
lua_pushlstring(L, str.c_str(), str.size());
break;
}
case 'Z': {
if (arg > num_args) {
throw LuaError("Missing verbatim string size");
}
double size = luaL_checknumber(L, arg);
std::string str;
if (size < 0) {
// Read the entire rest of the input stream.
std::ostringstream os(std::ios_base::binary);
os << is.rdbuf();
str = os.str();
} else if (size != 0) {
// Read the specified number of characters.
str.resize(size);
is.read(&str[0], size);
}
lua_pushlstring(L, str.c_str(), str.size());
arg++;
break;
}
case ' ':
// Continue because we don't want to increment ret.
continue;
default:
throw LuaError("Invalid format string");
}
ret++;
}
return ret;
}
// encode_png(w, h, data, level)
int ModApiUtil::l_encode_png(lua_State *L)
{
@ -714,8 +905,12 @@ void ModApiUtil::Initialize(lua_State *L, int top)
API_FCT(sha1);
API_FCT(sha256);
API_FCT(colorspec_to_colorstring);
API_FCT(colorspec_to_colorint);
API_FCT(colorspec_to_bytes);
API_FCT(encode_network);
API_FCT(decode_network);
API_FCT(encode_png);
API_FCT(get_last_run_mod);
@ -748,11 +943,15 @@ void ModApiUtil::InitializeClient(lua_State *L, int top)
API_FCT(sha1);
API_FCT(sha256);
API_FCT(colorspec_to_colorstring);
API_FCT(colorspec_to_colorint);
API_FCT(colorspec_to_bytes);
API_FCT(get_last_run_mod);
API_FCT(set_last_run_mod);
API_FCT(encode_network);
API_FCT(decode_network);
API_FCT(urlencode);
LuaSettings::create(L, g_settings, g_settings_path);
@ -792,8 +991,12 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top)
API_FCT(sha1);
API_FCT(sha256);
API_FCT(colorspec_to_colorstring);
API_FCT(colorspec_to_colorint);
API_FCT(colorspec_to_bytes);
API_FCT(encode_network);
API_FCT(decode_network);
API_FCT(encode_png);
API_FCT(get_last_run_mod);

View File

@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#include "lua_api/l_base.h"
#include "util/serialize.h"
class AsyncEngine;
@ -119,9 +120,18 @@ private:
// colorspec_to_colorstring(colorspec)
static int l_colorspec_to_colorstring(lua_State *L);
// colorspec_to_colorint(colorspec)
static int l_colorspec_to_colorint(lua_State *L);
// colorspec_to_bytes(colorspec)
static int l_colorspec_to_bytes(lua_State *L);
// encode_network(format, ...)
static int l_encode_network(lua_State *L);
// decode_network(format, data)
static int l_decode_network(lua_State *L);
// encode_png(w, h, data, level)
static int l_encode_png(lua_State *L);

View File

@ -3364,6 +3364,19 @@ bool Server::showFormspec(const char *playername, const std::string &formspec,
return true;
}
void Server::sendUiMessage(const char *name, const char *data, size_t len)
{
RemotePlayer *player = m_env->getPlayer(name);
if (!player) {
return;
}
NetworkPacket pkt(TOCLIENT_UI_MESSAGE, 0, player->getPeerId());
pkt.putRawString(data, len);
Send(&pkt);
}
u32 Server::hudAdd(RemotePlayer *player, HudElement *form)
{
if (!player)

View File

@ -330,6 +330,8 @@ public:
void addShutdownError(const ModError &e);
bool showFormspec(const char *name, const std::string &formspec, const std::string &formname);
void sendUiMessage(const char *name, const char *data, size_t len);
Map & getMap() { return m_env->getMap(); }
ServerEnvironment & getEnv() { return *m_env; }
v3f findSpawnPos();

View File

@ -34,39 +34,54 @@ FloatType g_serialize_f32_type = FLOATTYPE_UNKNOWN;
//// String
////
std::string serializeString16(std::string_view plain)
std::string serializeString16(std::string_view plain, bool truncate)
{
std::string s;
char buf[2];
size_t size = plain.size();
if (plain.size() > STRING_MAX_LEN)
throw SerializationError("String too long for serializeString16");
s.reserve(2 + plain.size());
if (size > STRING_MAX_LEN) {
if (truncate) {
size = STRING_MAX_LEN;
} else {
throw SerializationError("String too long for serializeString16");
}
}
writeU16((u8 *)&buf[0], plain.size());
s.append(buf, 2);
char size_buf[2];
writeU16((u8 *)size_buf, size);
s.reserve(2 + size);
s.append(size_buf, 2);
s.append(plain.substr(0, size));
s.append(plain);
return s;
}
std::string deSerializeString16(std::istream &is)
std::string deSerializeString16(std::istream &is, bool truncate)
{
std::string s;
char buf[2];
char size_buf[2];
is.read(buf, 2);
if (is.gcount() != 2)
is.read(size_buf, 2);
if (is.gcount() != 2) {
if (truncate) {
return s;
}
throw SerializationError("deSerializeString16: size not read");
}
u16 s_size = readU16((u8 *)buf);
if (s_size == 0)
u16 size = readU16((u8 *)size_buf);
if (size == 0) {
return s;
}
s.resize(s_size);
is.read(&s[0], s_size);
if (is.gcount() != s_size)
s.resize(size);
is.read(&s[0], size);
if (truncate) {
s.resize(is.gcount());
} else if (is.gcount() != size) {
throw SerializationError("deSerializeString16: couldn't read all chars");
}
return s;
}
@ -76,44 +91,72 @@ std::string deSerializeString16(std::istream &is)
//// Long String
////
std::string serializeString32(std::string_view plain)
std::string serializeString32(std::string_view plain, bool truncate)
{
std::string s;
char buf[4];
size_t size = plain.size();
if (plain.size() > LONG_STRING_MAX_LEN)
throw SerializationError("String too long for serializeLongString");
s.reserve(4 + plain.size());
if (size > LONG_STRING_MAX_LEN) {
if (truncate) {
size = LONG_STRING_MAX_LEN;
} else {
throw SerializationError("String too long for serializeString32");
}
}
char size_buf[4];
writeU32((u8 *)size_buf, size);
s.reserve(4 + size);
s.append(size_buf, 4);
s.append(plain.substr(0, size));
writeU32((u8*)&buf[0], plain.size());
s.append(buf, 4);
s.append(plain);
return s;
}
std::string deSerializeString32(std::istream &is)
std::string deSerializeString32(std::istream &is, bool truncate)
{
std::string s;
char buf[4];
char size_buf[4];
is.read(buf, 4);
if (is.gcount() != 4)
throw SerializationError("deSerializeLongString: size not read");
u32 s_size = readU32((u8 *)buf);
if (s_size == 0)
return s;
// We don't really want a remote attacker to force us to allocate 4GB...
if (s_size > LONG_STRING_MAX_LEN) {
throw SerializationError("deSerializeLongString: "
"string too long: " + itos(s_size) + " bytes");
is.read(size_buf, 4);
if (is.gcount() != 4) {
if (truncate) {
return s;
}
throw SerializationError("deSerializeString32: size not read");
}
s.resize(s_size);
is.read(&s[0], s_size);
if ((u32)is.gcount() != s_size)
throw SerializationError("deSerializeLongString: couldn't read all chars");
u32 size = readU32((u8 *)size_buf);
u32 ignore = 0;
if (size == 0) {
return s;
}
if (size > LONG_STRING_MAX_LEN) {
if (truncate) {
ignore = size - LONG_STRING_MAX_LEN;
size = LONG_STRING_MAX_LEN;
} else {
// We don't really want a remote attacker to force us to allocate 4GB...
throw SerializationError("deSerializeString32: "
"string too long: " + itos(size) + " bytes");
}
}
s.resize(size);
is.read(&s[0], size);
if (truncate) {
s.resize(is.gcount());
} else if (is.gcount() != size) {
throw SerializationError("deSerializeString32: couldn't read all chars");
}
// If the string was truncated due to exceeding the string max length, we
// need to ignore the rest of the characters.
if (truncate) {
is.seekg(ignore, std::ios_base::cur);
}
return s;
}

View File

@ -461,16 +461,16 @@ inline v3f clampToF1000(v3f v)
}
// Creates a string with the length as the first two bytes
std::string serializeString16(std::string_view plain);
std::string serializeString16(std::string_view plain, bool truncate = false);
// Reads a string with the length as the first two bytes
std::string deSerializeString16(std::istream &is);
std::string deSerializeString16(std::istream &is, bool truncate = false);
// Creates a string with the length as the first four bytes
std::string serializeString32(std::string_view plain);
std::string serializeString32(std::string_view plain, bool truncate = false);
// Reads a string with the length as the first four bytes
std::string deSerializeString32(std::istream &is);
std::string deSerializeString32(std::istream &is, bool truncate = false);
// Creates a string encoded in JSON format (almost equivalent to a C string literal)
std::string serializeJsonString(std::string_view plain);