From 801f29f04335bdc9f0b8b2b7686085804c6a9294 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 12:17:00 -0700 Subject: [PATCH] fix: sync player appearance after barber shop or polymorph PLAYER_BYTES and PLAYER_BYTES_2 changes in SMSG_UPDATE_OBJECT now update the Character struct's appearanceBytes and facialFeatures, and fire an appearance-changed callback that resets the inventory screen preview so it reloads with the new hair/face values. --- include/game/game_handler.hpp | 5 +++++ include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 28 ++++++++++++++++++++++++++-- src/ui/game_screen.cpp | 8 ++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d0b6a1db..254a6d79 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -949,6 +949,10 @@ public: using StandStateCallback = std::function; void setStandStateCallback(StandStateCallback cb) { standStateCallback_ = std::move(cb); } + // Appearance changed callback — fired when PLAYER_BYTES or facial features update (barber shop, etc.) + using AppearanceChangedCallback = std::function; + void setAppearanceChangedCallback(AppearanceChangedCallback cb) { appearanceChangedCallback_ = std::move(cb); } + // Ghost state callback — fired when player enters or leaves ghost (spirit) form using GhostStateCallback = std::function; void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); } @@ -3348,6 +3352,7 @@ private: NpcAggroCallback npcAggroCallback_; NpcRespawnCallback npcRespawnCallback_; StandStateCallback standStateCallback_; + AppearanceChangedCallback appearanceChangedCallback_; GhostStateCallback ghostStateCallback_; MeleeSwingCallback meleeSwingCallback_; uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 72523cc6..4bc10707 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -665,6 +665,7 @@ private: float resurrectFlashTimer_ = 0.0f; static constexpr float kResurrectFlashDuration = 3.0f; bool ghostStateCallbackSet_ = false; + bool appearanceCallbackSet_ = false; bool ghostOpacityStateKnown_ = false; bool ghostOpacityLastState_ = false; uint32_t ghostOpacityLastInstanceId_ = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8980febd..301ca142 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -12177,6 +12177,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS); const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); + const uint16_t ufPBytesV = fieldIndex(UF::PLAYER_BYTES); const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2); const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE); const uint16_t ufStatsV[5] = { @@ -12227,15 +12228,38 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) { playerResistances_[key - ufArmor - 1] = static_cast(val); } + else if (ufPBytesV != 0xFFFF && key == ufPBytesV) { + // PLAYER_BYTES changed (barber shop, polymorph, etc.) + // Update the Character struct so inventory preview refreshes + for (auto& ch : characters) { + if (ch.guid == playerGuid) { + ch.appearanceBytes = val; + break; + } + } + if (appearanceChangedCallback_) + appearanceChangedCallback_(); + } else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) { + // Byte 0 (bits 0-7): facial hair / piercings + uint8_t facialHair = static_cast(val & 0xFF); + for (auto& ch : characters) { + if (ch.guid == playerGuid) { + ch.facialFeatures = facialHair; + break; + } + } uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); - LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, - " bankBagSlots=", static_cast(bankBagSlots)); + LOG_DEBUG("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, + " bankBagSlots=", static_cast(bankBagSlots), + " facial=", static_cast(facialHair)); inventory.setPurchasedBankBagSlots(bankBagSlots); // Byte 3 (bits 24-31): REST_STATE // 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY uint8_t restStateByte = static_cast((val >> 24) & 0xFF); isResting_ = (restStateByte != 0); + if (appearanceChangedCallback_) + appearanceChangedCallback_(); } else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) { chosenTitleBit_ = static_cast(val); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e09aed65..26c38c32 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -401,6 +401,14 @@ void GameScreen::render(game::GameHandler& gameHandler) { ghostStateCallbackSet_ = true; } + // Set up appearance-changed callback to refresh inventory preview (barber shop, etc.) + if (!appearanceCallbackSet_) { + gameHandler.setAppearanceChangedCallback([this]() { + inventoryScreenCharGuid_ = 0; // force preview re-sync on next frame + }); + appearanceCallbackSet_ = true; + } + // Set up UI error frame callback (once) if (!uiErrorCallbackSet_) { gameHandler.setUIErrorCallback([this](const std::string& msg) {