mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
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:
parent
82e63fc95d
commit
fbeb14fc98
9 changed files with 156 additions and 15 deletions
|
|
@ -103,10 +103,14 @@ public:
|
||||||
const std::vector<Character>& getCharacters() const { return characters; }
|
const std::vector<Character>& getCharacters() const { return characters; }
|
||||||
|
|
||||||
void createCharacter(const CharCreateData& data);
|
void createCharacter(const CharCreateData& data);
|
||||||
|
void deleteCharacter(uint64_t characterGuid);
|
||||||
|
|
||||||
using CharCreateCallback = std::function<void(bool success, const std::string& message)>;
|
using CharCreateCallback = std::function<void(bool success, const std::string& message)>;
|
||||||
void setCharCreateCallback(CharCreateCallback cb) { charCreateCallback_ = std::move(cb); }
|
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
|
* Select and log in with a character
|
||||||
* @param characterGuid GUID of character to log in with
|
* @param characterGuid GUID of character to log in with
|
||||||
|
|
@ -601,6 +605,7 @@ private:
|
||||||
WorldConnectSuccessCallback onSuccess;
|
WorldConnectSuccessCallback onSuccess;
|
||||||
WorldConnectFailureCallback onFailure;
|
WorldConnectFailureCallback onFailure;
|
||||||
CharCreateCallback charCreateCallback_;
|
CharCreateCallback charCreateCallback_;
|
||||||
|
CharDeleteCallback charDeleteCallback_;
|
||||||
bool pendingCharCreateResult_ = false;
|
bool pendingCharCreateResult_ = false;
|
||||||
bool pendingCharCreateSuccess_ = false;
|
bool pendingCharCreateSuccess_ = false;
|
||||||
std::string pendingCharCreateMsg_;
|
std::string pendingCharCreateMsg_;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ enum class Opcode : uint16_t {
|
||||||
CMSG_AUTH_SESSION = 0x1ED,
|
CMSG_AUTH_SESSION = 0x1ED,
|
||||||
CMSG_CHAR_CREATE = 0x036,
|
CMSG_CHAR_CREATE = 0x036,
|
||||||
CMSG_CHAR_ENUM = 0x037,
|
CMSG_CHAR_ENUM = 0x037,
|
||||||
|
CMSG_CHAR_DELETE = 0x038,
|
||||||
CMSG_PLAYER_LOGIN = 0x03D,
|
CMSG_PLAYER_LOGIN = 0x03D,
|
||||||
|
|
||||||
// ---- Movement ----
|
// ---- Movement ----
|
||||||
|
|
@ -38,6 +39,7 @@ enum class Opcode : uint16_t {
|
||||||
SMSG_AUTH_RESPONSE = 0x1EE,
|
SMSG_AUTH_RESPONSE = 0x1EE,
|
||||||
SMSG_CHAR_CREATE = 0x03A,
|
SMSG_CHAR_CREATE = 0x03A,
|
||||||
SMSG_CHAR_ENUM = 0x03B,
|
SMSG_CHAR_ENUM = 0x03B,
|
||||||
|
SMSG_CHAR_DELETE = 0x03C,
|
||||||
SMSG_PONG = 0x1DD,
|
SMSG_PONG = 0x1DD,
|
||||||
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
||||||
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ public:
|
||||||
* @param info Movement info
|
* @param info Movement info
|
||||||
* @return Packet ready to send
|
* @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
|
// Forward declare Entity types
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void setOnCreateCharacter(std::function<void()> cb) { onCreateCharacter = std::move(cb); }
|
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
|
* Check if a character has been selected
|
||||||
|
|
@ -42,6 +44,11 @@ public:
|
||||||
*/
|
*/
|
||||||
uint64_t getSelectedGuid() const { return selectedCharacterGuid; }
|
uint64_t getSelectedGuid() const { return selectedCharacterGuid; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update status message
|
||||||
|
*/
|
||||||
|
void setStatus(const std::string& message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// UI state
|
// UI state
|
||||||
int selectedCharacterIndex = -1;
|
int selectedCharacterIndex = -1;
|
||||||
|
|
@ -54,11 +61,9 @@ private:
|
||||||
// Callbacks
|
// Callbacks
|
||||||
std::function<void(uint64_t)> onCharacterSelected;
|
std::function<void(uint64_t)> onCharacterSelected;
|
||||||
std::function<void()> onCreateCharacter;
|
std::function<void()> onCreateCharacter;
|
||||||
|
std::function<void()> onBack;
|
||||||
/**
|
std::function<void(uint64_t)> onDeleteCharacter;
|
||||||
* Update status message
|
bool confirmDelete = false;
|
||||||
*/
|
|
||||||
void setStatus(const std::string& message);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get faction color based on race
|
* Get faction color based on race
|
||||||
|
|
|
||||||
|
|
@ -640,6 +640,39 @@ void Application::setupUICallbacks() {
|
||||||
uiManager->getCharacterCreateScreen().initializePreview(assetManager.get());
|
uiManager->getCharacterCreateScreen().initializePreview(assetManager.get());
|
||||||
setState(AppState::CHARACTER_CREATION);
|
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() {
|
void Application::spawnPlayerCharacter() {
|
||||||
|
|
|
||||||
|
|
@ -894,6 +894,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleCharCreateResponse(packet);
|
handleCharCreateResponse(packet);
|
||||||
break;
|
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:
|
case Opcode::SMSG_CHAR_ENUM:
|
||||||
if (state == WorldState::CHAR_LIST_REQUESTED) {
|
if (state == WorldState::CHAR_LIST_REQUESTED) {
|
||||||
handleCharEnum(packet);
|
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 {
|
const Character* GameHandler::getActiveCharacter() const {
|
||||||
if (activeCharacterGuid_ == 0) return nullptr;
|
if (activeCharacterGuid_ == 0) return nullptr;
|
||||||
for (const auto& ch : characters) {
|
for (const auto& ch : characters) {
|
||||||
|
|
@ -2262,7 +2310,7 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
wireInfo.z = serverPos.z;
|
wireInfo.z = serverPos.z;
|
||||||
|
|
||||||
// Build and send movement packet
|
// Build and send movement packet
|
||||||
auto packet = MovementPacket::build(opcode, wireInfo);
|
auto packet = MovementPacket::build(opcode, wireInfo, playerGuid);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -517,16 +517,33 @@ bool PongParser::parse(network::Packet& packet, PongData& data) {
|
||||||
return true;
|
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));
|
network::Packet packet(static_cast<uint16_t>(opcode));
|
||||||
|
|
||||||
// Movement packet format (WoW 3.3.5a):
|
// Movement packet format (WoW 3.3.5a):
|
||||||
|
// packed GUID
|
||||||
// uint32 flags
|
// uint32 flags
|
||||||
// uint16 flags2
|
// uint16 flags2
|
||||||
// uint32 time
|
// uint32 time
|
||||||
// float x, y, z
|
// float x, y, z
|
||||||
// float orientation
|
// 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
|
// Write movement flags
|
||||||
packet.writeUInt32(info.flags);
|
packet.writeUInt32(info.flags);
|
||||||
packet.writeUInt16(info.flags2);
|
packet.writeUInt16(info.flags2);
|
||||||
|
|
|
||||||
|
|
@ -161,10 +161,29 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
// Display character GUID
|
// Delete Character button
|
||||||
std::stringstream guidStr;
|
if (!confirmDelete) {
|
||||||
guidStr << "GUID: 0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << character.guid;
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 1.0f));
|
||||||
ImGui::TextDisabled("%s", guidStr.str().c_str());
|
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();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Back/Refresh/Create buttons
|
// Back/Refresh/Create buttons
|
||||||
|
if (ImGui::Button("Back", ImVec2(120, 0))) {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
|
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
|
||||||
if (gameHandler.getState() == game::WorldState::READY ||
|
if (gameHandler.getState() == game::WorldState::READY ||
|
||||||
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
|
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
|
||||||
|
|
|
||||||
|
|
@ -263,18 +263,22 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
return;
|
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)
|
// 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);
|
renderEquipmentPanel(inventory);
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, 0.0f), true);
|
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, panelHeight), true);
|
||||||
renderBackpackPanel(inventory);
|
renderBackpackPanel(inventory);
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
ImGui::Separator();
|
// Money display
|
||||||
uint64_t gold = moneyCopper / 10000;
|
uint64_t gold = moneyCopper / 10000;
|
||||||
uint64_t silver = (moneyCopper / 100) % 100;
|
uint64_t silver = (moneyCopper / 100) % 100;
|
||||||
uint64_t copper = moneyCopper % 100;
|
uint64_t copper = moneyCopper % 100;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue