From fdc149f31606a2f9c2bf572a7ecc9176c7b19476 Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Sun, 22 Jun 2025 22:06:47 +0200 Subject: [PATCH] Formspec: Show a player inventory using core.show_formspec (#15963) 'core.show_formspec' now shows and updates the inventory formspec as if it was opened using the hotkey on client-side. --- doc/lua_api.md | 18 +++++++++++++----- src/client/game.cpp | 15 ++++++++++----- src/client/game_formspec.cpp | 29 ++++++++++++++++++++--------- src/client/game_formspec.h | 4 +++- src/client/localplayer.h | 2 ++ src/network/clientpackethandler.cpp | 1 + src/network/serveropcodes.cpp | 1 + src/remoteplayer.h | 2 ++ src/script/lua_api/l_object.cpp | 4 +++- src/server.cpp | 6 ++++-- 10 files changed, 59 insertions(+), 23 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index ae0533d7e..b34e571c2 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6872,10 +6872,15 @@ Formspec * `core.show_formspec(playername, formname, formspec)` * `playername`: name of player to show formspec * `formname`: name passed to `on_player_receive_fields` callbacks. - It should follow the `"modname:"` naming convention. - * `formname` must not be empty, unless you want to reshow - the inventory formspec without updating it for future opens. + * It should follow the `"modname:"` naming convention. + * If empty: Shows a custom, temporary inventory formspec. + * An inventory formspec shown this way will also be updated if + `ObjectRef:set_inventory_formspec` is called. + * Use `ObjectRef:set_inventory_formspec` to change the player's + inventory formspec for future opens. + * Supported if server AND client are both of version >= 5.13.0. * `formspec`: formspec to display + * See also: `core.register_on_player_receive_fields` * `core.close_formspec(playername, formname)` * `playername`: name of player to close formspec * `formname`: has to exactly match the one given in `show_formspec`, or the @@ -8653,9 +8658,12 @@ child will follow movement and rotation of that bone. * Returns `nil` if no attribute found. * `get_meta()`: Returns metadata associated with the player (a PlayerMetaRef). * `set_inventory_formspec(formspec)` - * Redefine player's inventory form - * Should usually be called in `on_joinplayer` + * Redefines the player's inventory formspec. + * Should usually be called at least once in the `on_joinplayer` callback. * If `formspec` is `""`, the player's inventory is disabled. + * If the inventory formspec is currently open on the client, it is + updated immediately. + * See also: `core.register_on_player_receive_fields` * `get_inventory_formspec()`: returns a formspec string * `set_formspec_prepend(formspec)`: * the formspec string will be added to every formspec shown to the user, diff --git a/src/client/game.cpp b/src/client/game.cpp index f9a52c43f..ebfc3f1c8 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -1925,7 +1925,7 @@ void Game::processKeyInput() if (g_settings->getBool("continuous_forward")) toggleAutoforward(); } else if (wasKeyDown(KeyType::INVENTORY)) { - m_game_formspec.showPlayerInventory(); + m_game_formspec.showPlayerInventory(nullptr); } else if (input->cancelPressed()) { #ifdef __ANDROID__ m_android_chat_open = false; @@ -2714,11 +2714,16 @@ void Game::handleClientEvent_DeathscreenLegacy(ClientEvent *event, CameraOrienta void Game::handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam) { - m_game_formspec.showFormSpec(*event->show_formspec.formspec, - *event->show_formspec.formname); + auto &fs = event->show_formspec; - delete event->show_formspec.formspec; - delete event->show_formspec.formname; + if (fs.formname->empty() && !fs.formspec->empty()) { + m_game_formspec.showPlayerInventory(fs.formspec); + } else { + m_game_formspec.showFormSpec(*fs.formspec, *fs.formname); + } + + delete fs.formspec; + delete fs.formname; } void Game::handleClientEvent_ShowCSMFormSpec(ClientEvent *event, CameraOrientation *cam) diff --git a/src/client/game_formspec.cpp b/src/client/game_formspec.cpp index 3d8dc6fc8..407b1d9d9 100644 --- a/src/client/game_formspec.cpp +++ b/src/client/game_formspec.cpp @@ -178,6 +178,10 @@ public: const std::string &getForm() const { LocalPlayer *player = m_client->getEnv().getLocalPlayer(); + + if (!player->inventory_formspec_override.empty()) + return player->inventory_formspec_override; + return player->inventory_formspec; } @@ -304,7 +308,7 @@ void GameFormSpec::showNodeFormspec(const std::string &formspec, const v3s16 &no m_formspec->setFormSpec(formspec, inventoryloc); } -void GameFormSpec::showPlayerInventory() +void GameFormSpec::showPlayerInventory(const std::string *fs_override) { /* * Don't permit to open inventory is CAO or player doesn't exists. @@ -317,28 +321,35 @@ void GameFormSpec::showPlayerInventory() infostream << "Game: Launching inventory" << std::endl; - PlayerInventoryFormSource *fs_src = new PlayerInventoryFormSource(m_client); + auto fs_src = std::make_unique(m_client); InventoryLocation inventoryloc; inventoryloc.setCurrentPlayer(); - if (m_client->modsLoaded() && m_client->getScript()->on_inventory_open(m_client->getInventory(inventoryloc))) { - delete fs_src; - return; + if (fs_override) { + // Temporary overwrite for this specific formspec. + player->inventory_formspec_override = *fs_override; + } else { + // Show the regular inventory formspec + player->inventory_formspec_override.clear(); } - if (fs_src->getForm().empty()) { - delete fs_src; + // If prevented by Client-Side Mods + if (m_client->modsLoaded() && m_client->getScript()->on_inventory_open(m_client->getInventory(inventoryloc))) + return; + + // Empty formspec -> do not show. + if (fs_src->getForm().empty()) return; - } TextDest *txt_dst = new TextDestPlayerInventory(m_client); GUIFormSpecMenu::create(m_formspec, m_client, m_rendering_engine->get_gui_env(), - &m_input->joystick, fs_src, txt_dst, m_client->getFormspecPrepend(), + &m_input->joystick, fs_src.get(), txt_dst, m_client->getFormspecPrepend(), m_client->getSoundManager()); m_formspec->setFormSpec(fs_src->getForm(), inventoryloc); + fs_src.release(); // owned by GUIFormSpecMenu } #define SIZE_TAG "size[11,5.5,true]" // Fixed size (ignored in touchscreen mode) diff --git a/src/client/game_formspec.h b/src/client/game_formspec.h index 980dac47f..8ad3059af 100644 --- a/src/client/game_formspec.h +++ b/src/client/game_formspec.h @@ -34,7 +34,9 @@ struct GameFormSpec // Currently only used for the in-game settings menu. void showPauseMenuFormSpec(const std::string &formspec, const std::string &formname); void showNodeFormspec(const std::string &formspec, const v3s16 &nodepos); - void showPlayerInventory(); + /// If `!fs_override`: Uses `player->inventory_formspec`. + /// If ` fs_override`: Uses a temporary formspec until an update is received. + void showPlayerInventory(const std::string *fs_override); void showDeathFormspecLegacy(); // Shows the hardcoded "main" pause menu. void showPauseMenu(); diff --git a/src/client/localplayer.h b/src/client/localplayer.h index 93b768ceb..2f108dcce 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -99,6 +99,8 @@ public: std::string hotbar_image = ""; std::string hotbar_selected_image = ""; + /// Temporary player inventory formspec. Empty value = feature inactive. + std::string inventory_formspec_override; video::SColor light_color = video::SColor(255, 255, 255, 255); diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index bb1930d96..6cd7150c6 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -902,6 +902,7 @@ void Client::handleCommand_InventoryFormSpec(NetworkPacket* pkt) // Store formspec in LocalPlayer player->inventory_formspec = pkt->readLongString(); + player->inventory_formspec_override.clear(); } void Client::handleCommand_DetachedInventory(NetworkPacket* pkt) diff --git a/src/network/serveropcodes.cpp b/src/network/serveropcodes.cpp index b50e13082..f75e1f5cd 100644 --- a/src/network/serveropcodes.cpp +++ b/src/network/serveropcodes.cpp @@ -178,6 +178,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_STOP_SOUND", 0, true }, // 0x40 { "TOCLIENT_PRIVILEGES", 0, true }, // 0x41 { "TOCLIENT_INVENTORY_FORMSPEC", 0, true }, // 0x42 + // ^ `channel` MUST be the same as TOCLIENT_SHOW_FORMSPEC { "TOCLIENT_DETACHED_INVENTORY", 0, true }, // 0x43 { "TOCLIENT_SHOW_FORMSPEC", 0, true }, // 0x44 { "TOCLIENT_MOVEMENT", 0, true }, // 0x45 diff --git a/src/remoteplayer.h b/src/remoteplayer.h index 1f2f8df9c..1f56bc517 100644 --- a/src/remoteplayer.h +++ b/src/remoteplayer.h @@ -121,6 +121,8 @@ public: u16 protocol_version = 0; u16 formspec_version = 0; + bool inventory_formspec_overridden = false; + /// returns PEER_ID_INEXISTENT when PlayerSAO is not ready session_t getPeerId() const { return m_peer_id; } diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 19c513dd3..eed2215ac 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -1574,7 +1574,9 @@ int ObjectRef::l_set_inventory_formspec(lua_State *L) auto formspec = readParam(L, 2); - if (formspec != player->inventory_formspec) { + if (player->inventory_formspec_overridden + || formspec != player->inventory_formspec) { + player->inventory_formspec_overridden = false; player->inventory_formspec = formspec; getServer(L)->reportInventoryFormspecModified(player->getName()); } diff --git a/src/server.cpp b/src/server.cpp index e3854802d..16434f447 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1595,11 +1595,10 @@ void Server::SendShowFormspecMessage(session_t peer_id, const std::string &forms (it->second == formname || formname.empty())) { m_formspec_state_data.erase(peer_id); } - pkt.putLongString(""); } else { m_formspec_state_data[peer_id] = formname; - pkt.putLongString(formspec); } + pkt.putLongString(formspec); pkt << formname; Send(&pkt); @@ -3397,6 +3396,9 @@ bool Server::showFormspec(const char *playername, const std::string &formspec, if (!player) return false; + // To allow re-sending the same inventory formspec. + player->inventory_formspec_overridden = formname.empty() && !formspec.empty(); + SendShowFormspecMessage(player->getPeerId(), formspec, formname); return true; }