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.
This commit is contained in:
Kelsi 2026-02-06 03:24:46 -08:00
parent 82e63fc95d
commit fbeb14fc98
9 changed files with 156 additions and 15 deletions

View file

@ -103,10 +103,14 @@ public:
const std::vector<Character>& getCharacters() const { return characters; }
void createCharacter(const CharCreateData& data);
void deleteCharacter(uint64_t characterGuid);
using CharCreateCallback = std::function<void(bool success, const std::string& message)>;
void setCharCreateCallback(CharCreateCallback cb) { charCreateCallback_ = std::move(cb); }
using CharDeleteCallback = std::function<void(bool success)>;
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_;

View file

@ -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,

View file

@ -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

View file

@ -31,6 +31,8 @@ public:
}
void setOnCreateCharacter(std::function<void()> cb) { onCreateCharacter = std::move(cb); }
void setOnBack(std::function<void()> cb) { onBack = std::move(cb); }
void setOnDeleteCharacter(std::function<void(uint64_t)> 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<void(uint64_t)> onCharacterSelected;
std::function<void()> onCreateCharacter;
/**
* Update status message
*/
void setStatus(const std::string& message);
std::function<void()> onBack;
std::function<void(uint64_t)> onDeleteCharacter;
bool confirmDelete = false;
/**
* Get faction color based on race

View file

@ -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() {

View file

@ -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<sqlite3_int64>(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<sqlite3_int64>(characterGuid));
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
if (charDeleteCallback_) charDeleteCallback_(true);
return;
}
if (!socket) {
if (charDeleteCallback_) charDeleteCallback_(false);
return;
}
network::Packet packet(static_cast<uint16_t>(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);
}

View file

@ -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<uint16_t>(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<uint8_t>((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);

View file

@ -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) {

View file

@ -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;