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;