diff --git a/doc/lua_api.md b/doc/lua_api.md index ec10458a73..b58d6b57f7 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -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 * `offset`: offset in pixels from position. * `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` @@ -8270,7 +8274,7 @@ child will follow movement and rotation of that bone. * `get_inventory()`: returns an `InvRef` for players, otherwise returns `nil` * `get_wield_list()`: returns the name of the inventory list the wielded item 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` * `set_wielded_item(item)`: replaces the wielded item, returns `true` if 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 * `count`: number of items, must be between `1` and `32` * If `count` exceeds the `"main"` list size, the list size will be used instead. -* `hud_get_hotbar_itemcount()`: returns number of visible items - * This value is also clamped by the `"main"` list size. + * equal `set_hotbar_source({{list = "main", length = count}})` +* `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)` * sets background image for hotbar * `hud_get_hotbar_image()`: returns texturename diff --git a/games/devtest/mods/testhud/init.lua b/games/devtest/mods/testhud/init.lua index 0512345f73..972ca32578 100644 --- a/games/devtest/mods/testhud/init.lua +++ b/games/devtest/mods/testhud/init.lua @@ -212,40 +212,144 @@ core.register_chatcommand("zoomfov", { local hud_hotbar_defs = { { - type = "hotbar", - position = {x=0.2, y=0.5}, - direction = 0, - alignment = {x=1, y=-1}, + { + type = "hotbar", + position = {x=0.2, y=0.5}, + direction = 1, + alignment = {x=1, y=-1}, + }, + { + type = "hotbar", + position = {x=0.2, y=0.5}, + direction = 3, + alignment = {x=1, y=1}, + }, + { + type = "hotbar", + position = {x=0.7, y=0.5}, + direction = 0, + offset = {x=140, y=20}, + alignment = {x=-1, y=-1}, + }, + { + type = "hotbar", + position = {x=0.7, y=0.5}, + direction = 2, + offset = {x=140, y=20}, + alignment = {x=-1, y=1}, + }, }, { - type = "hotbar", - position = {x=0.2, y=0.5}, - direction = 2, - 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, + }, }, { - type = "hotbar", - position = {x=0.7, y=0.5}, - direction = 0, - offset = {x=140, y=20}, - alignment = {x=-1, y=-1}, - }, - { - type = "hotbar", - position = {x=0.7, y=0.5}, - direction = 2, - offset = {x=140, y=20}, - alignment = {x=-1, y=1}, - }, + 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= {} core.register_chatcommand("hudhotbars", { description = "Shows some test Lua HUD elements of type hotbar. " .. - "(add: Adds elements (default). remove: Removes elements)", - params = "[ add | remove ]", - func = function(name, params) + "(Cycles between: none, aligned using all direction and offset," .. + "changed hotbar_source each only using a single inventory," .. + "using multiple inventories (world_pos.x = 0))", + func = function(name) local player = core.get_player_by_name(name) if not player then return false, "No player." @@ -253,22 +357,37 @@ core.register_chatcommand("hudhotbars", { local id_table = player_hud_hotbars[name] if not id_table then - id_table = {} + id_table = {mode = 0} player_hud_hotbars[name] = id_table end - if params == "remove" then - for _, id in ipairs(id_table) do - player:hud_remove(id) - end + id_table.mode = (id_table.mode + 1) % (#hud_hotbar_defs + 1) + + -- Reset + for _, id in ipairs(id_table) do + player:hud_remove(id) + end + player:hud_set_hotbar_itemcount(8) + player:hud_set_hotbar_image("") + + if id_table.mode == 0 then return true, "Hotbars removed." end - -- params == "add" or default - for _, def in ipairs(hud_hotbar_defs) do + for _, def in ipairs(hud_hotbar_defs[id_table.mode]) do table.insert(id_table, player:hud_add(def)) 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 }) diff --git a/games/devtest/mods/unittests/player.lua b/games/devtest/mods/unittests/player.lua index f8945f3201..77fc7fe869 100644 --- a/games/devtest/mods/unittests/player.lua +++ b/games/devtest/mods/unittests/player.lua @@ -183,9 +183,32 @@ end 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 old_inv_size = inv:get_size("main") 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) 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) 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_list("main", old_inv_list) player:hud_set_hotbar_itemcount(old_bar_size) 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}) diff --git a/src/client/game.cpp b/src/client/game.cpp index c2513fd9b5..59b75bf3bf 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -1957,7 +1957,7 @@ void Game::processItemSelection(u16 *new_playeritem) LocalPlayer *player = client->getEnv().getLocalPlayer(); *new_playeritem = player->getWieldIndex(); - u16 max_item = player->getMaxHotbarItemcount(); + u16 max_item = player->hotbar_source.getMaxLength(); if (max_item == 0) return; max_item -= 1; @@ -2009,9 +2009,14 @@ void Game::dropSelectedItem(bool single_item) IDropAction *a = new IDropAction(); a->count = single_item ? 1 : 0; a->from_inv.setCurrentPlayer(); - a->from_list = "main"; - a->from_i = client->getEnv().getLocalPlayer()->getWieldIndex(); - client->inventoryAction(a); + + 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); + } } void Game::openConsole(float scale, const wchar_t *line) diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 47ef56039c..f13b764428 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -229,14 +229,37 @@ void Hud::drawItem(const ItemStack &item, const core::rect& rect, client, selected ? IT_ROT_SELECTED : IT_ROT_NONE); } -// NOTE: selectitem = 0 -> no selected; selectitem is 1-based -// mainlist can be NULL, but draw the frame anyway. -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) +void Hud::drawItems(const v2s32& pos, s32 inv_size, s32 inv_offset, InventoryList *mainlist, + u16 selectitem, u16 direction, bool is_hotbar, u16 hotbar_touchcontrol_offset) { - s32 height = m_hotbar_imagesize + m_padding * 2; - s32 width = (itemcount - inv_offset) * (m_hotbar_imagesize + m_padding * 2); + // 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 items + core::rect 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 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) { 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 - v2s32 pos(screen_offset.X * m_scale_factor, screen_offset.Y * m_scale_factor); - pos += screen_pos; - pos.X += (alignment.X - 1.0f) * (width * 0.5f); - pos.Y += (alignment.Y - 1.0f) * (height * 0.5f); + pos.X = screen_pos.X + screen_offset.X * m_scale_factor + + (alignment.X - 1.0f) * (width * 0.5f); + pos.Y = screen_pos.Y + screen_offset.Y * m_scale_factor + + (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 +// arguments are the number of items before and the remaining number of items +v2s32 Hud::getInventoryPosOffset(u16 direction, s32 before, s32 remainder) +{ + s32 fullimglen = m_hotbar_imagesize + m_padding * 2; + + v2s32 steppos; + switch (direction) { + case HUD_DIR_RIGHT_LEFT: + steppos = v2s32((remainder - before) * fullimglen, 0); + break; + case HUD_DIR_TOP_BOTTOM: + steppos = v2s32(0, before * fullimglen); + break; + case HUD_DIR_BOTTOM_TOP: + steppos = v2s32(0, (remainder - before) * fullimglen); + break; + default: + steppos = v2s32(before * fullimglen, 0); + break; + } + + return steppos; +} + +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(); } - // 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(); + if (!use_hotbar_image) { + return; } - // draw customized item background - if (use_hotbar_image) { - core::rect imgrect2(-m_padding/2, -m_padding/2, - width+m_padding/2, height+m_padding/2); - core::rect rect2 = imgrect2 + pos; - video::ITexture *texture = tsrc->getTexture(hotbar_image); - core::dimension2di imgsize(texture->getOriginalSize()); - draw2DImageFilterScaled(driver, texture, rect2, + core::rect imgrect2(-m_padding/2, -m_padding/2, width+m_padding/2, height+m_padding/2); + core::rect rect2 = imgrect2 + pos; + video::ITexture *texture = tsrc->getTexture(hotbar_image); + core::dimension2di imgsize(texture->getOriginalSize()); + draw2DImageFilterScaled(driver, texture, rect2, core::rect(core::position2d(0,0), imgsize), - NULL, hbar_colors, true); - } + nullptr, hbar_colors, true); +} - // Draw items - core::rect 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; +// 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); - v2s32 steppos; - switch (direction) { - case HUD_DIR_RIGHT_LEFT: - steppos = v2s32(m_padding + (list_max - 1 - i - inv_offset) * fullimglen, m_padding); - break; - case HUD_DIR_TOP_BOTTOM: - steppos = v2s32(m_padding, m_padding + (i - inv_offset) * fullimglen); - break; - case HUD_DIR_BOTTOM_TOP: - steppos = v2s32(m_padding, m_padding + (list_max - 1 - i - inv_offset) * fullimglen); - break; - default: - steppos = v2s32(m_padding + (i - inv_offset) * fullimglen, m_padding); - break; + 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; } - core::rect item_rect = imgrect + pos + steppos; + 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; - drawItem(mainlist->getItem(i), item_rect, (i + 1) == selectitem); + 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; - if (is_hotbar && g_touchcontrols) - g_touchcontrols->registerHotbarRect(i, item_rect); + } + } 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); if (!inv) warningstream << "HUD: Unknown inventory list. name=" << e->text << std::endl; - drawItems(pos, v2s32(e->offset.X, e->offset.Y), e->number, e->align, 0, - inv, e->item, e->dir, false); + drawInventory(pos, e->offset, e->number, e->align, inv, e->item, e->dir); break; } case HUD_ELEM_WAYPOINT: { if (!calculateScreenPos(camera_offset, e, &pos)) @@ -567,7 +673,25 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) client->getMinimap()->drawMinimap(rect); break; } 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; } default: 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() { diff --git a/src/client/hud.h b/src/client/hud.h index e2cf757de8..cb9cc65627 100644 --- a/src/client/hud.h +++ b/src/client/hud.h @@ -62,7 +62,6 @@ public: void disableBlockBounds(); void drawBlockBounds(); - void drawHotbar(const v2s32 &pos, const v2f &offset, u16 direction, const v2f &align); void resizeHotbar(); void drawCrosshair(); void drawSelectionMesh(); @@ -104,11 +103,20 @@ private: const std::string &texture, const std::string& bgtexture, 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 &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, const core::rect &rect, int way); diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 88969f008b..22afe7421e 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -11,6 +11,7 @@ #include "hud.h" #include "log_internal.h" #include "client/renderingengine.h" +#include "util/hotbar_source.h" // HOTBAR_ITEMCOUNT_MAX void KeyCache::populate_nonchanging() { @@ -69,7 +70,7 @@ void KeyCache::populate() key[KeyType::QUICKTUNE_INC] = getKeySetting("keymap_quicktune_inc"); 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); key[KeyType::SLOT_1 + i] = getKeySetting(slot_key_name.c_str()); } diff --git a/src/hud.h b/src/hud.h index 2b02e99883..582088879f 100644 --- a/src/hud.h +++ b/src/hud.h @@ -35,12 +35,10 @@ #define HUD_FLAG_BASIC_DEBUG (1 << 7) #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_SELECTED_IMAGE 3 - -#define HUD_HOTBAR_ITEMCOUNT_DEFAULT 8 -#define HUD_HOTBAR_ITEMCOUNT_MAX 32 +#define HUD_PARAM_HOTBAR_SOURCE 4 #define HOTBAR_IMAGE_SIZE 48 diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index bb1930d960..e81d3e88a0 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -1264,18 +1264,27 @@ void Client::handleCommand_HudSetParam(NetworkPacket* pkt) *pkt >> param >> value; 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()); - if (hotbar_itemcount > 0 && hotbar_itemcount <= HUD_HOTBAR_ITEMCOUNT_MAX) - player->hud_hotbar_itemcount = hotbar_itemcount; - } - else if (param == HUD_PARAM_HOTBAR_IMAGE) { + player->hotbar_source.setHotbarItemcountLegacy(hotbar_itemcount); + break; } + case HUD_PARAM_HOTBAR_IMAGE: player->hotbar_image = value; - } - else if (param == HUD_PARAM_HOTBAR_SELECTED_IMAGE) { + break; + case HUD_PARAM_HOTBAR_SELECTED_IMAGE: player->hotbar_selected_image = value; + break; + case HUD_PARAM_HOTBAR_SOURCE: { + std::istringstream is(value); + player->hotbar_source.deSerialize(is); + break; } + default: + break; } } diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp index d1cc245d73..b7875e1f68 100644 --- a/src/network/networkprotocol.cpp +++ b/src/network/networkprotocol.cpp @@ -64,6 +64,7 @@ [scheduled bump for 5.11.0] PROTOCOL VERSION 48 Add compression to some existing packets + Send HUD_PARAM_HOTBAR_SOURCE instead of HUD_PARAM_HOTBAR_ITEMCOUNT [scheduled bump for 5.12.0] */ diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index 17ba8ca211..123b632c3c 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -819,11 +819,11 @@ void Server::handleCommand_PlayerItem(NetworkPacket* pkt) *pkt >> item; - if (item >= player->getMaxHotbarItemcount()) { + if (item >= player->hotbar_source.getMaxLength()) { actionstream << "Player " << player->getName() << " tried to access item=" << item - << " out of hotbar_itemcount=" - << player->getMaxHotbarItemcount() + << " while max=" + << player->hotbar_source.getMaxLength() << "; ignoring." << std::endl; return; } @@ -916,11 +916,11 @@ void Server::handleCommand_Interact(NetworkPacket *pkt) // Update wielded item - if (item_i >= player->getMaxHotbarItemcount()) { + if (item_i >= player->hotbar_source.getMaxLength()) { actionstream << "Player " << player->getName() << " tried to access item=" << item_i - << " out of hotbar_itemcount=" - << player->getMaxHotbarItemcount() + << " while max=" + << player->hotbar_source.getMaxLength() << "; ignoring." << std::endl; return; } diff --git a/src/player.cpp b/src/player.cpp index 10f66ec11d..79cbe0e551 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -72,7 +72,7 @@ Player::Player(const std::string &name, IItemDefManager *idef): HUD_FLAG_MINIMAP_RADAR_VISIBLE | HUD_FLAG_BASIC_DEBUG | HUD_FLAG_CHAT_VISIBLE; - hud_hotbar_itemcount = HUD_HOTBAR_ITEMCOUNT_DEFAULT; + hotbar_source.addSource(HOTBAR_INVENTORY_LIST_DEFAULT, HOTBAR_ITEMCOUNT_DEFAULT, 0); } Player::~Player() @@ -82,25 +82,27 @@ Player::~Player() void Player::setWieldIndex(u16 index) { - const InventoryList *mlist = inventory.getList("main"); - m_wield_index = MYMIN(index, mlist ? mlist->getSize() : 0); + m_wield_index = MYMIN(index, hotbar_source.getMaxLength() - 1); } 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 { 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"); - - if (mlist && m_wield_index < mlist->getSize()) - *selected = mlist->getItem(m_wield_index); - if (hand && hlist) *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() { bool a_up = direction_keys & (1 << 0), diff --git a/src/player.h b/src/player.h index 80a3c98c13..96b850c78b 100644 --- a/src/player.h +++ b/src/player.h @@ -12,6 +12,7 @@ #include #include #include +#include "util/hotbar_source.h" #define PLAYERNAME_SIZE 20 @@ -228,10 +229,7 @@ public: void clearHud(); u32 hud_flags; - s32 hud_hotbar_itemcount; - - // Get actual usable number of hotbar items (clamped to size of "main" list) - u16 getMaxHotbarItemcount(); + HotbarSource hotbar_source; protected: std::string m_name; diff --git a/src/remoteplayer.h b/src/remoteplayer.h index 1f2f8df9c3..5b1f0c4b1b 100644 --- a/src/remoteplayer.h +++ b/src/remoteplayer.h @@ -35,12 +35,8 @@ public: RemotePlayerChatResult canSendChatMessage(); - void setHotbarItemcount(s32 hotbar_itemcount) - { - hud_hotbar_itemcount = hotbar_itemcount; - } - - s32 getHotbarItemcount() const { return hud_hotbar_itemcount; } + void setHotbarSource(const HotbarSource& source) { hotbar_source = source; } + const HotbarSource& getHotbarSource() const { return hotbar_source; } void overrideDayNightRatio(bool do_override, float ratio) { diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 816f428571..02eb8811a8 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -1933,7 +1933,7 @@ int ObjectRef::l_hud_set_hotbar_itemcount(lua_State *L) s32 hotbar_itemcount = luaL_checkint(L, 2); - if (!getServer(L)->hudSetHotbarItemcount(player, hotbar_itemcount)) + if (!getServer(L)->hudSetHotbarItemcountLegacy(player, hotbar_itemcount)) return 0; lua_pushboolean(L, true); @@ -1949,7 +1949,71 @@ int ObjectRef::l_hud_get_hotbar_itemcount(lua_State *L) if (player == nullptr) 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(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(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; } @@ -2905,6 +2969,8 @@ luaL_Reg ObjectRef::methods[] = { luamethod(ObjectRef, hud_get_flags), luamethod(ObjectRef, hud_set_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_get_hotbar_image), luamethod(ObjectRef, hud_set_hotbar_selected_image), diff --git a/src/script/lua_api/l_object.h b/src/script/lua_api/l_object.h index 43415f5efb..727bd7e5bc 100644 --- a/src/script/lua_api/l_object.h +++ b/src/script/lua_api/l_object.h @@ -314,6 +314,12 @@ private: // hud_get_hotbar_itemcount(self) 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) static int l_hud_set_hotbar_image(lua_State *L); diff --git a/src/server.cpp b/src/server.cpp index af6fed3a0f..820a655ca2 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -64,6 +64,7 @@ #include "particles.h" #include "gettext.h" #include "util/tracy_wrapper.h" +#include "util/hotbar_source.h" class ClientNotFoundException : public BaseException { @@ -3451,21 +3452,44 @@ bool Server::hudSetFlags(RemotePlayer *player, u32 flags, u32 mask) return true; } -bool Server::hudSetHotbarItemcount(RemotePlayer *player, s32 hotbar_itemcount) +bool Server::hudSetHotbarItemcountLegacy(RemotePlayer *player, s32 hotbar_itemcount) { if (!player) return false; - if (hotbar_itemcount <= 0 || hotbar_itemcount > HUD_HOTBAR_ITEMCOUNT_MAX) + if (hotbar_itemcount <= 0 || hotbar_itemcount > HOTBAR_ITEMCOUNT_MAX) return false; - if (player->getHotbarItemcount() == hotbar_itemcount) - return true; + InventoryList *mainlist = player->inventory.getList("main"); + hotbar_itemcount = mainlist ? MYMIN(mainlist->getSize(), (u32) hotbar_itemcount) : 0; + + player->hotbar_source.setHotbarItemcountLegacy(hotbar_itemcount); + + if (player->protocol_version < 48) { + std::ostringstream os(std::ios::binary); + writeS32(os, hotbar_itemcount); + 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); - player->setHotbarItemcount(hotbar_itemcount); std::ostringstream os(std::ios::binary); - writeS32(os, hotbar_itemcount); - SendHUDSetParam(player->getPeerId(), HUD_PARAM_HOTBAR_ITEMCOUNT, os.str()); + source.serialize(os); + SendHUDSetParam(player->getPeerId(), HUD_PARAM_HOTBAR_SOURCE, os.str()); return true; } diff --git a/src/server.h b/src/server.h index c9869e1ddf..f899ec3600 100644 --- a/src/server.h +++ b/src/server.h @@ -17,6 +17,7 @@ #include "util/thread.h" #include "util/basic_macros.h" #include "util/metricsbackend.h" +#include "util/hotbar_source.h" #include "serverenvironment.h" #include "server/clientiface.h" #include "threading/ordered_mutex.h" @@ -364,7 +365,8 @@ public: bool hudRemove(RemotePlayer *player, u32 id); bool hudChange(RemotePlayer *player, u32 id, HudElementStat stat, void *value); 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 hudSetHotbarSelectedImage(RemotePlayer *player, const std::string &name); diff --git a/src/server/player_sao.cpp b/src/server/player_sao.cpp index 11fc155976..3af3d2bf0f 100644 --- a/src/server/player_sao.cpp +++ b/src/server/player_sao.cpp @@ -582,9 +582,13 @@ ItemStack PlayerSAO::getWieldedItem(ItemStack *selected, ItemStack *hand) const bool PlayerSAO::setWieldedItem(const ItemStack &item) { - InventoryList *mlist = m_player->inventory.getList(getWieldList()); - if (mlist) { - mlist->changeItem(m_player->getWieldIndex(), item); + std::string list; + u16 index; + 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 false; diff --git a/src/server/player_sao.h b/src/server/player_sao.h index 0ce26f7cca..81fbb2589a 100644 --- a/src/server/player_sao.h +++ b/src/server/player_sao.h @@ -117,7 +117,6 @@ public: Inventory *getInventory() const override; InventoryLocation getInventoryLocation() const override; void setInventoryModified() override {} - std::string getWieldList() const override { return "main"; } u16 getWieldIndex() const override; ItemStack getWieldedItem(ItemStack *selected, ItemStack *hand = nullptr) const override; bool setWieldedItem(const ItemStack &item) override; diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 7d5ac48fb0..e695789f8e 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -19,4 +19,5 @@ set(util_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/timetaker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/png.cpp ${CMAKE_CURRENT_SOURCE_DIR}/enum_string.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/hotbar_source.cpp PARENT_SCOPE) diff --git a/src/util/hotbar_source.cpp b/src/util/hotbar_source.cpp new file mode 100644 index 0000000000..ca6f5ecdda --- /dev/null +++ b/src/util/hotbar_source.cpp @@ -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); + } +} diff --git a/src/util/hotbar_source.h b/src/util/hotbar_source.h new file mode 100644 index 0000000000..1f97cdf271 --- /dev/null +++ b/src/util/hotbar_source.h @@ -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 +#include +#include +#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 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 sources; +}; +