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.
This commit is contained in:
Kelsi 2026-03-18 12:17:00 -07:00
parent 2e134b686d
commit 801f29f043
4 changed files with 40 additions and 2 deletions

View file

@ -949,6 +949,10 @@ public:
using StandStateCallback = std::function<void(uint8_t standState)>;
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()>;
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(bool isGhost)>;
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

View file

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

View file

@ -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<int32_t>(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<uint8_t>(val & 0xFF);
for (auto& ch : characters) {
if (ch.guid == playerGuid) {
ch.facialFeatures = facialHair;
break;
}
}
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
" bankBagSlots=", static_cast<int>(bankBagSlots));
LOG_DEBUG("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
" bankBagSlots=", static_cast<int>(bankBagSlots),
" facial=", static_cast<int>(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<uint8_t>((val >> 24) & 0xFF);
isResting_ = (restStateByte != 0);
if (appearanceChangedCallback_)
appearanceChangedCallback_();
}
else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) {
chosenTitleBit_ = static_cast<int32_t>(val);

View file

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