From fbeb14fc9807f83c026650eaed839e1a5e3efed7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 03:24:46 -0800 Subject: [PATCH] Add movement packed GUID, inventory money display, and character screen buttons Fix movement packets to include packed player GUID prefix so the server tracks position. Fix inventory money display being clipped by child panels. Add Back and Delete Character buttons to character selection screen with two-step delete confirmation. --- include/game/game_handler.hpp | 5 ++++ include/game/opcodes.hpp | 2 ++ include/game/world_packets.hpp | 2 +- include/ui/character_screen.hpp | 15 ++++++---- src/core/application.cpp | 33 ++++++++++++++++++++++ src/game/game_handler.cpp | 50 ++++++++++++++++++++++++++++++++- src/game/world_packets.cpp | 19 ++++++++++++- src/ui/character_screen.cpp | 35 ++++++++++++++++++++--- src/ui/inventory_screen.cpp | 10 +++++-- 9 files changed, 156 insertions(+), 15 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index fd14631c..618786df 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -103,10 +103,14 @@ public: const std::vector& getCharacters() const { return characters; } void createCharacter(const CharCreateData& data); + void deleteCharacter(uint64_t characterGuid); using CharCreateCallback = std::function; void setCharCreateCallback(CharCreateCallback cb) { charCreateCallback_ = std::move(cb); } + using CharDeleteCallback = std::function; + void setCharDeleteCallback(CharDeleteCallback cb) { charDeleteCallback_ = std::move(cb); } + /** * Select and log in with a character * @param characterGuid GUID of character to log in with @@ -601,6 +605,7 @@ private: WorldConnectSuccessCallback onSuccess; WorldConnectFailureCallback onFailure; CharCreateCallback charCreateCallback_; + CharDeleteCallback charDeleteCallback_; bool pendingCharCreateResult_ = false; bool pendingCharCreateSuccess_ = false; std::string pendingCharCreateMsg_; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 414fa20f..d4d2161b 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -14,6 +14,7 @@ enum class Opcode : uint16_t { CMSG_AUTH_SESSION = 0x1ED, CMSG_CHAR_CREATE = 0x036, CMSG_CHAR_ENUM = 0x037, + CMSG_CHAR_DELETE = 0x038, CMSG_PLAYER_LOGIN = 0x03D, // ---- Movement ---- @@ -38,6 +39,7 @@ enum class Opcode : uint16_t { SMSG_AUTH_RESPONSE = 0x1EE, SMSG_CHAR_CREATE = 0x03A, SMSG_CHAR_ENUM = 0x03B, + SMSG_CHAR_DELETE = 0x03C, SMSG_PONG = 0x1DD, SMSG_LOGIN_VERIFY_WORLD = 0x236, SMSG_ACCOUNT_DATA_TIMES = 0x209, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 645d02dd..331d2b54 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -429,7 +429,7 @@ public: * @param info Movement info * @return Packet ready to send */ - static network::Packet build(Opcode opcode, const MovementInfo& info); + static network::Packet build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid = 0); }; // Forward declare Entity types diff --git a/include/ui/character_screen.hpp b/include/ui/character_screen.hpp index 3cb038a9..e9a9b7f1 100644 --- a/include/ui/character_screen.hpp +++ b/include/ui/character_screen.hpp @@ -31,6 +31,8 @@ public: } void setOnCreateCharacter(std::function cb) { onCreateCharacter = std::move(cb); } + void setOnBack(std::function cb) { onBack = std::move(cb); } + void setOnDeleteCharacter(std::function cb) { onDeleteCharacter = std::move(cb); } /** * Check if a character has been selected @@ -42,6 +44,11 @@ public: */ uint64_t getSelectedGuid() const { return selectedCharacterGuid; } + /** + * Update status message + */ + void setStatus(const std::string& message); + private: // UI state int selectedCharacterIndex = -1; @@ -54,11 +61,9 @@ private: // Callbacks std::function onCharacterSelected; std::function onCreateCharacter; - - /** - * Update status message - */ - void setStatus(const std::string& message); + std::function onBack; + std::function onDeleteCharacter; + bool confirmDelete = false; /** * Get faction color based on race diff --git a/src/core/application.cpp b/src/core/application.cpp index 3e1af01a..cd0bdad5 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -640,6 +640,39 @@ void Application::setupUICallbacks() { uiManager->getCharacterCreateScreen().initializePreview(assetManager.get()); setState(AppState::CHARACTER_CREATION); }); + + // "Back" button on character screen + uiManager->getCharacterScreen().setOnBack([this]() { + if (singlePlayerMode) { + setState(AppState::AUTHENTICATION); + singlePlayerMode = false; + gameHandler->setSinglePlayerMode(false); + } else { + setState(AppState::REALM_SELECTION); + } + }); + + // "Delete Character" button on character screen + uiManager->getCharacterScreen().setOnDeleteCharacter([this](uint64_t guid) { + if (gameHandler) { + gameHandler->deleteCharacter(guid); + } + }); + + // Character delete result callback + gameHandler->setCharDeleteCallback([this](bool success) { + if (success) { + uiManager->getCharacterScreen().setStatus("Character deleted."); + // Refresh character list + if (singlePlayerMode) { + gameHandler->setSinglePlayerCharListReady(); + } else { + gameHandler->requestCharacterList(); + } + } else { + uiManager->getCharacterScreen().setStatus("Delete failed."); + } + }); } void Application::spawnPlayerCharacter() { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fe774da4..b9fda259 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -894,6 +894,15 @@ void GameHandler::handlePacket(network::Packet& packet) { handleCharCreateResponse(packet); break; + case Opcode::SMSG_CHAR_DELETE: { + uint8_t result = packet.readUInt8(); + bool success = (result == 0x47); // CHAR_DELETE_SUCCESS + LOG_INFO("SMSG_CHAR_DELETE result: ", (int)result, success ? " (success)" : " (failed)"); + if (success) requestCharacterList(); + if (charDeleteCallback_) charDeleteCallback_(success); + break; + } + case Opcode::SMSG_CHAR_ENUM: if (state == WorldState::CHAR_LIST_REQUESTED) { handleCharEnum(packet); @@ -1393,6 +1402,45 @@ void GameHandler::handleCharCreateResponse(network::Packet& packet) { } } +void GameHandler::deleteCharacter(uint64_t characterGuid) { + if (singlePlayerMode_) { + // Remove from local list + characters.erase( + std::remove_if(characters.begin(), characters.end(), + [characterGuid](const Character& c) { return c.guid == characterGuid; }), + characters.end()); + // Remove from database + auto& sp = getSinglePlayerSqlite(); + if (sp.db) { + const char* sql = "DELETE FROM characters WHERE guid=?"; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(sp.db, sql, -1, &stmt, nullptr) == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, static_cast(characterGuid)); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + const char* sql2 = "DELETE FROM character_inventory WHERE guid=?"; + if (sqlite3_prepare_v2(sp.db, sql2, -1, &stmt, nullptr) == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, static_cast(characterGuid)); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + } + if (charDeleteCallback_) charDeleteCallback_(true); + return; + } + + if (!socket) { + if (charDeleteCallback_) charDeleteCallback_(false); + return; + } + + network::Packet packet(static_cast(Opcode::CMSG_CHAR_DELETE)); + packet.writeUInt64(characterGuid); + socket->send(packet); + LOG_INFO("CMSG_CHAR_DELETE sent for GUID: 0x", std::hex, characterGuid, std::dec); +} + const Character* GameHandler::getActiveCharacter() const { if (activeCharacterGuid_ == 0) return nullptr; for (const auto& ch : characters) { @@ -2262,7 +2310,7 @@ void GameHandler::sendMovement(Opcode opcode) { wireInfo.z = serverPos.z; // Build and send movement packet - auto packet = MovementPacket::build(opcode, wireInfo); + auto packet = MovementPacket::build(opcode, wireInfo, playerGuid); socket->send(packet); } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index bffc5fff..ac8730cb 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -517,16 +517,33 @@ bool PongParser::parse(network::Packet& packet, PongData& data) { return true; } -network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info) { +network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) { network::Packet packet(static_cast(opcode)); // Movement packet format (WoW 3.3.5a): + // packed GUID // uint32 flags // uint16 flags2 // uint32 time // float x, y, z // float orientation + // Write packed GUID + uint8_t mask = 0; + uint8_t guidBytes[8]; + int guidByteCount = 0; + for (int i = 0; i < 8; i++) { + uint8_t byte = static_cast((playerGuid >> (i * 8)) & 0xFF); + if (byte != 0) { + mask |= (1 << i); + guidBytes[guidByteCount++] = byte; + } + } + packet.writeUInt8(mask); + for (int i = 0; i < guidByteCount; i++) { + packet.writeUInt8(guidBytes[i]); + } + // Write movement flags packet.writeUInt32(info.flags); packet.writeUInt16(info.flags2); diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index d4a3fb58..b907ce39 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -161,10 +161,29 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::SameLine(); - // Display character GUID - std::stringstream guidStr; - guidStr << "GUID: 0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << character.guid; - ImGui::TextDisabled("%s", guidStr.str().c_str()); + // Delete Character button + if (!confirmDelete) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button("Delete Character", ImVec2(150, 40))) { + confirmDelete = true; + } + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.0f, 0.0f, 1.0f)); + if (ImGui::Button("Confirm Delete?", ImVec2(150, 40))) { + if (onDeleteCharacter) { + onDeleteCharacter(character.guid); + } + confirmDelete = false; + selectedCharacterIndex = -1; + selectedCharacterGuid = 0; + } + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 40))) { + confirmDelete = false; + } + } } } @@ -173,6 +192,14 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::Spacing(); // Back/Refresh/Create buttons + if (ImGui::Button("Back", ImVec2(120, 0))) { + if (onBack) { + onBack(); + } + } + + ImGui::SameLine(); + if (ImGui::Button("Refresh", ImVec2(120, 0))) { if (gameHandler.getState() == game::WorldState::READY || gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) { diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 90b4ab19..973577bf 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -263,18 +263,22 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { return; } + // Reserve space for money display at bottom + float moneyHeight = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y; + float panelHeight = ImGui::GetContentRegionAvail().y - moneyHeight; + // Two-column layout: Equipment (left) | Backpack (right) - ImGui::BeginChild("EquipPanel", ImVec2(200.0f, 0.0f), true); + ImGui::BeginChild("EquipPanel", ImVec2(200.0f, panelHeight), true); renderEquipmentPanel(inventory); ImGui::EndChild(); ImGui::SameLine(); - ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, 0.0f), true); + ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, panelHeight), true); renderBackpackPanel(inventory); ImGui::EndChild(); - ImGui::Separator(); + // Money display uint64_t gold = moneyCopper / 10000; uint64_t silver = (moneyCopper / 100) % 100; uint64_t copper = moneyCopper % 100;