From e8a4a7402f13facff2fc3bc8bc70296ecc382d00 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 30 Mar 2026 15:48:30 -0700 Subject: [PATCH] fix: clamp player percentage stats, add scale field why-comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - entity_controller: clamp block/dodge/parry/crit/rangedCrit percentage fields to [0..100] after memcpy from update fields — guards against NaN/Inf from corrupted packets reaching the UI renderer - entity_controller: add why-comment on OBJECT_FIELD_SCALE_X raw==0 check — IEEE 754 0.0f is all-zero bits, so raw==0 means the field was never populated; keeping default 1.0f prevents invisible entities --- src/game/entity_controller.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/game/entity_controller.cpp b/src/game/entity_controller.cpp index 7022e9e7..6f74f46e 100644 --- a/src/game/entity_controller.cpp +++ b/src/game/entity_controller.cpp @@ -1015,11 +1015,14 @@ bool EntityController::applyPlayerStatFields(const std::map& owner_.playerSpellDmgBonus_[key - pfi.spDmg1] = static_cast(val); } else if (pfi.healBonus != 0xFFFF && key == pfi.healBonus) { owner_.playerHealBonus_ = static_cast(val); } - else if (pfi.blockPct != 0xFFFF && key == pfi.blockPct) { std::memcpy(&owner_.playerBlockPct_, &val, 4); } - else if (pfi.dodgePct != 0xFFFF && key == pfi.dodgePct) { std::memcpy(&owner_.playerDodgePct_, &val, 4); } - else if (pfi.parryPct != 0xFFFF && key == pfi.parryPct) { std::memcpy(&owner_.playerParryPct_, &val, 4); } - else if (pfi.critPct != 0xFFFF && key == pfi.critPct) { std::memcpy(&owner_.playerCritPct_, &val, 4); } - else if (pfi.rangedCritPct != 0xFFFF && key == pfi.rangedCritPct) { std::memcpy(&owner_.playerRangedCritPct_, &val, 4); } + // Percentage stats are stored as IEEE 754 floats packed into uint32 update fields. + // memcpy reinterprets the bits; clamp to [0..100] to guard against NaN/Inf from + // corrupted packets reaching the UI (display-only, no gameplay logic depends on these). + else if (pfi.blockPct != 0xFFFF && key == pfi.blockPct) { std::memcpy(&owner_.playerBlockPct_, &val, 4); owner_.playerBlockPct_ = std::clamp(owner_.playerBlockPct_, 0.0f, 100.0f); } + else if (pfi.dodgePct != 0xFFFF && key == pfi.dodgePct) { std::memcpy(&owner_.playerDodgePct_, &val, 4); owner_.playerDodgePct_ = std::clamp(owner_.playerDodgePct_, 0.0f, 100.0f); } + else if (pfi.parryPct != 0xFFFF && key == pfi.parryPct) { std::memcpy(&owner_.playerParryPct_, &val, 4); owner_.playerParryPct_ = std::clamp(owner_.playerParryPct_, 0.0f, 100.0f); } + else if (pfi.critPct != 0xFFFF && key == pfi.critPct) { std::memcpy(&owner_.playerCritPct_, &val, 4); owner_.playerCritPct_ = std::clamp(owner_.playerCritPct_, 0.0f, 100.0f); } + else if (pfi.rangedCritPct != 0xFFFF && key == pfi.rangedCritPct) { std::memcpy(&owner_.playerRangedCritPct_, &val, 4); owner_.playerRangedCritPct_ = std::clamp(owner_.playerRangedCritPct_, 0.0f, 100.0f); } else if (pfi.sCrit1 != 0xFFFF && key >= pfi.sCrit1 && key < pfi.sCrit1 + 7) { std::memcpy(&owner_.playerSpellCritPct_[key - pfi.sCrit1], &val, 4); } @@ -1072,6 +1075,8 @@ void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType, float unitScale = 1.0f; uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); if (scaleIdx != 0xFFFF) { + // raw == 0 means the field was never populated (IEEE 754 0.0f is all-zero bits). + // Keep the default 1.0f rather than setting scale to 0 and making the entity invisible. uint32_t raw = entity->getField(scaleIdx); if (raw != 0) { std::memcpy(&unitScale, &raw, sizeof(float));