mirror of
https://github.com/luanti-org/luanti.git
synced 2025-09-15 18:57:08 +00:00
Replace hotbar_itemcount by hotbar_source
This commit is contained in:
parent
4b85062caf
commit
29d1dcd23b
23 changed files with 682 additions and 189 deletions
|
@ -1840,6 +1840,10 @@ Displays a horizontal bar made up of half-images with an optional background.
|
||||||
* `direction`: Direction the list will be displayed in
|
* `direction`: Direction the list will be displayed in
|
||||||
* `offset`: offset in pixels from position.
|
* `offset`: offset in pixels from position.
|
||||||
* `alignment`: The alignment of the inventory.
|
* `alignment`: The alignment of the inventory.
|
||||||
|
* `world_pos`: Inventory of the hotbar depending on `hotbar_source`
|
||||||
|
`x` the index of the `hotbar_source` table to be used, if 0 use all next to each other
|
||||||
|
`y` an additional length to clamp the size, if 0 use whole length as defined in `hotbar_source`
|
||||||
|
`z` an additional offset to adjust where the displayed inventory starts
|
||||||
|
|
||||||
### `waypoint`
|
### `waypoint`
|
||||||
|
|
||||||
|
@ -8270,7 +8274,7 @@ child will follow movement and rotation of that bone.
|
||||||
* `get_inventory()`: returns an `InvRef` for players, otherwise returns `nil`
|
* `get_inventory()`: returns an `InvRef` for players, otherwise returns `nil`
|
||||||
* `get_wield_list()`: returns the name of the inventory list the wielded item
|
* `get_wield_list()`: returns the name of the inventory list the wielded item
|
||||||
is in.
|
is in.
|
||||||
* `get_wield_index()`: returns the wield list index of the wielded item (starting with 1)
|
* `get_wield_index()`: returns the index of the wielded item in the wield list
|
||||||
* `get_wielded_item()`: returns a copy of the wielded item as an `ItemStack`
|
* `get_wielded_item()`: returns a copy of the wielded item as an `ItemStack`
|
||||||
* `set_wielded_item(item)`: replaces the wielded item, returns `true` if
|
* `set_wielded_item(item)`: replaces the wielded item, returns `true` if
|
||||||
successful.
|
successful.
|
||||||
|
@ -8632,8 +8636,16 @@ child will follow movement and rotation of that bone.
|
||||||
* `hud_set_hotbar_itemcount(count)`: sets number of items in builtin hotbar
|
* `hud_set_hotbar_itemcount(count)`: sets number of items in builtin hotbar
|
||||||
* `count`: number of items, must be between `1` and `32`
|
* `count`: number of items, must be between `1` and `32`
|
||||||
* If `count` exceeds the `"main"` list size, the list size will be used instead.
|
* If `count` exceeds the `"main"` list size, the list size will be used instead.
|
||||||
* `hud_get_hotbar_itemcount()`: returns number of visible items
|
* equal `set_hotbar_source({{list = "main", length = count}})`
|
||||||
* This value is also clamped by the `"main"` list size.
|
* `hud_get_hotbar_itemcount()`: returns number of selectable items
|
||||||
|
* `get_hotbar_source()` returns used `hotbar_source`
|
||||||
|
* `set_hotbar_source({{list = "main", length = 6, offset = 24}, {list = "bag1", length = 4}, ...})`
|
||||||
|
* Sets inventory lists for the player to use in hotbar(s) and to select the wield item from.
|
||||||
|
`list` is a player inventory list
|
||||||
|
`length` is the amount of inventory slots
|
||||||
|
`offset` adjusts starting inventory position, 0 if not specified
|
||||||
|
* Note: Do not use this together with mods that relay on a fixed wield list and list size.
|
||||||
|
All mods should use `get_wield_list()` and `get_wield_index()` to get the wield position.
|
||||||
* `hud_set_hotbar_image(texturename)`
|
* `hud_set_hotbar_image(texturename)`
|
||||||
* sets background image for hotbar
|
* sets background image for hotbar
|
||||||
* `hud_get_hotbar_image()`: returns texturename
|
* `hud_get_hotbar_image()`: returns texturename
|
||||||
|
|
|
@ -211,16 +211,17 @@ core.register_chatcommand("zoomfov", {
|
||||||
-- Hotbars
|
-- Hotbars
|
||||||
|
|
||||||
local hud_hotbar_defs = {
|
local hud_hotbar_defs = {
|
||||||
|
{
|
||||||
{
|
{
|
||||||
type = "hotbar",
|
type = "hotbar",
|
||||||
position = {x=0.2, y=0.5},
|
position = {x=0.2, y=0.5},
|
||||||
direction = 0,
|
direction = 1,
|
||||||
alignment = {x=1, y=-1},
|
alignment = {x=1, y=-1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type = "hotbar",
|
type = "hotbar",
|
||||||
position = {x=0.2, y=0.5},
|
position = {x=0.2, y=0.5},
|
||||||
direction = 2,
|
direction = 3,
|
||||||
alignment = {x=1, y=1},
|
alignment = {x=1, y=1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -237,15 +238,118 @@ local hud_hotbar_defs = {
|
||||||
offset = {x=140, y=20},
|
offset = {x=140, y=20},
|
||||||
alignment = {x=-1, y=1},
|
alignment = {x=-1, y=1},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hotbar_source = {
|
||||||
|
{list = "craft", length = 4, offset = 3},
|
||||||
|
{list = "main", length = 5, offset = 0},
|
||||||
|
},
|
||||||
|
hotbar_image = "default_stone.png^[opacity:150",
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.2},
|
||||||
|
world_pos = {x=1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.3},
|
||||||
|
world_pos = {x=1, y=6},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.4},
|
||||||
|
world_pos = {x=1, z=5, y=3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.5},
|
||||||
|
world_pos = {x=2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.6},
|
||||||
|
world_pos = {x=2, z=1, y=3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.7},
|
||||||
|
world_pos = {x=2},
|
||||||
|
direction = 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.8},
|
||||||
|
world_pos = {x=2, z=1, y=3},
|
||||||
|
direction = 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.2, y=0.5},
|
||||||
|
world_pos = {x=2, z=1, y=3},
|
||||||
|
direction = 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.8, y=0.5},
|
||||||
|
world_pos = {x=2, z=1, y=3},
|
||||||
|
direction = 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hotbar_source = {
|
||||||
|
{list = "craft", length = 4, offset = 3},
|
||||||
|
{list = "main", length = 5, offset = 0},
|
||||||
|
{list = "main", length = 2, offset = 4},
|
||||||
|
{list = "craft", length = 4, offset = 3},
|
||||||
|
},
|
||||||
|
hotbar_image = "default_stone.png^[opacity:150",
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.8},
|
||||||
|
direction = 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.7},
|
||||||
|
world_pos = {z=1, y=10},
|
||||||
|
direction = 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.6},
|
||||||
|
world_pos = {z=1, y=10},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.5},
|
||||||
|
world_pos = {z=17, y=5},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.5, y=0.4},
|
||||||
|
world_pos = {z=10, y=10},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.2, y=0.5},
|
||||||
|
direction = 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type = "hotbar",
|
||||||
|
position = {x=0.8, y=0.5},
|
||||||
|
direction = 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
local player_hud_hotbars= {}
|
local player_hud_hotbars= {}
|
||||||
core.register_chatcommand("hudhotbars", {
|
core.register_chatcommand("hudhotbars", {
|
||||||
description = "Shows some test Lua HUD elements of type hotbar. " ..
|
description = "Shows some test Lua HUD elements of type hotbar. " ..
|
||||||
"(add: Adds elements (default). remove: Removes elements)",
|
"(Cycles between: none, aligned using all direction and offset," ..
|
||||||
params = "[ add | remove ]",
|
"changed hotbar_source each only using a single inventory," ..
|
||||||
func = function(name, params)
|
"using multiple inventories (world_pos.x = 0))",
|
||||||
|
func = function(name)
|
||||||
local player = core.get_player_by_name(name)
|
local player = core.get_player_by_name(name)
|
||||||
if not player then
|
if not player then
|
||||||
return false, "No player."
|
return false, "No player."
|
||||||
|
@ -253,22 +357,37 @@ core.register_chatcommand("hudhotbars", {
|
||||||
|
|
||||||
local id_table = player_hud_hotbars[name]
|
local id_table = player_hud_hotbars[name]
|
||||||
if not id_table then
|
if not id_table then
|
||||||
id_table = {}
|
id_table = {mode = 0}
|
||||||
player_hud_hotbars[name] = id_table
|
player_hud_hotbars[name] = id_table
|
||||||
end
|
end
|
||||||
|
|
||||||
if params == "remove" then
|
id_table.mode = (id_table.mode + 1) % (#hud_hotbar_defs + 1)
|
||||||
|
|
||||||
|
-- Reset
|
||||||
for _, id in ipairs(id_table) do
|
for _, id in ipairs(id_table) do
|
||||||
player:hud_remove(id)
|
player:hud_remove(id)
|
||||||
end
|
end
|
||||||
|
player:hud_set_hotbar_itemcount(8)
|
||||||
|
player:hud_set_hotbar_image("")
|
||||||
|
|
||||||
|
if id_table.mode == 0 then
|
||||||
return true, "Hotbars removed."
|
return true, "Hotbars removed."
|
||||||
end
|
end
|
||||||
|
|
||||||
-- params == "add" or default
|
for _, def in ipairs(hud_hotbar_defs[id_table.mode]) do
|
||||||
for _, def in ipairs(hud_hotbar_defs) do
|
|
||||||
table.insert(id_table, player:hud_add(def))
|
table.insert(id_table, player:hud_add(def))
|
||||||
end
|
end
|
||||||
return true, #hud_hotbar_defs .." Hotbars added."
|
|
||||||
|
if hud_hotbar_defs[id_table.mode].hotbar_source then
|
||||||
|
player:set_hotbar_source(hud_hotbar_defs[id_table.mode].hotbar_source)
|
||||||
|
end
|
||||||
|
|
||||||
|
if hud_hotbar_defs[id_table.mode].hotbar_image then
|
||||||
|
player:hud_set_hotbar_image(hud_hotbar_defs[id_table.mode].hotbar_image)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return true, #hud_hotbar_defs[id_table.mode] .." Hotbars added."
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -183,9 +183,32 @@ end
|
||||||
unittests.register("test_player_add_pos", run_player_add_pos_tests, {player=true})
|
unittests.register("test_player_add_pos", run_player_add_pos_tests, {player=true})
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Hotbar selection clamp
|
-- Hotbar Source
|
||||||
--
|
--
|
||||||
local function run_player_hotbar_clamp_tests(player)
|
|
||||||
|
local function table_equals(t1, t2)
|
||||||
|
local keys = {}
|
||||||
|
local checked = {}
|
||||||
|
for k, v in pairs(t1) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
if not (checked[v] or table_equals(v, t2[k])) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
checked[v] = true
|
||||||
|
elseif t2[k] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
keys[k] = true
|
||||||
|
end
|
||||||
|
for k, _ in pairs(t2) do
|
||||||
|
if not keys[k] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run_player_hotbar_source_tests(player)
|
||||||
local inv = player:get_inventory()
|
local inv = player:get_inventory()
|
||||||
local old_inv_size = inv:get_size("main")
|
local old_inv_size = inv:get_size("main")
|
||||||
local old_inv_list = inv:get_list("main") -- Avoid accidentally removing item
|
local old_inv_list = inv:get_list("main") -- Avoid accidentally removing item
|
||||||
|
@ -195,12 +218,22 @@ local function run_player_hotbar_clamp_tests(player)
|
||||||
|
|
||||||
player:hud_set_hotbar_itemcount(2)
|
player:hud_set_hotbar_itemcount(2)
|
||||||
assert(player:hud_get_hotbar_itemcount() == 2)
|
assert(player:hud_get_hotbar_itemcount() == 2)
|
||||||
|
assert(table_equals(player:get_hotbar_source(), {{list = "main", length = 2, offset = 0}}))
|
||||||
|
|
||||||
|
-- Test clamp
|
||||||
player:hud_set_hotbar_itemcount(6)
|
player:hud_set_hotbar_itemcount(6)
|
||||||
assert(player:hud_get_hotbar_itemcount() == 5)
|
assert(player:hud_get_hotbar_itemcount() == 5)
|
||||||
|
assert(table_equals(player:get_hotbar_source(), {{list = "main", length = 5, offset = 0}}))
|
||||||
|
|
||||||
|
local hotbar_source = {{list = "test", length = 4, offset = 2}, {list = "myinv", length = 16, offset = 99}}
|
||||||
|
print(dump(player:get_hotbar_source()))
|
||||||
|
print(player:set_hotbar_source(hotbar_source))
|
||||||
|
print(dump(player:get_hotbar_source()))
|
||||||
|
assert(table_equals(player:get_hotbar_source(), hotbar_source))
|
||||||
|
assert(player:hud_get_hotbar_itemcount() == 20)
|
||||||
|
|
||||||
inv:set_size("main", old_inv_size)
|
inv:set_size("main", old_inv_size)
|
||||||
inv:set_list("main", old_inv_list)
|
inv:set_list("main", old_inv_list)
|
||||||
player:hud_set_hotbar_itemcount(old_bar_size)
|
player:hud_set_hotbar_itemcount(old_bar_size)
|
||||||
end
|
end
|
||||||
unittests.register("test_player_hotbar_clamp", run_player_hotbar_clamp_tests, {player=true})
|
unittests.register("test_player_hotbar_source", run_player_hotbar_source_tests, {player=true})
|
||||||
|
|
|
@ -1957,7 +1957,7 @@ void Game::processItemSelection(u16 *new_playeritem)
|
||||||
LocalPlayer *player = client->getEnv().getLocalPlayer();
|
LocalPlayer *player = client->getEnv().getLocalPlayer();
|
||||||
|
|
||||||
*new_playeritem = player->getWieldIndex();
|
*new_playeritem = player->getWieldIndex();
|
||||||
u16 max_item = player->getMaxHotbarItemcount();
|
u16 max_item = player->hotbar_source.getMaxLength();
|
||||||
if (max_item == 0)
|
if (max_item == 0)
|
||||||
return;
|
return;
|
||||||
max_item -= 1;
|
max_item -= 1;
|
||||||
|
@ -2009,9 +2009,14 @@ void Game::dropSelectedItem(bool single_item)
|
||||||
IDropAction *a = new IDropAction();
|
IDropAction *a = new IDropAction();
|
||||||
a->count = single_item ? 1 : 0;
|
a->count = single_item ? 1 : 0;
|
||||||
a->from_inv.setCurrentPlayer();
|
a->from_inv.setCurrentPlayer();
|
||||||
a->from_list = "main";
|
|
||||||
a->from_i = client->getEnv().getLocalPlayer()->getWieldIndex();
|
LocalPlayer* player = client->getEnv().getLocalPlayer();
|
||||||
|
u16 index;
|
||||||
|
if (player->hotbar_source.getInventoryFromWieldIndex(player->getWieldIndex(),
|
||||||
|
a->from_list, index)) {
|
||||||
|
a->from_i = index;
|
||||||
client->inventoryAction(a);
|
client->inventoryAction(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::openConsole(float scale, const wchar_t *line)
|
void Game::openConsole(float scale, const wchar_t *line)
|
||||||
|
|
|
@ -229,14 +229,37 @@ void Hud::drawItem(const ItemStack &item, const core::rect<s32>& rect,
|
||||||
client, selected ? IT_ROT_SELECTED : IT_ROT_NONE);
|
client, selected ? IT_ROT_SELECTED : IT_ROT_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: selectitem = 0 -> no selected; selectitem is 1-based
|
void Hud::drawItems(const v2s32& pos, s32 inv_size, s32 inv_offset, InventoryList *mainlist,
|
||||||
// mainlist can be NULL, but draw the frame anyway.
|
u16 selectitem, u16 direction, bool is_hotbar, u16 hotbar_touchcontrol_offset)
|
||||||
void Hud::drawItems(v2s32 screen_pos, v2s32 screen_offset, s32 itemcount, v2f alignment,
|
|
||||||
s32 inv_offset, InventoryList *mainlist, u16 selectitem, u16 direction,
|
|
||||||
bool is_hotbar)
|
|
||||||
{
|
{
|
||||||
s32 height = m_hotbar_imagesize + m_padding * 2;
|
// Store hotbar_selected_image in member variable, used by drawItem()
|
||||||
s32 width = (itemcount - inv_offset) * (m_hotbar_imagesize + m_padding * 2);
|
if (hotbar_selected_image != player->hotbar_selected_image) {
|
||||||
|
hotbar_selected_image = player->hotbar_selected_image;
|
||||||
|
use_hotbar_selected_image = !hotbar_selected_image.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw items
|
||||||
|
core::rect<s32> imgrect(0, 0, m_hotbar_imagesize, m_hotbar_imagesize);
|
||||||
|
const s32 list_max = std::min(inv_size, (s32) (mainlist ? mainlist->getSize() : 0 ));
|
||||||
|
for (s32 i = inv_offset; i < list_max; i++) {
|
||||||
|
|
||||||
|
v2s32 steppos = getInventoryPosOffset(direction, i - inv_offset, list_max - 1 - inv_offset);
|
||||||
|
|
||||||
|
core::rect<s32> item_rect = imgrect + pos + v2s32{m_padding, m_padding} + steppos;
|
||||||
|
|
||||||
|
drawItem(mainlist->getItem(i), item_rect, (i + 1) == selectitem);
|
||||||
|
|
||||||
|
if (is_hotbar && g_touchcontrols)
|
||||||
|
g_touchcontrols->registerHotbarRect(i + hotbar_touchcontrol_offset, item_rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the total width, height and pos to draw a HUD inventory
|
||||||
|
void Hud::getInventoryDimensions(v2s32 screen_pos, const v2s32& screen_offset, s32 inv_length,
|
||||||
|
v2f alignment, u16 direction, v2s32& pos, s32& width, s32& height)
|
||||||
|
{
|
||||||
|
height = m_hotbar_imagesize + m_padding * 2;
|
||||||
|
width = inv_length * (m_hotbar_imagesize + m_padding * 2);
|
||||||
|
|
||||||
if (direction == HUD_DIR_TOP_BOTTOM || direction == HUD_DIR_BOTTOM_TOP) {
|
if (direction == HUD_DIR_TOP_BOTTOM || direction == HUD_DIR_BOTTOM_TOP) {
|
||||||
s32 tmp = height;
|
s32 tmp = height;
|
||||||
|
@ -245,63 +268,147 @@ void Hud::drawItems(v2s32 screen_pos, v2s32 screen_offset, s32 itemcount, v2f al
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position: screen_pos + screen_offset + alignment
|
// Position: screen_pos + screen_offset + alignment
|
||||||
v2s32 pos(screen_offset.X * m_scale_factor, screen_offset.Y * m_scale_factor);
|
pos.X = screen_pos.X + screen_offset.X * m_scale_factor +
|
||||||
pos += screen_pos;
|
(alignment.X - 1.0f) * (width * 0.5f);
|
||||||
pos.X += (alignment.X - 1.0f) * (width * 0.5f);
|
pos.Y = screen_pos.Y + screen_offset.Y * m_scale_factor +
|
||||||
pos.Y += (alignment.Y - 1.0f) * (height * 0.5f);
|
(alignment.Y - 1.0f) * (height * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
// Store hotbar_image in member variable, used by drawItem()
|
// Returns an inventory position offset depending on the direction
|
||||||
if (hotbar_image != player->hotbar_image) {
|
// arguments are the number of items before and the remaining number of items
|
||||||
hotbar_image = player->hotbar_image;
|
v2s32 Hud::getInventoryPosOffset(u16 direction, s32 before, s32 remainder)
|
||||||
use_hotbar_image = !hotbar_image.empty();
|
{
|
||||||
}
|
|
||||||
|
|
||||||
// Store hotbar_selected_image in member variable, used by drawItem()
|
|
||||||
if (hotbar_selected_image != player->hotbar_selected_image) {
|
|
||||||
hotbar_selected_image = player->hotbar_selected_image;
|
|
||||||
use_hotbar_selected_image = !hotbar_selected_image.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw customized item background
|
|
||||||
if (use_hotbar_image) {
|
|
||||||
core::rect<s32> imgrect2(-m_padding/2, -m_padding/2,
|
|
||||||
width+m_padding/2, height+m_padding/2);
|
|
||||||
core::rect<s32> rect2 = imgrect2 + pos;
|
|
||||||
video::ITexture *texture = tsrc->getTexture(hotbar_image);
|
|
||||||
core::dimension2di imgsize(texture->getOriginalSize());
|
|
||||||
draw2DImageFilterScaled(driver, texture, rect2,
|
|
||||||
core::rect<s32>(core::position2d<s32>(0,0), imgsize),
|
|
||||||
NULL, hbar_colors, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw items
|
|
||||||
core::rect<s32> imgrect(0, 0, m_hotbar_imagesize, m_hotbar_imagesize);
|
|
||||||
const s32 list_max = std::min(itemcount, (s32) (mainlist ? mainlist->getSize() : 0 ));
|
|
||||||
for (s32 i = inv_offset; i < list_max; i++) {
|
|
||||||
s32 fullimglen = m_hotbar_imagesize + m_padding * 2;
|
s32 fullimglen = m_hotbar_imagesize + m_padding * 2;
|
||||||
|
|
||||||
v2s32 steppos;
|
v2s32 steppos;
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case HUD_DIR_RIGHT_LEFT:
|
case HUD_DIR_RIGHT_LEFT:
|
||||||
steppos = v2s32(m_padding + (list_max - 1 - i - inv_offset) * fullimglen, m_padding);
|
steppos = v2s32((remainder - before) * fullimglen, 0);
|
||||||
break;
|
break;
|
||||||
case HUD_DIR_TOP_BOTTOM:
|
case HUD_DIR_TOP_BOTTOM:
|
||||||
steppos = v2s32(m_padding, m_padding + (i - inv_offset) * fullimglen);
|
steppos = v2s32(0, before * fullimglen);
|
||||||
break;
|
break;
|
||||||
case HUD_DIR_BOTTOM_TOP:
|
case HUD_DIR_BOTTOM_TOP:
|
||||||
steppos = v2s32(m_padding, m_padding + (list_max - 1 - i - inv_offset) * fullimglen);
|
steppos = v2s32(0, (remainder - before) * fullimglen);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
steppos = v2s32(m_padding + (i - inv_offset) * fullimglen, m_padding);
|
steppos = v2s32(before * fullimglen, 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
core::rect<s32> item_rect = imgrect + pos + steppos;
|
return steppos;
|
||||||
|
}
|
||||||
|
|
||||||
drawItem(mainlist->getItem(i), item_rect, (i + 1) == selectitem);
|
void Hud::drawInventoryBackground(const v2s32& pos, s32 width, s32 height)
|
||||||
|
{
|
||||||
|
// Store hotbar_image in member variable
|
||||||
|
if (hotbar_image != player->hotbar_image) {
|
||||||
|
hotbar_image = player->hotbar_image;
|
||||||
|
use_hotbar_image = !hotbar_image.empty();
|
||||||
|
}
|
||||||
|
|
||||||
if (is_hotbar && g_touchcontrols)
|
if (!use_hotbar_image) {
|
||||||
g_touchcontrols->registerHotbarRect(i, item_rect);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::rect<s32> imgrect2(-m_padding/2, -m_padding/2, width+m_padding/2, height+m_padding/2);
|
||||||
|
core::rect<s32> rect2 = imgrect2 + pos;
|
||||||
|
video::ITexture *texture = tsrc->getTexture(hotbar_image);
|
||||||
|
core::dimension2di imgsize(texture->getOriginalSize());
|
||||||
|
draw2DImageFilterScaled(driver, texture, rect2,
|
||||||
|
core::rect<s32>(core::position2d<s32>(0,0), imgsize),
|
||||||
|
nullptr, hbar_colors, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: selectitem = 0 -> no selected; selectitem is 1-based
|
||||||
|
// mainlist can be NULL, but draw the frame anyway.
|
||||||
|
void Hud::drawInventory(const v2s32& screen_pos, const v2f& offset, s32 itemcount, v2f alignment,
|
||||||
|
InventoryList *mainlist, u16 selectitem, u16 direction)
|
||||||
|
{
|
||||||
|
v2s32 screen_offset(offset.X, offset.Y);
|
||||||
|
s32 height, width;
|
||||||
|
v2s32 pos;
|
||||||
|
getInventoryDimensions(screen_pos, screen_offset, itemcount, alignment,
|
||||||
|
direction, pos, width, height);
|
||||||
|
|
||||||
|
drawInventoryBackground(pos, width, height);
|
||||||
|
drawItems(pos, itemcount, 0, mainlist, selectitem, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::drawHotbar(const v2s32 &screen_pos, const v2f &offset, u16 direction,
|
||||||
|
const v2f &alignment, const v3f &world_pos)
|
||||||
|
{
|
||||||
|
if (g_touchcontrols)
|
||||||
|
g_touchcontrols->resetHotbarRects();
|
||||||
|
|
||||||
|
auto& sources = player->hotbar_source.getSources();
|
||||||
|
u16 wield_index = player->getWieldIndex() + 1;
|
||||||
|
v2s32 screen_offset(offset.X, offset.Y);
|
||||||
|
|
||||||
|
if (!world_pos.X) {
|
||||||
|
// All invs next to each other
|
||||||
|
if (world_pos.Z > player->hotbar_source.getMaxLength())
|
||||||
|
return;
|
||||||
|
s32 hotbar_length = world_pos.Y ? world_pos.Y : player->hotbar_source.getMaxLength() - world_pos.Z;
|
||||||
|
|
||||||
|
s32 height, width;
|
||||||
|
v2s32 pos;
|
||||||
|
getInventoryDimensions(screen_pos, screen_offset, hotbar_length, alignment,
|
||||||
|
direction, pos, width, height);
|
||||||
|
drawInventoryBackground(pos, width, height);
|
||||||
|
|
||||||
|
// Handle offset
|
||||||
|
std::size_t source_index = 0;
|
||||||
|
u16 length_before = 0;
|
||||||
|
for (s32 inv_offset = world_pos.Z; source_index < sources.size(); source_index++) {
|
||||||
|
const HotbarSource::Source& source = sources[source_index];
|
||||||
|
if (inv_offset < source.length) {
|
||||||
|
s32 inv_length = MYMIN(source.length - inv_offset, hotbar_length);
|
||||||
|
|
||||||
|
drawItems(pos + getInventoryPosOffset(direction, 0, hotbar_length - inv_length),
|
||||||
|
inv_length + source.offset + inv_offset, source.offset + inv_offset,
|
||||||
|
inventory->getList(source.list), wield_index + source.offset,
|
||||||
|
direction, true, 0);
|
||||||
|
length_before = inv_length;
|
||||||
|
source_index++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
inv_offset -= source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; source_index < sources.size(); source_index++) {
|
||||||
|
const HotbarSource::Source& source = sources[source_index];
|
||||||
|
s32 inv_length = MYMIN(source.length, hotbar_length - length_before);
|
||||||
|
if(inv_length <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
drawItems(pos + getInventoryPosOffset(direction, length_before, hotbar_length - inv_length),
|
||||||
|
inv_length + source.offset,
|
||||||
|
source.offset, inventory->getList(source.list),
|
||||||
|
wield_index - length_before + source.offset, direction, true, length_before);
|
||||||
|
length_before += inv_length;
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only a single inventory
|
||||||
|
if (world_pos.X > sources.size())
|
||||||
|
return;
|
||||||
|
const HotbarSource::Source& source = sources[world_pos.X-1];
|
||||||
|
if (world_pos.Z > source.length)
|
||||||
|
return;
|
||||||
|
s32 inv_length = world_pos.Y ? world_pos.Y : source.length - world_pos.Z;
|
||||||
|
|
||||||
|
s32 height, width;
|
||||||
|
v2s32 pos;
|
||||||
|
getInventoryDimensions(screen_pos, screen_offset, inv_length, alignment, direction,
|
||||||
|
pos, width, height);
|
||||||
|
drawInventoryBackground(pos, width, height);
|
||||||
|
|
||||||
|
u16 length_before = player->hotbar_source.getLengthBefore(world_pos.X - 1);
|
||||||
|
s32 inv_offset = source.offset + world_pos.Z;
|
||||||
|
inv_length = MYMIN(inv_length, source.length);
|
||||||
|
drawItems(pos, inv_length + inv_offset, inv_offset, inventory->getList(source.list),
|
||||||
|
wield_index - length_before + inv_offset, direction, true, length_before);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,8 +541,7 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
|
||||||
InventoryList *inv = inventory->getList(e->text);
|
InventoryList *inv = inventory->getList(e->text);
|
||||||
if (!inv)
|
if (!inv)
|
||||||
warningstream << "HUD: Unknown inventory list. name=" << e->text << std::endl;
|
warningstream << "HUD: Unknown inventory list. name=" << e->text << std::endl;
|
||||||
drawItems(pos, v2s32(e->offset.X, e->offset.Y), e->number, e->align, 0,
|
drawInventory(pos, e->offset, e->number, e->align, inv, e->item, e->dir);
|
||||||
inv, e->item, e->dir, false);
|
|
||||||
break; }
|
break; }
|
||||||
case HUD_ELEM_WAYPOINT: {
|
case HUD_ELEM_WAYPOINT: {
|
||||||
if (!calculateScreenPos(camera_offset, e, &pos))
|
if (!calculateScreenPos(camera_offset, e, &pos))
|
||||||
|
@ -567,7 +673,25 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
|
||||||
client->getMinimap()->drawMinimap(rect);
|
client->getMinimap()->drawMinimap(rect);
|
||||||
break; }
|
break; }
|
||||||
case HUD_ELEM_HOTBAR: {
|
case HUD_ELEM_HOTBAR: {
|
||||||
drawHotbar(pos, e->offset, e->dir, e->align);
|
if (!e->world_pos.X) { // Handle splitting caused by hud_hotbar_max_width
|
||||||
|
u16 hotbar_itemcount = e->world_pos.Y ? e->world_pos.Y :
|
||||||
|
player->hotbar_source.getMaxLength();
|
||||||
|
hotbar_itemcount -= e->world_pos.Z;
|
||||||
|
s32 width = hotbar_itemcount * (m_hotbar_imagesize + m_padding * 2);
|
||||||
|
const v2u32 &window_size = RenderingEngine::getWindowSize();
|
||||||
|
if ((float) width / (float) window_size.X >
|
||||||
|
g_settings->getFloat("hud_hotbar_max_width")) {
|
||||||
|
v2s32 upper_pos = pos - v2s32(0, m_hotbar_imagesize + m_padding);
|
||||||
|
u16 upper_itemcount = hotbar_itemcount/2;
|
||||||
|
drawHotbar(upper_pos, e->offset, e->dir, e->align,
|
||||||
|
{0.f, (float) upper_itemcount, e->world_pos.Z});
|
||||||
|
drawHotbar(pos, e->offset, e->dir, e->align,
|
||||||
|
{0.f, (float) hotbar_itemcount - upper_itemcount,
|
||||||
|
e->world_pos.Z + upper_itemcount});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawHotbar(pos, e->offset, e->dir, e->align, e->world_pos);
|
||||||
break; }
|
break; }
|
||||||
default:
|
default:
|
||||||
infostream << "Hud::drawLuaElements: ignoring drawform " << e->type
|
infostream << "Hud::drawLuaElements: ignoring drawform " << e->type
|
||||||
|
@ -778,38 +902,6 @@ void Hud::drawStatbar(v2s32 pos, u16 corner, u16 drawdir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void Hud::drawHotbar(const v2s32 &pos, const v2f &offset, u16 dir, const v2f &align)
|
|
||||||
{
|
|
||||||
if (g_touchcontrols)
|
|
||||||
g_touchcontrols->resetHotbarRects();
|
|
||||||
|
|
||||||
InventoryList *mainlist = inventory->getList("main");
|
|
||||||
if (mainlist == NULL) {
|
|
||||||
// Silently ignore this. We may not be initialized completely.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
u16 playeritem = player->getWieldIndex();
|
|
||||||
v2s32 screen_offset(offset.X, offset.Y);
|
|
||||||
|
|
||||||
s32 hotbar_itemcount = player->getMaxHotbarItemcount();
|
|
||||||
s32 width = hotbar_itemcount * (m_hotbar_imagesize + m_padding * 2);
|
|
||||||
|
|
||||||
const v2u32 &window_size = RenderingEngine::getWindowSize();
|
|
||||||
if ((float) width / (float) window_size.X <=
|
|
||||||
g_settings->getFloat("hud_hotbar_max_width")) {
|
|
||||||
drawItems(pos, screen_offset, hotbar_itemcount, align, 0,
|
|
||||||
mainlist, playeritem + 1, dir, true);
|
|
||||||
} else {
|
|
||||||
v2s32 upper_pos = pos - v2s32(0, m_hotbar_imagesize + m_padding);
|
|
||||||
|
|
||||||
drawItems(upper_pos, screen_offset, hotbar_itemcount / 2, align, 0,
|
|
||||||
mainlist, playeritem + 1, dir, true);
|
|
||||||
drawItems(pos, screen_offset, hotbar_itemcount, align,
|
|
||||||
hotbar_itemcount / 2, mainlist, playeritem + 1, dir, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Hud::drawCrosshair()
|
void Hud::drawCrosshair()
|
||||||
{
|
{
|
||||||
|
|
|
@ -62,7 +62,6 @@ public:
|
||||||
void disableBlockBounds();
|
void disableBlockBounds();
|
||||||
void drawBlockBounds();
|
void drawBlockBounds();
|
||||||
|
|
||||||
void drawHotbar(const v2s32 &pos, const v2f &offset, u16 direction, const v2f &align);
|
|
||||||
void resizeHotbar();
|
void resizeHotbar();
|
||||||
void drawCrosshair();
|
void drawCrosshair();
|
||||||
void drawSelectionMesh();
|
void drawSelectionMesh();
|
||||||
|
@ -104,11 +103,20 @@ private:
|
||||||
const std::string &texture, const std::string& bgtexture,
|
const std::string &texture, const std::string& bgtexture,
|
||||||
s32 count, s32 maxcount, v2s32 offset, v2s32 size = v2s32());
|
s32 count, s32 maxcount, v2s32 offset, v2s32 size = v2s32());
|
||||||
|
|
||||||
void drawItems(v2s32 screen_pos, v2s32 screen_offset, s32 itemcount, v2f alignment,
|
|
||||||
s32 inv_offset, InventoryList *mainlist, u16 selectitem,
|
|
||||||
u16 direction, bool is_hotbar);
|
|
||||||
|
|
||||||
void drawItem(const ItemStack &item, const core::rect<s32> &rect, bool selected);
|
void drawItem(const ItemStack &item, const core::rect<s32> &rect, bool selected);
|
||||||
|
void drawItems(const v2s32& pos, s32 inv_size, s32 inv_offset, InventoryList *mainlist,
|
||||||
|
u16 selectitem, u16 direction, bool is_hotbar = false,
|
||||||
|
u16 hotbar_touchcontrol_offset = 0);
|
||||||
|
|
||||||
|
v2s32 getInventoryPosOffset(u16 direction, s32 before, s32 remainder);
|
||||||
|
void getInventoryDimensions(v2s32 screen_pos, const v2s32& screen_offset, s32 inv_length,
|
||||||
|
v2f alignment, u16 direction, v2s32& pos, s32& width, s32& height);
|
||||||
|
void drawInventoryBackground(const v2s32& pos, s32 width, s32 height);
|
||||||
|
void drawInventory(const v2s32& screen_pos, const v2f& offset, s32 itemcount,
|
||||||
|
v2f alignment, InventoryList *mainlist, u16 selectitem, u16 direction);
|
||||||
|
|
||||||
|
void drawHotbar(const v2s32 &pos, const v2f &offset, u16 direction, const v2f &alignment,
|
||||||
|
const v3f &world_pos);
|
||||||
|
|
||||||
void drawCompassTranslate(HudElement *e, video::ITexture *texture,
|
void drawCompassTranslate(HudElement *e, video::ITexture *texture,
|
||||||
const core::rect<s32> &rect, int way);
|
const core::rect<s32> &rect, int way);
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "hud.h"
|
#include "hud.h"
|
||||||
#include "log_internal.h"
|
#include "log_internal.h"
|
||||||
#include "client/renderingengine.h"
|
#include "client/renderingengine.h"
|
||||||
|
#include "util/hotbar_source.h" // HOTBAR_ITEMCOUNT_MAX
|
||||||
|
|
||||||
void KeyCache::populate_nonchanging()
|
void KeyCache::populate_nonchanging()
|
||||||
{
|
{
|
||||||
|
@ -69,7 +70,7 @@ void KeyCache::populate()
|
||||||
key[KeyType::QUICKTUNE_INC] = getKeySetting("keymap_quicktune_inc");
|
key[KeyType::QUICKTUNE_INC] = getKeySetting("keymap_quicktune_inc");
|
||||||
key[KeyType::QUICKTUNE_DEC] = getKeySetting("keymap_quicktune_dec");
|
key[KeyType::QUICKTUNE_DEC] = getKeySetting("keymap_quicktune_dec");
|
||||||
|
|
||||||
for (int i = 0; i < HUD_HOTBAR_ITEMCOUNT_MAX; i++) {
|
for (int i = 0; i < HOTBAR_ITEMCOUNT_MAX; i++) {
|
||||||
std::string slot_key_name = "keymap_slot" + std::to_string(i + 1);
|
std::string slot_key_name = "keymap_slot" + std::to_string(i + 1);
|
||||||
key[KeyType::SLOT_1 + i] = getKeySetting(slot_key_name.c_str());
|
key[KeyType::SLOT_1 + i] = getKeySetting(slot_key_name.c_str());
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,10 @@
|
||||||
#define HUD_FLAG_BASIC_DEBUG (1 << 7)
|
#define HUD_FLAG_BASIC_DEBUG (1 << 7)
|
||||||
#define HUD_FLAG_CHAT_VISIBLE (1 << 8)
|
#define HUD_FLAG_CHAT_VISIBLE (1 << 8)
|
||||||
|
|
||||||
#define HUD_PARAM_HOTBAR_ITEMCOUNT 1
|
#define HUD_PARAM_HOTBAR_ITEMCOUNT 1 // Only send by servers with protocol version < 48
|
||||||
#define HUD_PARAM_HOTBAR_IMAGE 2
|
#define HUD_PARAM_HOTBAR_IMAGE 2
|
||||||
#define HUD_PARAM_HOTBAR_SELECTED_IMAGE 3
|
#define HUD_PARAM_HOTBAR_SELECTED_IMAGE 3
|
||||||
|
#define HUD_PARAM_HOTBAR_SOURCE 4
|
||||||
#define HUD_HOTBAR_ITEMCOUNT_DEFAULT 8
|
|
||||||
#define HUD_HOTBAR_ITEMCOUNT_MAX 32
|
|
||||||
|
|
||||||
#define HOTBAR_IMAGE_SIZE 48
|
#define HOTBAR_IMAGE_SIZE 48
|
||||||
|
|
||||||
|
|
|
@ -1264,18 +1264,27 @@ void Client::handleCommand_HudSetParam(NetworkPacket* pkt)
|
||||||
*pkt >> param >> value;
|
*pkt >> param >> value;
|
||||||
|
|
||||||
LocalPlayer *player = m_env.getLocalPlayer();
|
LocalPlayer *player = m_env.getLocalPlayer();
|
||||||
assert(player != NULL);
|
assert(player != nullptr);
|
||||||
|
|
||||||
if (param == HUD_PARAM_HOTBAR_ITEMCOUNT && value.size() == 4) {
|
switch(param) {
|
||||||
|
case HUD_PARAM_HOTBAR_ITEMCOUNT: {
|
||||||
|
if (value.size() != 4)
|
||||||
|
break;
|
||||||
s32 hotbar_itemcount = readS32((u8*) value.c_str());
|
s32 hotbar_itemcount = readS32((u8*) value.c_str());
|
||||||
if (hotbar_itemcount > 0 && hotbar_itemcount <= HUD_HOTBAR_ITEMCOUNT_MAX)
|
player->hotbar_source.setHotbarItemcountLegacy(hotbar_itemcount);
|
||||||
player->hud_hotbar_itemcount = hotbar_itemcount;
|
break; }
|
||||||
}
|
case HUD_PARAM_HOTBAR_IMAGE:
|
||||||
else if (param == HUD_PARAM_HOTBAR_IMAGE) {
|
|
||||||
player->hotbar_image = value;
|
player->hotbar_image = value;
|
||||||
}
|
break;
|
||||||
else if (param == HUD_PARAM_HOTBAR_SELECTED_IMAGE) {
|
case HUD_PARAM_HOTBAR_SELECTED_IMAGE:
|
||||||
player->hotbar_selected_image = value;
|
player->hotbar_selected_image = value;
|
||||||
|
break;
|
||||||
|
case HUD_PARAM_HOTBAR_SOURCE: {
|
||||||
|
std::istringstream is(value);
|
||||||
|
player->hotbar_source.deSerialize(is);
|
||||||
|
break; }
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
[scheduled bump for 5.11.0]
|
[scheduled bump for 5.11.0]
|
||||||
PROTOCOL VERSION 48
|
PROTOCOL VERSION 48
|
||||||
Add compression to some existing packets
|
Add compression to some existing packets
|
||||||
|
Send HUD_PARAM_HOTBAR_SOURCE instead of HUD_PARAM_HOTBAR_ITEMCOUNT
|
||||||
[scheduled bump for 5.12.0]
|
[scheduled bump for 5.12.0]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -819,11 +819,11 @@ void Server::handleCommand_PlayerItem(NetworkPacket* pkt)
|
||||||
|
|
||||||
*pkt >> item;
|
*pkt >> item;
|
||||||
|
|
||||||
if (item >= player->getMaxHotbarItemcount()) {
|
if (item >= player->hotbar_source.getMaxLength()) {
|
||||||
actionstream << "Player " << player->getName()
|
actionstream << "Player " << player->getName()
|
||||||
<< " tried to access item=" << item
|
<< " tried to access item=" << item
|
||||||
<< " out of hotbar_itemcount="
|
<< " while max="
|
||||||
<< player->getMaxHotbarItemcount()
|
<< player->hotbar_source.getMaxLength()
|
||||||
<< "; ignoring." << std::endl;
|
<< "; ignoring." << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -916,11 +916,11 @@ void Server::handleCommand_Interact(NetworkPacket *pkt)
|
||||||
|
|
||||||
// Update wielded item
|
// Update wielded item
|
||||||
|
|
||||||
if (item_i >= player->getMaxHotbarItemcount()) {
|
if (item_i >= player->hotbar_source.getMaxLength()) {
|
||||||
actionstream << "Player " << player->getName()
|
actionstream << "Player " << player->getName()
|
||||||
<< " tried to access item=" << item_i
|
<< " tried to access item=" << item_i
|
||||||
<< " out of hotbar_itemcount="
|
<< " while max="
|
||||||
<< player->getMaxHotbarItemcount()
|
<< player->hotbar_source.getMaxLength()
|
||||||
<< "; ignoring." << std::endl;
|
<< "; ignoring." << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ Player::Player(const std::string &name, IItemDefManager *idef):
|
||||||
HUD_FLAG_MINIMAP_RADAR_VISIBLE | HUD_FLAG_BASIC_DEBUG |
|
HUD_FLAG_MINIMAP_RADAR_VISIBLE | HUD_FLAG_BASIC_DEBUG |
|
||||||
HUD_FLAG_CHAT_VISIBLE;
|
HUD_FLAG_CHAT_VISIBLE;
|
||||||
|
|
||||||
hud_hotbar_itemcount = HUD_HOTBAR_ITEMCOUNT_DEFAULT;
|
hotbar_source.addSource(HOTBAR_INVENTORY_LIST_DEFAULT, HOTBAR_ITEMCOUNT_DEFAULT, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Player::~Player()
|
Player::~Player()
|
||||||
|
@ -82,25 +82,27 @@ Player::~Player()
|
||||||
|
|
||||||
void Player::setWieldIndex(u16 index)
|
void Player::setWieldIndex(u16 index)
|
||||||
{
|
{
|
||||||
const InventoryList *mlist = inventory.getList("main");
|
m_wield_index = MYMIN(index, hotbar_source.getMaxLength() - 1);
|
||||||
m_wield_index = MYMIN(index, mlist ? mlist->getSize() : 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u16 Player::getWieldIndex()
|
u16 Player::getWieldIndex()
|
||||||
{
|
{
|
||||||
return std::min(m_wield_index, getMaxHotbarItemcount());
|
return MYMIN(m_wield_index, hotbar_source.getMaxLength() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack &Player::getWieldedItem(ItemStack *selected, ItemStack *hand) const
|
ItemStack &Player::getWieldedItem(ItemStack *selected, ItemStack *hand) const
|
||||||
{
|
{
|
||||||
assert(selected);
|
assert(selected);
|
||||||
|
|
||||||
const InventoryList *mlist = inventory.getList("main"); // TODO: Make this generic
|
std::string list;
|
||||||
|
u16 index;
|
||||||
|
if (hotbar_source.getInventoryFromWieldIndex(m_wield_index, list, index)) {
|
||||||
|
const InventoryList *mlist = inventory.getList(list);
|
||||||
|
if (mlist && index < mlist->getSize())
|
||||||
|
*selected = mlist->getItem(index);
|
||||||
|
}
|
||||||
|
|
||||||
const InventoryList *hlist = inventory.getList("hand");
|
const InventoryList *hlist = inventory.getList("hand");
|
||||||
|
|
||||||
if (mlist && m_wield_index < mlist->getSize())
|
|
||||||
*selected = mlist->getItem(m_wield_index);
|
|
||||||
|
|
||||||
if (hand && hlist)
|
if (hand && hlist)
|
||||||
*hand = hlist->getItem(0);
|
*hand = hlist->getItem(0);
|
||||||
|
|
||||||
|
@ -160,12 +162,6 @@ void Player::clearHud()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u16 Player::getMaxHotbarItemcount()
|
|
||||||
{
|
|
||||||
InventoryList *mainlist = inventory.getList("main");
|
|
||||||
return mainlist ? std::min(mainlist->getSize(), (u32) hud_hotbar_itemcount) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerControl::setMovementFromKeys()
|
void PlayerControl::setMovementFromKeys()
|
||||||
{
|
{
|
||||||
bool a_up = direction_keys & (1 << 0),
|
bool a_up = direction_keys & (1 << 0),
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include "util/hotbar_source.h"
|
||||||
|
|
||||||
#define PLAYERNAME_SIZE 20
|
#define PLAYERNAME_SIZE 20
|
||||||
|
|
||||||
|
@ -228,10 +229,7 @@ public:
|
||||||
void clearHud();
|
void clearHud();
|
||||||
|
|
||||||
u32 hud_flags;
|
u32 hud_flags;
|
||||||
s32 hud_hotbar_itemcount;
|
HotbarSource hotbar_source;
|
||||||
|
|
||||||
// Get actual usable number of hotbar items (clamped to size of "main" list)
|
|
||||||
u16 getMaxHotbarItemcount();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string m_name;
|
std::string m_name;
|
||||||
|
|
|
@ -35,12 +35,8 @@ public:
|
||||||
|
|
||||||
RemotePlayerChatResult canSendChatMessage();
|
RemotePlayerChatResult canSendChatMessage();
|
||||||
|
|
||||||
void setHotbarItemcount(s32 hotbar_itemcount)
|
void setHotbarSource(const HotbarSource& source) { hotbar_source = source; }
|
||||||
{
|
const HotbarSource& getHotbarSource() const { return hotbar_source; }
|
||||||
hud_hotbar_itemcount = hotbar_itemcount;
|
|
||||||
}
|
|
||||||
|
|
||||||
s32 getHotbarItemcount() const { return hud_hotbar_itemcount; }
|
|
||||||
|
|
||||||
void overrideDayNightRatio(bool do_override, float ratio)
|
void overrideDayNightRatio(bool do_override, float ratio)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1933,7 +1933,7 @@ int ObjectRef::l_hud_set_hotbar_itemcount(lua_State *L)
|
||||||
|
|
||||||
s32 hotbar_itemcount = luaL_checkint(L, 2);
|
s32 hotbar_itemcount = luaL_checkint(L, 2);
|
||||||
|
|
||||||
if (!getServer(L)->hudSetHotbarItemcount(player, hotbar_itemcount))
|
if (!getServer(L)->hudSetHotbarItemcountLegacy(player, hotbar_itemcount))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
lua_pushboolean(L, true);
|
lua_pushboolean(L, true);
|
||||||
|
@ -1949,7 +1949,71 @@ int ObjectRef::l_hud_get_hotbar_itemcount(lua_State *L)
|
||||||
if (player == nullptr)
|
if (player == nullptr)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
lua_pushinteger(L, player->getMaxHotbarItemcount());
|
lua_pushinteger(L, player->hotbar_source.getMaxLength());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_hotbar_source(self)
|
||||||
|
int ObjectRef::l_get_hotbar_source(lua_State *L)
|
||||||
|
{
|
||||||
|
NO_MAP_LOCK_REQUIRED;
|
||||||
|
ObjectRef *ref = checkObject<ObjectRef>(L, 1);
|
||||||
|
RemotePlayer *player = getplayer(ref);
|
||||||
|
if (player == nullptr)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
lua_newtable(L);
|
||||||
|
|
||||||
|
auto& sources = player->hotbar_source.getSources();
|
||||||
|
for (std::size_t i = 0; i < sources.size(); i++) {
|
||||||
|
auto& source = sources[i];
|
||||||
|
lua_newtable(L);
|
||||||
|
|
||||||
|
lua_pushstring(L, source.list.c_str());
|
||||||
|
lua_setfield(L, -2, "list");
|
||||||
|
|
||||||
|
lua_pushinteger(L, source.length);
|
||||||
|
lua_setfield(L, -2, "length");
|
||||||
|
|
||||||
|
lua_pushinteger(L, source.offset);
|
||||||
|
lua_setfield(L, -2, "offset");
|
||||||
|
|
||||||
|
lua_rawseti(L,-2, i+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set_hotbar_source(self, sources)
|
||||||
|
int ObjectRef::l_set_hotbar_source(lua_State *L)
|
||||||
|
{
|
||||||
|
NO_MAP_LOCK_REQUIRED;
|
||||||
|
ObjectRef *ref = checkObject<ObjectRef>(L, 1);
|
||||||
|
RemotePlayer *player = getplayer(ref);
|
||||||
|
if (player == nullptr)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if(!lua_istable(L, 2))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
HotbarSource hotbar_source;
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
while(lua_next(L, 2) != 0){
|
||||||
|
std::string_view list;
|
||||||
|
if(lua_istable(L, -1) && getstringfield(L, -1, "list", list)) {
|
||||||
|
hotbar_source.addSource(list,
|
||||||
|
getintfield_default(L, -1, "length", 0),
|
||||||
|
getintfield_default(L, -1, "offset", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getServer(L)->hudSetHotbarSource(player, hotbar_source))
|
||||||
|
return 0;
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2905,6 +2969,8 @@ luaL_Reg ObjectRef::methods[] = {
|
||||||
luamethod(ObjectRef, hud_get_flags),
|
luamethod(ObjectRef, hud_get_flags),
|
||||||
luamethod(ObjectRef, hud_set_hotbar_itemcount),
|
luamethod(ObjectRef, hud_set_hotbar_itemcount),
|
||||||
luamethod(ObjectRef, hud_get_hotbar_itemcount),
|
luamethod(ObjectRef, hud_get_hotbar_itemcount),
|
||||||
|
luamethod(ObjectRef, get_hotbar_source),
|
||||||
|
luamethod(ObjectRef, set_hotbar_source),
|
||||||
luamethod(ObjectRef, hud_set_hotbar_image),
|
luamethod(ObjectRef, hud_set_hotbar_image),
|
||||||
luamethod(ObjectRef, hud_get_hotbar_image),
|
luamethod(ObjectRef, hud_get_hotbar_image),
|
||||||
luamethod(ObjectRef, hud_set_hotbar_selected_image),
|
luamethod(ObjectRef, hud_set_hotbar_selected_image),
|
||||||
|
|
|
@ -314,6 +314,12 @@ private:
|
||||||
// hud_get_hotbar_itemcount(self)
|
// hud_get_hotbar_itemcount(self)
|
||||||
static int l_hud_get_hotbar_itemcount(lua_State *L);
|
static int l_hud_get_hotbar_itemcount(lua_State *L);
|
||||||
|
|
||||||
|
// get_hotbar_source(self)
|
||||||
|
static int l_get_hotbar_source(lua_State *L);
|
||||||
|
|
||||||
|
// set_hotbar_source(self, sources)
|
||||||
|
static int l_set_hotbar_source(lua_State *L);
|
||||||
|
|
||||||
// hud_set_hotbar_image(self, name)
|
// hud_set_hotbar_image(self, name)
|
||||||
static int l_hud_set_hotbar_image(lua_State *L);
|
static int l_hud_set_hotbar_image(lua_State *L);
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
#include "particles.h"
|
#include "particles.h"
|
||||||
#include "gettext.h"
|
#include "gettext.h"
|
||||||
#include "util/tracy_wrapper.h"
|
#include "util/tracy_wrapper.h"
|
||||||
|
#include "util/hotbar_source.h"
|
||||||
|
|
||||||
class ClientNotFoundException : public BaseException
|
class ClientNotFoundException : public BaseException
|
||||||
{
|
{
|
||||||
|
@ -3451,21 +3452,44 @@ bool Server::hudSetFlags(RemotePlayer *player, u32 flags, u32 mask)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Server::hudSetHotbarItemcount(RemotePlayer *player, s32 hotbar_itemcount)
|
bool Server::hudSetHotbarItemcountLegacy(RemotePlayer *player, s32 hotbar_itemcount)
|
||||||
{
|
{
|
||||||
if (!player)
|
if (!player)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (hotbar_itemcount <= 0 || hotbar_itemcount > HUD_HOTBAR_ITEMCOUNT_MAX)
|
if (hotbar_itemcount <= 0 || hotbar_itemcount > HOTBAR_ITEMCOUNT_MAX)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (player->getHotbarItemcount() == hotbar_itemcount)
|
InventoryList *mainlist = player->inventory.getList("main");
|
||||||
return true;
|
hotbar_itemcount = mainlist ? MYMIN(mainlist->getSize(), (u32) hotbar_itemcount) : 0;
|
||||||
|
|
||||||
player->setHotbarItemcount(hotbar_itemcount);
|
player->hotbar_source.setHotbarItemcountLegacy(hotbar_itemcount);
|
||||||
|
|
||||||
|
if (player->protocol_version < 48) {
|
||||||
std::ostringstream os(std::ios::binary);
|
std::ostringstream os(std::ios::binary);
|
||||||
writeS32(os, hotbar_itemcount);
|
writeS32(os, hotbar_itemcount);
|
||||||
SendHUDSetParam(player->getPeerId(), HUD_PARAM_HOTBAR_ITEMCOUNT, os.str());
|
SendHUDSetParam(player->getPeerId(), HUD_PARAM_HOTBAR_ITEMCOUNT, os.str());
|
||||||
|
} else {
|
||||||
|
std::ostringstream os(std::ios::binary);
|
||||||
|
player->hotbar_source.serialize(os);
|
||||||
|
SendHUDSetParam(player->getPeerId(), HUD_PARAM_HOTBAR_SOURCE, os.str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Server::hudSetHotbarSource(RemotePlayer *player, const HotbarSource& source)
|
||||||
|
{
|
||||||
|
if (!player)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (source.getMaxLength() > HOTBAR_ITEMCOUNT_MAX)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
player->setHotbarSource(source);
|
||||||
|
|
||||||
|
std::ostringstream os(std::ios::binary);
|
||||||
|
source.serialize(os);
|
||||||
|
SendHUDSetParam(player->getPeerId(), HUD_PARAM_HOTBAR_SOURCE, os.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
#include "util/basic_macros.h"
|
#include "util/basic_macros.h"
|
||||||
#include "util/metricsbackend.h"
|
#include "util/metricsbackend.h"
|
||||||
|
#include "util/hotbar_source.h"
|
||||||
#include "serverenvironment.h"
|
#include "serverenvironment.h"
|
||||||
#include "server/clientiface.h"
|
#include "server/clientiface.h"
|
||||||
#include "threading/ordered_mutex.h"
|
#include "threading/ordered_mutex.h"
|
||||||
|
@ -364,7 +365,8 @@ public:
|
||||||
bool hudRemove(RemotePlayer *player, u32 id);
|
bool hudRemove(RemotePlayer *player, u32 id);
|
||||||
bool hudChange(RemotePlayer *player, u32 id, HudElementStat stat, void *value);
|
bool hudChange(RemotePlayer *player, u32 id, HudElementStat stat, void *value);
|
||||||
bool hudSetFlags(RemotePlayer *player, u32 flags, u32 mask);
|
bool hudSetFlags(RemotePlayer *player, u32 flags, u32 mask);
|
||||||
bool hudSetHotbarItemcount(RemotePlayer *player, s32 hotbar_itemcount);
|
bool hudSetHotbarItemcountLegacy(RemotePlayer *player, s32 hotbar_itemcount);
|
||||||
|
bool hudSetHotbarSource(RemotePlayer *player, const HotbarSource& source);
|
||||||
void hudSetHotbarImage(RemotePlayer *player, const std::string &name);
|
void hudSetHotbarImage(RemotePlayer *player, const std::string &name);
|
||||||
void hudSetHotbarSelectedImage(RemotePlayer *player, const std::string &name);
|
void hudSetHotbarSelectedImage(RemotePlayer *player, const std::string &name);
|
||||||
|
|
||||||
|
|
|
@ -582,9 +582,13 @@ ItemStack PlayerSAO::getWieldedItem(ItemStack *selected, ItemStack *hand) const
|
||||||
|
|
||||||
bool PlayerSAO::setWieldedItem(const ItemStack &item)
|
bool PlayerSAO::setWieldedItem(const ItemStack &item)
|
||||||
{
|
{
|
||||||
InventoryList *mlist = m_player->inventory.getList(getWieldList());
|
std::string list;
|
||||||
if (mlist) {
|
u16 index;
|
||||||
mlist->changeItem(m_player->getWieldIndex(), item);
|
if (m_player->hotbar_source.getInventoryFromWieldIndex(getWieldIndex(), list, index)) {
|
||||||
|
InventoryList *mlist = m_player->inventory.getList(list);
|
||||||
|
if (!mlist)
|
||||||
|
return false;
|
||||||
|
mlist->changeItem(index, item);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -117,7 +117,6 @@ public:
|
||||||
Inventory *getInventory() const override;
|
Inventory *getInventory() const override;
|
||||||
InventoryLocation getInventoryLocation() const override;
|
InventoryLocation getInventoryLocation() const override;
|
||||||
void setInventoryModified() override {}
|
void setInventoryModified() override {}
|
||||||
std::string getWieldList() const override { return "main"; }
|
|
||||||
u16 getWieldIndex() const override;
|
u16 getWieldIndex() const override;
|
||||||
ItemStack getWieldedItem(ItemStack *selected, ItemStack *hand = nullptr) const override;
|
ItemStack getWieldedItem(ItemStack *selected, ItemStack *hand = nullptr) const override;
|
||||||
bool setWieldedItem(const ItemStack &item) override;
|
bool setWieldedItem(const ItemStack &item) override;
|
||||||
|
|
|
@ -19,4 +19,5 @@ set(util_SRCS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/timetaker.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/timetaker.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/png.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/png.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/enum_string.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/enum_string.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/hotbar_source.cpp
|
||||||
PARENT_SCOPE)
|
PARENT_SCOPE)
|
||||||
|
|
71
src/util/hotbar_source.cpp
Normal file
71
src/util/hotbar_source.cpp
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Luanti
|
||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
// Copyright (C) 2025 cx384
|
||||||
|
|
||||||
|
#include "hotbar_source.h"
|
||||||
|
#include "serialize.h"
|
||||||
|
|
||||||
|
void HotbarSource::setHotbarItemcountLegacy(s32 count)
|
||||||
|
{
|
||||||
|
sources.clear();
|
||||||
|
sources.push_back({HOTBAR_INVENTORY_LIST_DEFAULT, (u16) count, 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
u16 HotbarSource::getMaxLength() const
|
||||||
|
{
|
||||||
|
u16 length = 0;
|
||||||
|
for (auto& source : sources) {
|
||||||
|
length += source.length;
|
||||||
|
}
|
||||||
|
return length ;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HotbarSource::getInventoryFromWieldIndex(u16 wield_index, std::string &list, u16 &index) const
|
||||||
|
{
|
||||||
|
for (auto& source : sources) {
|
||||||
|
if (wield_index < source.length) {
|
||||||
|
list = source.list;
|
||||||
|
index = wield_index + source.offset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
wield_index -= source.length;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
u16 HotbarSource::getLengthBefore(std::size_t index) const {
|
||||||
|
u16 length_before = 0;
|
||||||
|
for (std::size_t i = 0; i < index && i < sources.size(); i++) {
|
||||||
|
length_before += sources[i].length;
|
||||||
|
}
|
||||||
|
return length_before;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HotbarSource::serialize(std::ostream &os) const
|
||||||
|
{
|
||||||
|
writeU8(os, 0); // version
|
||||||
|
|
||||||
|
writeU16(os, sources.size());
|
||||||
|
for (const auto &source : sources) {
|
||||||
|
os << serializeString16(source.list);
|
||||||
|
writeU16(os, source.length);
|
||||||
|
writeU16(os, source.offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HotbarSource::deSerialize(std::istream &is)
|
||||||
|
{
|
||||||
|
int version = readU8(is);
|
||||||
|
if (version != 0)
|
||||||
|
throw SerializationError("unsupported HotbarSource version");
|
||||||
|
|
||||||
|
sources.clear();
|
||||||
|
u16 size = readU16(is);
|
||||||
|
for (u16 i = 0; i < size; i++) {
|
||||||
|
Source source;
|
||||||
|
source.list = deSerializeString16(is);
|
||||||
|
source.length = readU16(is);
|
||||||
|
source.offset = readU16(is);
|
||||||
|
sources.push_back(source);
|
||||||
|
}
|
||||||
|
}
|
52
src/util/hotbar_source.h
Normal file
52
src/util/hotbar_source.h
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Luanti
|
||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
// Copyright (C) 2025 cx384
|
||||||
|
|
||||||
|
// Stores a list of player inventories to be used by a hotbar
|
||||||
|
// Independent of the HUD element
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include "irrlichttypes.h"
|
||||||
|
|
||||||
|
#define HOTBAR_ITEMCOUNT_DEFAULT 8
|
||||||
|
#define HOTBAR_INVENTORY_LIST_DEFAULT "main"
|
||||||
|
#define HOTBAR_ITEMCOUNT_MAX 32
|
||||||
|
|
||||||
|
struct HotbarSource {
|
||||||
|
struct Source {
|
||||||
|
std::string list;
|
||||||
|
u16 length;
|
||||||
|
u16 offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Old functionality which could only use the "main" list
|
||||||
|
void setHotbarItemcountLegacy(s32 count);
|
||||||
|
|
||||||
|
// Returns the total length of all sources
|
||||||
|
u16 getMaxLength() const;
|
||||||
|
|
||||||
|
// Returns list and index of the inventory if it exists
|
||||||
|
bool getInventoryFromWieldIndex(u16 wield_index, std::string &list, u16 &index) const;
|
||||||
|
|
||||||
|
// Returns number of inventory slots before the source at index
|
||||||
|
u16 getLengthBefore(std::size_t index) const;
|
||||||
|
|
||||||
|
const std::vector<Source> getSources() const {
|
||||||
|
return sources;
|
||||||
|
};
|
||||||
|
|
||||||
|
void addSource(std::string_view list, u16 length, u16 offset) {
|
||||||
|
sources.push_back(Source{std::string{list}, length, offset});
|
||||||
|
};
|
||||||
|
|
||||||
|
void serialize(std::ostream &os) const;
|
||||||
|
void deSerialize(std::istream &is);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Source> sources;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue