From 5bfe4b61aad2172b4d3a4ba69b6622dd2907313e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 14:21:50 -0800 Subject: [PATCH] Track player skills from update fields and display in character screen Extract skill data from PLAYER_SKILL_INFO_1_1 update fields (636-1019), detect skill increases with chat messages, and replace placeholder Skills tab with live data grouped by category with progress bars. --- include/game/game_handler.hpp | 20 ++++++++ src/game/game_handler.cpp | 97 ++++++++++++++++++++++++++++++++++- src/ui/inventory_screen.cpp | 82 +++++++++++++++++++++-------- 3 files changed, 175 insertions(+), 24 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index fd81f37e..706dfb55 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -14,12 +14,19 @@ #include #include #include +#include namespace wowee { namespace network { class WorldSocket; class Packet; } namespace game { +struct PlayerSkill { + uint32_t skillId = 0; + uint16_t value = 0; + uint16_t maxValue = 0; +}; + /** * Quest giver status values (WoW 3.3.5a) */ @@ -343,6 +350,11 @@ public: uint32_t getPlayerLevel() const { return serverPlayerLevel_; } static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel); + // Player skills + const std::map& getPlayerSkills() const { return playerSkills_; } + const std::string& getSkillName(uint32_t skillId) const; + uint32_t getSkillCategory(uint32_t skillId) const; + // World entry callback (online mode - triggered when entering world) // Parameters: mapId, x, y, z (canonical WoW coordinates) using WorldEntryCallback = std::function; @@ -804,6 +816,14 @@ private: uint32_t serverPlayerLevel_ = 1; static uint32_t xpForLevel(uint32_t level); + // ---- Player skills ---- + std::map playerSkills_; + std::unordered_map skillLineNames_; + std::unordered_map skillLineCategories_; + bool skillLineDbcLoaded_ = false; + void loadSkillLineDbc(); + void extractSkillFields(const std::map& fields); + NpcDeathCallback npcDeathCallback_; NpcRespawnCallback npcRespawnCallback_; MeleeSwingCallback meleeSwingCallback_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 78c36d6b..5aa88707 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3,6 +3,9 @@ #include "network/world_socket.hpp" #include "network/packet.hpp" #include "core/coordinates.hpp" +#include "core/application.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" #include #include @@ -1251,7 +1254,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } - // Extract XP / inventory slot fields for player entity + // Extract XP / inventory slot / skill fields for player entity if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { lastPlayerFields_ = block.fields; detectInventorySlotBases(block.fields); @@ -1269,6 +1272,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); + extractSkillFields(lastPlayerFields_); } break; } @@ -1331,7 +1335,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } } - // Update XP / inventory slot fields for player entity + // Update XP / inventory slot / skill fields for player entity if (block.guid == playerGuid) { for (const auto& [key, val] : block.fields) { lastPlayerFields_[key] = val; @@ -1365,6 +1369,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); + extractSkillFields(lastPlayerFields_); } // Update item stack count for online items @@ -3967,5 +3972,93 @@ void GameHandler::fail(const std::string& reason) { } +// ============================================================ +// Player Skills +// ============================================================ + +static const std::string kEmptySkillName; + +const std::string& GameHandler::getSkillName(uint32_t skillId) const { + auto it = skillLineNames_.find(skillId); + return (it != skillLineNames_.end()) ? it->second : kEmptySkillName; +} + +uint32_t GameHandler::getSkillCategory(uint32_t skillId) const { + auto it = skillLineCategories_.find(skillId); + return (it != skillLineCategories_.end()) ? it->second : 0; +} + +void GameHandler::loadSkillLineDbc() { + if (skillLineDbcLoaded_) return; + skillLineDbcLoaded_ = true; + + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + + auto dbc = am->loadDBC("SkillLine.dbc"); + if (!dbc || !dbc->isLoaded()) { + LOG_WARNING("GameHandler: Could not load SkillLine.dbc"); + return; + } + + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + uint32_t id = dbc->getUInt32(i, 0); + uint32_t category = dbc->getUInt32(i, 1); + std::string name = dbc->getString(i, 3); + if (id > 0 && !name.empty()) { + skillLineNames_[id] = name; + skillLineCategories_[id] = category; + } + } + LOG_INFO("GameHandler: Loaded ", skillLineNames_.size(), " skill line names"); +} + +void GameHandler::extractSkillFields(const std::map& fields) { + loadSkillLineDbc(); + + // PLAYER_SKILL_INFO_1_1 = field 636, 128 slots x 3 fields each (636..1019) + static constexpr uint16_t PLAYER_SKILL_INFO_START = 636; + static constexpr int MAX_SKILL_SLOTS = 128; + + std::map newSkills; + + for (int slot = 0; slot < MAX_SKILL_SLOTS; slot++) { + uint16_t baseField = PLAYER_SKILL_INFO_START + slot * 3; + + auto idIt = fields.find(baseField); + if (idIt == fields.end()) continue; + + uint32_t raw0 = idIt->second; + uint16_t skillId = raw0 & 0xFFFF; + if (skillId == 0) continue; + + auto valIt = fields.find(baseField + 1); + if (valIt == fields.end()) continue; + + uint32_t raw1 = valIt->second; + uint16_t value = raw1 & 0xFFFF; + uint16_t maxValue = (raw1 >> 16) & 0xFFFF; + + PlayerSkill skill; + skill.skillId = skillId; + skill.value = value; + skill.maxValue = maxValue; + newSkills[skillId] = skill; + } + + // Detect increases and emit chat messages + for (const auto& [skillId, skill] : newSkills) { + if (skill.value == 0) continue; + auto oldIt = playerSkills_.find(skillId); + if (oldIt != playerSkills_.end() && skill.value > oldIt->second.value) { + const std::string& name = getSkillName(skillId); + std::string skillName = name.empty() ? ("Skill #" + std::to_string(skillId)) : name; + addSystemChatMessage("Your skill in " + skillName + " has increased to " + std::to_string(skill.value) + "."); + } + } + + playerSkills_ = std::move(newSkills); +} + } // namespace game } // namespace wowee diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 4beb8c64..4b4da531 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -704,32 +704,70 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { } if (ImGui::BeginTabItem("Skills")) { - uint32_t level = gameHandler.getPlayerLevel(); - uint32_t cap = (level > 0) ? (level * 5) : 0; - ImGui::TextDisabled("Skills (online sync pending)"); - ImGui::Spacing(); - if (ImGui::BeginTable("SkillsTable", 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH)) { - ImGui::TableSetupColumn("Skill", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableHeadersRow(); - - const char* skills[] = { - "Unarmed", "Swords", "Axes", "Maces", "Daggers", - "Staves", "Polearms", "Bows", "Guns", "Crossbows" + const auto& skills = gameHandler.getPlayerSkills(); + if (skills.empty()) { + ImGui::TextDisabled("No skill data received yet."); + } else { + // Group skills by SkillLine.dbc category + struct CategoryGroup { + const char* label; + uint32_t categoryId; }; - for (const char* skill : skills) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%s", skill); - ImGui::TableSetColumnIndex(1); - if (cap > 0) { - ImGui::Text("-- / %u", cap); - } else { - ImGui::TextDisabled("--"); + static const CategoryGroup groups[] = { + { "Weapon Skills", 6 }, + { "Armor Skills", 8 }, + { "Secondary Skills", 10 }, + { "Professions", 11 }, + { "Languages", 9 }, + { "Other", 0 }, + }; + + ImGui::BeginChild("##SkillsList", ImVec2(0, 0), true); + + for (const auto& group : groups) { + // Collect skills for this category + std::vector groupSkills; + for (const auto& [id, skill] : skills) { + if (skill.value == 0 && skill.maxValue == 0) continue; + uint32_t cat = gameHandler.getSkillCategory(id); + if (group.categoryId == 0) { + // "Other" catches everything not in the named categories + if (cat != 6 && cat != 8 && cat != 9 && cat != 10 && cat != 11) { + groupSkills.push_back(&skill); + } + } else if (cat == group.categoryId) { + groupSkills.push_back(&skill); + } + } + if (groupSkills.empty()) continue; + + if (ImGui::CollapsingHeader(group.label, ImGuiTreeNodeFlags_DefaultOpen)) { + for (const game::PlayerSkill* skill : groupSkills) { + const std::string& name = gameHandler.getSkillName(skill->skillId); + char label[128]; + if (name.empty()) { + snprintf(label, sizeof(label), "Skill #%u", skill->skillId); + } else { + snprintf(label, sizeof(label), "%s", name.c_str()); + } + + // Show progress bar with value/max overlay + float ratio = (skill->maxValue > 0) + ? static_cast(skill->value) / static_cast(skill->maxValue) + : 0.0f; + + char overlay[64]; + snprintf(overlay, sizeof(overlay), "%u / %u", skill->value, skill->maxValue); + + ImGui::Text("%s", label); + ImGui::SameLine(180.0f); + ImGui::SetNextItemWidth(-1.0f); + ImGui::ProgressBar(ratio, ImVec2(0, 14.0f), overlay); + } } } - ImGui::EndTable(); + ImGui::EndChild(); } ImGui::EndTabItem(); }