From c91315d9100f5e721b5f138f0155e9546b9451e2 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 17:56:55 -0700 Subject: [PATCH 01/20] Jugpet timer overflow UB fix --- src/map/entities/charentity.cpp | 2 +- src/map/entities/petentity.cpp | 4 ++-- src/map/items/item_usable.cpp | 4 ++-- src/map/utils/charutils.cpp | 19 +++++++++++++++---- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index 8c62dada929..eb9f0e0d64b 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -619,7 +619,7 @@ void CCharEntity::resetPetZoningInfo() petZoningInfo.petMP = 0; petZoningInfo.respawnPet = false; petZoningInfo.petType = PET_TYPE::AVATAR; - petZoningInfo.jugSpawnTime = timer::time_point::min(); + petZoningInfo.jugSpawnTime = timer::time_point{}; petZoningInfo.jugDuration = 0s; } diff --git a/src/map/entities/petentity.cpp b/src/map/entities/petentity.cpp index e54544c1942..7561ab018b8 100644 --- a/src/map/entities/petentity.cpp +++ b/src/map/entities/petentity.cpp @@ -51,8 +51,8 @@ CPetEntity::CPetEntity(PET_TYPE petType) , m_PetID(0) , m_PetType(petType) , m_spawnLevel(0) -, m_jugSpawnTime(timer::time_point::min()) -, m_jugDuration(timer::duration::min()) +, m_jugSpawnTime(timer::time_point{}) +, m_jugDuration(timer::duration{}) { TracyZoneScoped; objtype = TYPE_PET; diff --git a/src/map/items/item_usable.cpp b/src/map/items/item_usable.cpp index b173a87cc98..6bd41e92e41 100644 --- a/src/map/items/item_usable.cpp +++ b/src/map/items/item_usable.cpp @@ -37,8 +37,8 @@ CItemUsable::CItemUsable(uint16 id) m_MaxCharges = 0; m_Animation = 0; m_ValidTarget = 0; - m_AssignTime = timer::time_point::min(); - m_LastUseTime = timer::time_point::min(); + m_AssignTime = timer::time_point{}; + m_LastUseTime = timer::time_point{}; m_AoE = 0; } diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index 91c2175468f..6dd1e885c72 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -1072,7 +1072,10 @@ void LoadInventory(CCharEntity* PChar) { uint32 useTime = 0; std::memcpy(&useTime, PItemUsable->m_extra + 0x04, sizeof(useTime)); - PItemUsable->setLastUseTime(timer::now() - std::chrono::seconds(earth_time::vanadiel_timestamp() - useTime)); + if (useTime != 0) + { + PItemUsable->setLastUseTime(timer::now() - std::chrono::seconds(earth_time::vanadiel_timestamp() - useTime)); + } } if (PItem->isType(ITEM_FURNISHING) && (PItem->getLocationID() == LOC_MOGSAFE || PItem->getLocationID() == LOC_MOGSAFE2)) @@ -6043,9 +6046,17 @@ void SaveCharStats(CCharEntity* PChar) // These two are jug only variables. We should probably move pet char stats into its own table, but in the meantime // we use charvars for jug specific things - const auto jugTimestamp = earth_time::timestamp(timer::to_utc(PChar->petZoningInfo.jugSpawnTime)); - PChar->setCharVar("jugpet-spawn-time", jugTimestamp); - PChar->setCharVar("jugpet-duration-seconds", static_cast(timer::count_seconds(PChar->petZoningInfo.jugDuration))); + if (PChar->petZoningInfo.jugSpawnTime > timer::time_point{}) + { + const auto jugTimestamp = earth_time::timestamp(timer::to_utc(PChar->petZoningInfo.jugSpawnTime)); + PChar->setCharVar("jugpet-spawn-time", jugTimestamp); + PChar->setCharVar("jugpet-duration-seconds", static_cast(timer::count_seconds(PChar->petZoningInfo.jugDuration))); + } + else + { + PChar->setCharVar("jugpet-spawn-time", 0); + PChar->setCharVar("jugpet-duration-seconds", 0); + } } /************************************************************************ From c0fd767087ad535df568b5b45c211aa3ace82930 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 17:59:09 -0700 Subject: [PATCH 02/20] Dynamic TargId capacity can be 0 during testing --- src/map/map_networking.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/map/map_networking.cpp b/src/map/map_networking.cpp index da135c52af9..2843de7dd61 100644 --- a/src/map/map_networking.cpp +++ b/src/map/map_networking.cpp @@ -122,7 +122,9 @@ void MapNetworking::tapStatistics() mapStatistics_.set(MapStatistics::Key::ActiveMobs, mobCount); mapStatistics_.set(MapStatistics::Key::TaskManagerTasks, CTaskManager::getInstance()->getTaskList().size()); - const auto percent = (static_cast(dynamicTargIdCount) / static_cast(dynamicTargIdCapacity)) * 100.0; + const auto percent = dynamicTargIdCapacity > 0 + ? static_cast(dynamicTargIdCount) / static_cast(dynamicTargIdCapacity) * 100.0 + : 0.0; mapStatistics_.set(MapStatistics::Key::DynamicTargIdUsagePercent, static_cast(percent)); // Clear statistics From 9d84026a5e3f5caf857431c7d20e251559949840 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:04:17 -0700 Subject: [PATCH 03/20] Remove clearPath/pathThrough unnecessary cast --- src/map/lua/lua_baseentity.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 32960581929..7e2bc9e6cb3 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -1978,9 +1978,7 @@ bool CLuaBaseEntity::pathThrough(const sol::table& pointsTable, const sol::objec } } - CBattleEntity* PBattle = (CBattleEntity*)m_PBaseEntity; - - return PBattle->PAI->PathFind->PathThrough(std::move(points), flags); + return m_PBaseEntity->PAI->PathFind->PathThrough(std::move(points), flags); } /************************************************************************ @@ -2009,17 +2007,16 @@ bool CLuaBaseEntity::isFollowingPath() void CLuaBaseEntity::clearPath(const sol::object& pauseObj) { - auto* PBattle = static_cast(m_PBaseEntity); - bool pause = pauseObj.is() ? pauseObj.as() : false; + bool pause = pauseObj.is() ? pauseObj.as() : false; // Stop onPath ticks for NPCs if this is true if (m_PBaseEntity->objtype == TYPE_NPC && pause) { m_PBaseEntity->SetLocalVar("pauseNPCPathing", 1); } - else if (PBattle->PAI->PathFind != nullptr) + else if (m_PBaseEntity->PAI->PathFind != nullptr) { - PBattle->PAI->PathFind->Clear(); + m_PBaseEntity->PAI->PathFind->Clear(); } } From b459d0a24153410d1d478acc1f1e4bd6f19ee31c Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:05:28 -0700 Subject: [PATCH 04/20] Remove GetEntityWeapon cast to CMobEntity This can be called on many type of entities which are not necessarily mobs --- src/map/utils/battleutils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index 5c7b8a7dd8c..575cc4aaaac 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -3838,7 +3838,7 @@ CItemWeapon* GetEntityWeapon(CBattleEntity* PEntity, SLOTTYPE Slot) return nullptr; } - return dynamic_cast(((CMobEntity*)PEntity)->m_Weapons[Slot]); + return dynamic_cast(PEntity->m_Weapons[Slot]); } void MakeEntityStandUp(CBattleEntity* PEntity) From ce1d37049717681c03d618fe39ce762917cc9687 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:07:13 -0700 Subject: [PATCH 05/20] SELL_SET use-after-free when logging sale --- src/map/packets/c2s/0x085_shop_sell_set.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/map/packets/c2s/0x085_shop_sell_set.cpp b/src/map/packets/c2s/0x085_shop_sell_set.cpp index 80f86518d13..b78ef388df6 100644 --- a/src/map/packets/c2s/0x085_shop_sell_set.cpp +++ b/src/map/packets/c2s/0x085_shop_sell_set.cpp @@ -111,7 +111,8 @@ void GP_CLI_COMMAND_SHOP_SELL_SET::process(MapSession* PSession, CCharEntity* PC return; } - const auto cost = quantity * PItem->getBasePrice(); + const auto basePrice = PItem->getBasePrice(); + const auto cost = quantity * basePrice; if (charutils::UpdateItem(PChar, LOC_INVENTORY, slotId, -static_cast(quantity)) == 0) { ShowWarningFmt("GP_CLI_COMMAND_SHOP_SELL_SET: Player {} failed to remove item ID {} from inventory!", PChar->getName(), PItem->getID()); @@ -119,7 +120,7 @@ void GP_CLI_COMMAND_SHOP_SELL_SET::process(MapSession* PSession, CCharEntity* PC } charutils::UpdateItem(PChar, LOC_INVENTORY, 0, cost); - auditSale(PChar, itemId, quantity, PItem->getBasePrice()); + auditSale(PChar, itemId, quantity, basePrice); ShowInfo("GP_CLI_COMMAND_SHOP_SELL_SET: Player '%s' sold %u of itemID %u (Total: %u gil) [to VENDOR] ", PChar->getName(), quantity, itemId, cost); PChar->pushPacket(nullptr, itemId, quantity, MsgStd::Sell); PChar->pushPacket(); From 2c5f69ce30d43dce65a9c857be51a3a7a5d12ebf Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:16:18 -0700 Subject: [PATCH 06/20] Guard against vector reallocation mid-swing --- src/map/entities/battleentity.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index a166a3a54fb..a5da02c6f31 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -3100,6 +3100,7 @@ bool CBattleEntity::OnAttack(CAttackState& state, action_t& action) battleutils::HandleParrySpikesDamage(this, PTarget, &actionResult, attack.GetDamage()); } + const auto currentAttackType = attack.GetAttackType(); // try zanshin only on single swing attack rounds - it is last priority in the multi-hit order if (attack.IsFirstSwing() && attackRound.GetAttackSwingCount() == 1) { @@ -3120,12 +3121,12 @@ bool CBattleEntity::OnAttack(CAttackState& state, action_t& action) } // Remove shuriken if Daken proc and Sange is up - if (attack.GetAttackType() == PHYSICAL_ATTACK_TYPE::DAKEN) + if (currentAttackType == PHYSICAL_ATTACK_TYPE::DAKEN) { if (StatusEffectContainer && StatusEffectContainer->HasStatusEffect(EFFECT_SANGE)) { - CCharEntity* PChar = dynamic_cast(this); - CItemWeapon* PAmmo = dynamic_cast(PChar->getEquip(SLOT_AMMO)); + auto* PChar = dynamic_cast(this); + const auto* PAmmo = dynamic_cast(PChar->getEquip(SLOT_AMMO)); if (PChar && PAmmo && PAmmo->isShuriken()) // Not sure how they wouldn't have a shuriken by this point, but just in case... { From 12bd8120028d73097c2af74d9fef838085ff1286 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:22:04 -0700 Subject: [PATCH 07/20] Track last attacker by ID rather than pointer Certain codepaths are referencing possibly dangling pointers when executing in timers. Sidesteps the issue by refetching the entity at consumption time. --- src/map/entities/battleentity.cpp | 19 +++++++++++++------ src/map/entities/battleentity.h | 2 +- src/map/entities/charentity.cpp | 2 +- src/map/entities/mobentity.cpp | 2 +- src/map/lua/lua_baseentity.cpp | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index a5da02c6f31..fe3f53c3ab3 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -76,10 +76,9 @@ CBattleEntity::CBattleEntity() std::memset(&health, 0, sizeof(health)); health.maxhp = 1; - PPet = nullptr; - PParty = nullptr; - PMaster = nullptr; - PLastAttacker = nullptr; + PPet = nullptr; + PParty = nullptr; + PMaster = nullptr; StatusEffectContainer = std::make_unique(this); PRecastContainer = std::make_unique(this); @@ -901,7 +900,15 @@ int32 CBattleEntity::addMP(int32 mp) int32 CBattleEntity::takeDamage(int32 amount, CBattleEntity* attacker /* = nullptr*/, ATTACK_TYPE attackType /* = ATTACK_NONE*/, DAMAGE_TYPE damageType /* = DAMAGE_NONE*/, bool isSkillchainDamage /* = false */) { TracyZoneScoped; - PLastAttacker = attacker; + if (attacker) + { + lastAttackerId_.id = attacker->id; + lastAttackerId_.targid = attacker->targid; + } + else + { + lastAttackerId_.clean(); + } this->BattleHistory.lastHitTaken_atkType = attackType; PAI->EventHandler.triggerListener("TAKE_DAMAGE", this, amount, attacker, (uint16)attackType, (uint16)damageType); @@ -914,7 +921,7 @@ int32 CBattleEntity::takeDamage(int32 amount, CBattleEntity* attacker /* = nullp roeutils::event(ROE_EVENT::ROE_DMGTAKEN, static_cast(this), RoeDatagram("dmg", amount)); } } - else if (PLastAttacker && PLastAttacker->objtype == TYPE_PC) + else if (attacker && attacker->objtype == TYPE_PC) { if (amount > 0) { diff --git a/src/map/entities/battleentity.h b/src/map/entities/battleentity.h index 4d34b0240b4..d0f16adcea4 100644 --- a/src/map/entities/battleentity.h +++ b/src/map/entities/battleentity.h @@ -551,7 +551,7 @@ class CBattleEntity : public CBaseEntity CParty* PParty; CBattleEntity* PPet; CBattleEntity* PMaster; // Owner/owner of the entity (applies to all combat entities) - CBattleEntity* PLastAttacker; + EntityID_t lastAttackerId_{}; timer::time_point LastAttacked; battlehistory_t BattleHistory{}; // Stores info related to most recent combat actions taken towards this entity. diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index eb9f0e0d64b..36b686ff14a 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -2554,7 +2554,7 @@ void CCharEntity::Die() { TracyZoneScoped; - if (PLastAttacker) + if (auto* PLastAttacker = GetEntity(lastAttackerId_.targid); PLastAttacker && PLastAttacker->id == lastAttackerId_.id) { loc.zone->PushPacket(this, CHAR_INRANGE_SELF, std::make_unique(PLastAttacker, this, 0, 0, MsgBasic::PLAYER_DEFEATED_BY)); } diff --git a/src/map/entities/mobentity.cpp b/src/map/entities/mobentity.cpp index 7fee02c5369..b8a3cd1a599 100644 --- a/src/map/entities/mobentity.cpp +++ b/src/map/entities/mobentity.cpp @@ -1252,7 +1252,7 @@ void CMobEntity::Die() { if (static_cast(PEntity)->isDead()) { - if (PLastAttacker) + if (auto* PLastAttacker = GetEntity(lastAttackerId_.targid); PLastAttacker && PLastAttacker->id == lastAttackerId_.id) { loc.zone->PushPacket(this, CHAR_INRANGE, std::make_unique(PLastAttacker, this, 0, 0, MsgBasic::DEFEATS_TARG)); } diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 7e2bc9e6cb3..d43d2309667 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -9958,7 +9958,7 @@ void CLuaBaseEntity::setHP(int32 value) // When setting the HP to 0 the entity "falls to the ground" so the last attacker needs to be cleared if (value == 0) { - PBattle->PLastAttacker = nullptr; + PBattle->lastAttackerId_.clean(); } } From b0e804df5393d3012d47fcb977255d4001a74230 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:32:38 -0700 Subject: [PATCH 08/20] updateEnmityFromDamage bad downcast fix Base entity is not necessarily a mob --- src/map/lua/lua_baseentity.cpp | 14 +++++++------- src/map/lua/lua_baseentity.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index d43d2309667..4e134627c1c 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -13293,26 +13293,26 @@ void CLuaBaseEntity::transferEnmity(CLuaBaseEntity* entity, uint8 percent, float * Notes : Used in most Weaponskills and damaging abilities scripts ************************************************************************/ -void CLuaBaseEntity::updateEnmityFromDamage(CLuaBaseEntity* PEntity, int32 damage) +void CLuaBaseEntity::updateEnmityFromDamage(CLuaBaseEntity* PEntity, const int32 damage) const { - auto* PBaseMob = static_cast(m_PBaseEntity); - if (m_PBaseEntity->id == PEntity->getID()) { ShowWarning(fmt::format("updateEnmityFromDamage(): Attempting to add enmity from damage to self ({}, {})!", PEntity->getName(), PEntity->getID())); return; } + auto* PBaseMob = dynamic_cast(m_PBaseEntity); + // This is a mob attacking a target and losing enmity from doing damage - if (m_PBaseEntity->objtype == TYPE_PC || m_PBaseEntity->objtype == TYPE_PET || (m_PBaseEntity->objtype == TYPE_MOB && PBaseMob->isCharmed)) + if (m_PBaseEntity->objtype == TYPE_PC || m_PBaseEntity->objtype == TYPE_PET || (PBaseMob && PBaseMob->isCharmed)) { - if (PEntity->GetBaseEntity() && PEntity->GetBaseEntity()->objtype == TYPE_MOB) + if (auto* PTargetMob = dynamic_cast(PEntity->GetBaseEntity())) { - static_cast(PEntity->GetBaseEntity())->PEnmityContainer->UpdateEnmityFromAttack(static_cast(m_PBaseEntity), damage); + PTargetMob->PEnmityContainer->UpdateEnmityFromAttack(static_cast(m_PBaseEntity), damage); } } // This is a mob being attacked and gaining enmity on the attacker - else if (m_PBaseEntity->objtype == TYPE_MOB) + else if (PBaseMob) { if (PEntity->GetBaseEntity() && damage > 0 && PEntity->GetBaseEntity()->objtype != TYPE_NPC) { diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 7b67315c8a2..d00d661627e 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -666,7 +666,7 @@ class CLuaBaseEntity void lowerEnmity(CLuaBaseEntity* PEntity, uint8 percent); void updateEnmity(CLuaBaseEntity* PEntity); void transferEnmity(CLuaBaseEntity* entity, uint8 percent, float range); - void updateEnmityFromDamage(CLuaBaseEntity* PEntity, int32 damage); // Adds Enmity to player for specified mob for the damage specified + void updateEnmityFromDamage(CLuaBaseEntity* PEntity, int32 damage) const; // Adds Enmity to player for specified mob for the damage specified void updateEnmityFromCure(CLuaBaseEntity* PEntity, int32 amount, const sol::object& fixedCE, const sol::object& fixedVE); void resetEnmity(CLuaBaseEntity* PEntity); void setEnmityActive(CLuaBaseEntity* PEntity, bool active); From 14ef5e053372e94130ce726c4cff562345658ec2 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:36:01 -0700 Subject: [PATCH 09/20] CheckForDamageMultiplier check for entity type Attacker is not necessarily a player. --- src/map/attack.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/map/attack.cpp b/src/map/attack.cpp index cc701619cd8..8149e21e612 100644 --- a/src/map/attack.cpp +++ b/src/map/attack.cpp @@ -717,8 +717,11 @@ void CAttack::ProcessDamage() SetAttackType(PHYSICAL_ATTACK_TYPE::SAMBA); } - // Get damage multipliers. - m_damage = attackutils::CheckForDamageMultiplier((CCharEntity*)m_attacker, dynamic_cast(m_attacker->m_Weapons[slot]), m_damage, m_attackType, slot, m_isFirstSwing); + // Get player-only damage multipliers. + if (auto* PChar = dynamic_cast(m_attacker)) + { + m_damage = attackutils::CheckForDamageMultiplier(PChar, dynamic_cast(m_attacker->m_Weapons[slot]), m_damage, m_attackType, slot, m_isFirstSwing); + } // Apply Sneak Attack Augment Mod if (m_attacker->getMod(Mod::AUGMENTS_SA) > 0 && IsSneakAttack() && m_attacker->StatusEffectContainer->HasStatusEffect(EFFECT_SNEAK_ATTACK)) From d5f8e0c8b7c8d88dee7e292c9499d9f1dede081a Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:39:51 -0700 Subject: [PATCH 10/20] Resolve target by ID before attempting to face it In some scenarios PC can disappear as the mob is preparing a mobskill leading to crashes or worse --- src/map/ai/controllers/mob_controller.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/map/ai/controllers/mob_controller.cpp b/src/map/ai/controllers/mob_controller.cpp index f1f0accf841..c5d85bc412f 100644 --- a/src/map/ai/controllers/mob_controller.cpp +++ b/src/map/ai/controllers/mob_controller.cpp @@ -737,15 +737,14 @@ void CMobController::DoCombatTick(timer::time_point tick) void CMobController::FaceTarget(const uint16 targid) const { TracyZoneScoped; - const CBaseEntity* targ = PTarget; - if (targid != 0 && ((targ && targid != targ->targid) || !targ)) - { - targ = PMob->GetEntity(targid); - } - if (!(PMob->m_Behavior & BEHAVIOR_NO_TURN) && targ) + + const uint16 resolvedTargid = targid != 0 ? targid : PMob->GetBattleTargetID(); + const auto* maybeTarget = PMob->GetEntity(resolvedTargid); + if (!(PMob->m_Behavior & BEHAVIOR_NO_TURN) && maybeTarget) { - PMob->PAI->PathFind->LookAt(targ->loc.p); + PMob->PAI->PathFind->LookAt(maybeTarget->loc.p); } + PMob->UpdateSpeed(); } From b24d4ddfcd7f38fb657e981a6007d3edfcba970e Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:43:30 -0700 Subject: [PATCH 11/20] Check type before emitting ROE heal/buff event --- src/map/entities/battleentity.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index fe3f53c3ab3..2fd81d9ea50 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -2395,20 +2395,23 @@ void CBattleEntity::OnCastFinished(CMagicState& state, action_t& action) (PEminenceTarget->PParty && PTarget->PParty && ((PEminenceTarget->PParty == PTarget->PParty) || (PEminenceTarget->PParty->m_PAlliance && PEminenceTarget->PParty->m_PAlliance == PTarget->PParty->m_PAlliance)))) { - if (PSpell->isHeal()) + if (auto* PCharEminence = dynamic_cast(PEminenceTarget)) { - roeutils::event(ROE_HEALALLY, static_cast(PEminenceTarget), RoeDatagram("heal", actionResult.param)); + if (PSpell->isHeal()) + { + roeutils::event(ROE_HEALALLY, PCharEminence, RoeDatagram("heal", actionResult.param)); - // We know its an ally or self, if not self and leader matches, credit the RoE Objective - if (PEminenceTarget != PTarget && PEminenceTarget->objtype == TYPE_PC && PTarget->objtype == TYPE_PC && static_cast(PEminenceTarget)->profile.unity_leader == static_cast(PTarget)->profile.unity_leader) + if (auto* PCharTarget = dynamic_cast(PTarget); + PCharTarget && PEminenceTarget != PTarget && PCharEminence->profile.unity_leader == PCharTarget->profile.unity_leader) + { + roeutils::event(ROE_HEAL_UNITYALLY, PCharEminence, RoeDatagram("heal", actionResult.param)); + } + } + else if (PEminenceTarget != PTarget && PSpell->isBuff() && actionResult.param) { - roeutils::event(ROE_HEAL_UNITYALLY, static_cast(PEminenceTarget), RoeDatagram("heal", actionResult.param)); + roeutils::event(ROE_BUFFALLY, PCharEminence, RoeDatagramList{}); } } - else if (PEminenceTarget != PTarget && PSpell->isBuff() && actionResult.param) - { - roeutils::event(ROE_BUFFALLY, static_cast(PEminenceTarget), RoeDatagramList{}); - } } if (PActionTarget->id == PTarget->id) From f687732ace35cd74628e881158f220d23d138bfd Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:45:40 -0700 Subject: [PATCH 12/20] Check for NaN before handing off to Detour --- src/map/navmesh.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/map/navmesh.cpp b/src/map/navmesh.cpp index 3ca0df86112..c6d0a49f6ae 100644 --- a/src/map/navmesh.cpp +++ b/src/map/navmesh.cpp @@ -28,7 +28,6 @@ #include "common/utils.h" #include "common/xirand.h" -#include #include #include #include @@ -266,6 +265,13 @@ auto CNavMesh::findPath(const position_t& start, const position_t& end) -> std:: return {}; } + if (std::isnan(start.x) || std::isnan(start.y) || std::isnan(start.z) || + std::isnan(end.x) || std::isnan(end.y) || std::isnan(end.z)) + { + ShowWarning("CNavMesh::findPath NaN position detected (%u)", m_zoneID); + return {}; + } + DebugNavmesh("CNavMesh::findPath (%f, %f, %f) -> (%f, %f, %f) (zone: %u) (MAX_NAV_POLYS: %u)", start.x, start.y, start.z, end.x, end.y, end.z, m_zoneID, MAX_NAV_POLYS); dtStatus status = 0; From 949d3b8522c8c85ecf2dea2dfa7eff81e9c65a5c Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:47:19 -0700 Subject: [PATCH 13/20] Enforce alignment on party member buffs struct --- src/map/packets/s2c/0x076_group_effects.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/map/packets/s2c/0x076_group_effects.h b/src/map/packets/s2c/0x076_group_effects.h index 4571edb2387..b3691ce172f 100644 --- a/src/map/packets/s2c/0x076_group_effects.h +++ b/src/map/packets/s2c/0x076_group_effects.h @@ -25,6 +25,8 @@ #include class CCharEntity; + +#pragma pack(push, 1) struct partymemberbuffs_t { uint32_t UniqueNo; @@ -33,6 +35,7 @@ struct partymemberbuffs_t uint64_t Bits; uint8_t Buffs[32]; }; +#pragma pack(pop) // https://github.com/atom0s/XiPackets/tree/main/world/server/0x0076 // This packet is sent by the server to update party members' buff information From 36ee4c4ff3e2e052eafd48e914f25bdadc6bb568 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:49:48 -0700 Subject: [PATCH 14/20] getPartyJob use PMaster if called on trusts --- src/map/lua/lua_baseentity.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 4e134627c1c..69759f70562 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -11279,12 +11279,24 @@ uint8 CLuaBaseEntity::getPartySize(const sol::object& arg0) bool CLuaBaseEntity::hasPartyJob(uint8 job) { - if (static_cast(m_PBaseEntity)->PParty != nullptr) + auto* PChar = dynamic_cast(m_PBaseEntity); + if (!PChar) { - for (const auto& member : static_cast(m_PBaseEntity)->PParty->members) + if (auto* PTrust = dynamic_cast(m_PBaseEntity)) { - CCharEntity* PTarget = static_cast(member); + PChar = dynamic_cast(PTrust->PMaster); + } + } + if (!PChar || !PChar->PParty) + { + return false; + } + + for (const auto& member : PChar->PParty->members) + { + if (auto* PTarget = dynamic_cast(member)) + { if (PTarget->GetMJob() == job) { return true; From 8cacda2fd2d42cc213f748d60e01cc11f08b72a7 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:52:03 -0700 Subject: [PATCH 15/20] BST pets declaim logic check for null and type --- src/map/ai/controllers/mob_controller.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/map/ai/controllers/mob_controller.cpp b/src/map/ai/controllers/mob_controller.cpp index c5d85bc412f..04c833ccb5f 100644 --- a/src/map/ai/controllers/mob_controller.cpp +++ b/src/map/ai/controllers/mob_controller.cpp @@ -664,12 +664,16 @@ void CMobController::CastSpell(SpellID spellid) void CMobController::DoCombatTick(timer::time_point tick) { TracyZoneScopedC(0xFF0000); - if (PMob->m_OwnerID.targid != 0 && static_cast(PMob->GetEntity(PMob->m_OwnerID.targid))->PClaimedMob != static_cast(PMob)) + if (PMob->m_OwnerID.targid != 0) { - if (m_Tick >= m_DeclaimTime + 3s) + auto* POwner = dynamic_cast(PMob->GetEntity(PMob->m_OwnerID.targid)); + if (POwner && POwner->PClaimedMob != static_cast(PMob)) { - PMob->m_OwnerID.clean(); - PMob->updatemask |= UPDATE_STATUS; + if (m_Tick >= m_DeclaimTime + 3s) + { + PMob->m_OwnerID.clean(); + PMob->updatemask |= UPDATE_STATUS; + } } } From 303586417b101c8ef26672964b599a94345dbf5c Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 18:54:42 -0700 Subject: [PATCH 16/20] atan2f in worldAngle to avoid division by zero --- src/common/utils.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 11898214a5e..8c2201b9160 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -129,9 +129,13 @@ uint8 radianToRotation(float radian) uint8 worldAngle(const position_t& A, const position_t& B) { - uint8 angle = (uint8)(atanf((B.z - A.z) / (B.x - A.x)) * -(128.0f / M_PI)); + if (isWithinDistance(A, B, 0.1f, true)) + { + return A.rotation; + } - return isWithinDistance(A, B, 0.1f, true) ? A.rotation : (A.x > B.x ? angle + 128 : angle); + float radians = atan2f(B.z - A.z, B.x - A.x); + return static_cast(radians * -(128.0f / M_PI)); } uint8 relativeAngle(uint8 world, int16 diff) From b1ae20e5ea3314ec1301c7d46cc55461efa81f48 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 19:00:23 -0700 Subject: [PATCH 17/20] Prevent packet object slicing Certain packets have extra members beyond the buffer but are being passed as CBasicPacket during copy. Mostly relevant for BATTLE2 and LOGOUT --- src/map/entities/charentity.cpp | 2 +- src/map/packets/basic.h | 4 ++-- src/map/packets/s2c/base.h | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index 36b686ff14a..00d0d9aa4c6 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -403,7 +403,7 @@ auto CCharEntity::getPacketListCopy() -> std::deque> PacketListCopy; for (const auto& packet : PacketList) { - PacketListCopy.emplace_back(std::make_unique(packet)); + PacketListCopy.emplace_back(packet->copy()); } return PacketListCopy; } diff --git a/src/map/packets/basic.h b/src/map/packets/basic.h index 114050297eb..87994cba26e 100644 --- a/src/map/packets/basic.h +++ b/src/map/packets/basic.h @@ -83,9 +83,9 @@ class CBasicPacket CBasicPacket& operator=(const CBasicPacket& other) = delete; CBasicPacket& operator=(CBasicPacket&& other) noexcept = delete; - auto copy() -> std::unique_ptr> + virtual auto copy() const -> std::unique_ptr { - return std::make_unique>(*this); + return std::make_unique(*this); } auto getType() -> uint16 diff --git a/src/map/packets/s2c/base.h b/src/map/packets/s2c/base.h index f3b91c922bf..b570889313d 100644 --- a/src/map/packets/s2c/base.h +++ b/src/map/packets/s2c/base.h @@ -37,6 +37,12 @@ struct GP_SERV_HEADER template class GP_SERV_PACKET : public CBasicPacket { +public: + auto copy() const -> std::unique_ptr override + { + return std::make_unique(static_cast(*this)); + } + protected: GP_SERV_PACKET() { From 47724effa5e9e2de3d39b5ba0d34aa7f9c1e5c8d Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 19:06:21 -0700 Subject: [PATCH 18/20] Explicit uint32 for buff timers overflow --- .../s2c/0x063_miscdata_status_icons.cpp | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/map/packets/s2c/0x063_miscdata_status_icons.cpp b/src/map/packets/s2c/0x063_miscdata_status_icons.cpp index 7d07f17beb2..ae60cb29922 100644 --- a/src/map/packets/s2c/0x063_miscdata_status_icons.cpp +++ b/src/map/packets/s2c/0x063_miscdata_status_icons.cpp @@ -36,24 +36,23 @@ GP_SERV_COMMAND_MISCDATA::STATUS_ICONS::STATUS_ICONS(const CCharEntity* PChar) // Initialize all icons to 0xFF (no icon) std::ranges::fill(packet.icons, 0x00FF); - int i = 0; - // clang-format off + constexpr uint32 NO_TIMER = 0x7FFFFFFF; + int i = 0; PChar->StatusEffectContainer->ForEachEffect([&packet, &i](CStatusEffect* PEffect) - { - if (PEffect->GetIcon() != 0) - { - auto durationRemaining = 0x7FFFFFFF; - if (PEffect->GetDuration() > 0s && !PEffect->HasEffectFlag(EFFECTFLAG_HIDE_TIMER)) - { - // this value overflows, but the client expects the overflowed timestamp and corrects it - durationRemaining = timer::count_seconds(PEffect->GetStartTime() - timer::now() + PEffect->GetDuration()); - durationRemaining += earth_time::vanadiel_timestamp(); - durationRemaining *= 60; - } - packet.icons[i] = PEffect->GetIcon(); - packet.timestamps[i] = durationRemaining; - ++i; - } - }); - // clang-format on + { + if (PEffect->GetIcon() != 0) + { + uint32 timestamp = NO_TIMER; + if (PEffect->GetDuration() > 0s && !PEffect->HasEffectFlag(EFFECTFLAG_HIDE_TIMER)) + { + // this value overflows, but the client expects the overflowed timestamp and corrects it + uint32 seconds = timer::count_seconds(PEffect->GetStartTime() - timer::now() + PEffect->GetDuration()); + seconds += earth_time::vanadiel_timestamp(); + timestamp = seconds * 60; + } + packet.icons[i] = PEffect->GetIcon(); + packet.timestamps[i] = timestamp; + ++i; + } + }); } From 3e218963347f7d5f3b0b25e187f9290f509fb951 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 19:33:25 -0700 Subject: [PATCH 19/20] ItemState check if item was destroyed before manipulating --- CMakeLists.txt | 10 ++++++++++ cmake/Sanitizers.cmake | 8 ++++---- cmake/StandardProjectSettings.cmake | 7 ++++++- src/map/ai/states/item_state.cpp | 19 +++++++++++++++---- src/map/ai/states/item_state.h | 2 +- src/map/entities/charentity.cpp | 17 +++++++++-------- src/map/entities/charentity.h | 2 +- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1eda1c996ee..2a23c8016b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,16 @@ elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "RELEASE") message(STATUS "CMAKE_CXX_FLAGS_RELEASE: ${CMAKE_CXX_FLAGS_RELEASE}") elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "RELWITHDEBINFO") message(STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") +elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "ASAN") + message(STATUS "CMAKE_CXX_FLAGS_ASAN: ${CMAKE_CXX_FLAGS_ASAN}") +elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "UBSAN") + message(STATUS "CMAKE_CXX_FLAGS_UBSAN: ${CMAKE_CXX_FLAGS_UBSAN}") +elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "TSAN") + message(STATUS "CMAKE_CXX_FLAGS_TSAN: ${CMAKE_CXX_FLAGS_TSAN}") +elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "MSAN") + message(STATUS "CMAKE_CXX_FLAGS_MSAN: ${CMAKE_CXX_FLAGS_MSAN}") +elseif ("${CMAKE_BUILD_TYPE_UPPER}" STREQUAL "LSAN") + message(STATUS "CMAKE_CXX_FLAGS_LSAN: ${CMAKE_CXX_FLAGS_LSAN}") else() message(FATAL_ERROR "Did not recognise CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE} to print out compiler flags.") endif() diff --git a/cmake/Sanitizers.cmake b/cmake/Sanitizers.cmake index ae8cfc4f838..8b0414e690a 100644 --- a/cmake/Sanitizers.cmake +++ b/cmake/Sanitizers.cmake @@ -49,13 +49,13 @@ set(CMAKE_CXX_FLAGS_MSAN CACHE STRING "Flags used by the C++ compiler during MemorySanitizer builds." FORCE) -# Undefinedbehavior -# LeakSanitizer is a run-time undefined behavior detector. +# UndefinedBehaviorSanitizer +# UndefinedBehaviorSanitizer is a run-time undefined behavior detector. set(CMAKE_C_FLAGS_UBSAN - "-fsanitize=undefined" + "-fsanitize=undefined -fno-omit-frame-pointer -g -O2 -DNDEBUG" CACHE STRING "Flags used by the C compiler during UndefinedBehaviorSanitizer builds." FORCE) set(CMAKE_CXX_FLAGS_UBSAN - "-fsanitize=undefined" + "-fsanitize=undefined -fno-omit-frame-pointer -g -O2 -DNDEBUG" CACHE STRING "Flags used by the C++ compiler during UndefinedBehaviorSanitizer builds." FORCE) diff --git a/cmake/StandardProjectSettings.cmake b/cmake/StandardProjectSettings.cmake index 992194e462a..ba91acec78b 100644 --- a/cmake/StandardProjectSettings.cmake +++ b/cmake/StandardProjectSettings.cmake @@ -16,7 +16,7 @@ endif() option(ENABLE_IPO "Enable Interprocedural Optimization, aka Link Time Optimization (LTO)" ON) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF) -if(ENABLE_IPO AND NOT CMAKE_BUILD_TYPE STREQUAL Debug) +if(ENABLE_IPO AND NOT CMAKE_BUILD_TYPE STREQUAL Debug AND NOT CMAKE_BUILD_TYPE STREQUAL ASAN AND NOT CMAKE_BUILD_TYPE STREQUAL UBSAN AND NOT CMAKE_BUILD_TYPE STREQUAL TSAN AND NOT CMAKE_BUILD_TYPE STREQUAL MSAN AND NOT CMAKE_BUILD_TYPE STREQUAL LSAN) include(CheckIPOSupported) check_ipo_supported( RESULT @@ -106,6 +106,11 @@ function(set_target_output_directory target) RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_SOURCE_DIR}" RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_SOURCE_DIR}" RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_SOURCE_DIR}" + RUNTIME_OUTPUT_DIRECTORY_ASAN "${CMAKE_SOURCE_DIR}" + RUNTIME_OUTPUT_DIRECTORY_UBSAN "${CMAKE_SOURCE_DIR}" + RUNTIME_OUTPUT_DIRECTORY_TSAN "${CMAKE_SOURCE_DIR}" + RUNTIME_OUTPUT_DIRECTORY_MSAN "${CMAKE_SOURCE_DIR}" + RUNTIME_OUTPUT_DIRECTORY_LSAN "${CMAKE_SOURCE_DIR}" ) endfunction() diff --git a/src/map/ai/states/item_state.cpp b/src/map/ai/states/item_state.cpp index 01b7dc9f56f..d8b79a1e510 100644 --- a/src/map/ai/states/item_state.cpp +++ b/src/map/ai/states/item_state.cpp @@ -161,6 +161,11 @@ void CItemState::UpdateTarget(const uint16 targid) CState::UpdateTarget(targid); CState::SetTarget(targid); + if (!m_PItem) + { + return; + } + // Special case for Soultrapper usage: // Valid to use on mobs that are: // - unclaimed @@ -193,8 +198,14 @@ auto CItemState::Update(const timer::time_point tick) -> bool if (!m_interrupted) { - FinishItem(action); m_PEntity->PAI->EventHandler.triggerListener("ITEM_USE", m_PEntity, m_PItem, &action); + + bool consumed = FinishItem(action); + if (consumed) + { + m_PItem = nullptr; + } + // Only send packet if action was populated (e.g. interrupts return early) if (!action.targets.empty()) { @@ -225,7 +236,7 @@ void CItemState::Cleanup(timer::time_point tick) { m_PEntity->UContainer->Clean(); - if ((m_interrupted || !IsCompleted()) && !m_PItem->isType(ITEM_EQUIPMENT)) + if (m_PItem && (m_interrupted || !IsCompleted()) && !m_PItem->isType(ITEM_EQUIPMENT)) { m_PItem->setSubType(ITEM_UNLOCKED); } @@ -312,9 +323,9 @@ void CItemState::InterruptItem(action_t& action) } } -void CItemState::FinishItem(action_t& action) +auto CItemState::FinishItem(action_t& action) -> bool { - m_PEntity->OnItemFinish(*this, action); + return m_PEntity->OnItemFinish(*this, action); } auto CItemState::HasMoved() const -> bool diff --git a/src/map/ai/states/item_state.h b/src/map/ai/states/item_state.h index d2510c9366e..130b231ba8a 100644 --- a/src/map/ai/states/item_state.h +++ b/src/map/ai/states/item_state.h @@ -53,7 +53,7 @@ class CItemState : public CState CItemUsable* GetItem() const; void InterruptItem(action_t& action); - void FinishItem(action_t& action); + auto FinishItem(action_t& action) -> bool; protected: bool HasMoved() const; diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index 00d0d9aa4c6..b33b80d5907 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -2400,7 +2400,7 @@ void CCharEntity::OnRaise() } } -void CCharEntity::OnItemFinish(CItemState& state, action_t& action) +auto CCharEntity::OnItemFinish(CItemState& state, action_t& action) -> bool { TracyZoneScoped; @@ -2412,7 +2412,7 @@ void CCharEntity::OnItemFinish(CItemState& state, action_t& action) ShowWarning("OnItemFinish: %s attempted to use reserved/insufficient %s (%u).", this->getName(), PItem->getName(), PItem->getID()); this->pushPacket(this, this, PItem->getID(), 0, MsgBasic::ITEM_FAILS_TO_ACTIVATE); - return; + return false; } uint8 findFlags = 0; @@ -2430,7 +2430,7 @@ void CCharEntity::OnItemFinish(CItemState& state, action_t& action) if (PAI->TargetFind->m_targets.size() == 0) { // TODO: interrupt action packet? - return; + return false; } action.actorId = this->id; @@ -2494,13 +2494,14 @@ void CCharEntity::OnItemFinish(CItemState& state, action_t& action) // add recast timer to Recast List from any bag this->PRecastContainer->Add(RECAST_ITEM, static_cast(PItem->getSlotID() << 8 | PItem->getLocationID()), PItem->getReuseTime()); } + return false; } - else // unlock all items except equipment - { - PItem->setSubType(ITEM_UNLOCKED); - charutils::UpdateItem(this, PItem->getLocationID(), PItem->getSlotID(), -1, true); - } + // Consumable items + PItem->setSubType(ITEM_UNLOCKED); + const bool willBeDestroyed = PItem->getQuantity() == 1; + charutils::UpdateItem(this, PItem->getLocationID(), PItem->getSlotID(), -1, true); + return willBeDestroyed; } CBattleEntity* CCharEntity::IsValidTarget(uint16 targid, uint16 validTargetFlags, std::unique_ptr& errMsg) diff --git a/src/map/entities/charentity.h b/src/map/entities/charentity.h index 8782427d2bb..a74525e00b8 100644 --- a/src/map/entities/charentity.h +++ b/src/map/entities/charentity.h @@ -651,7 +651,7 @@ class CCharEntity : public CBattleEntity virtual void OnDeathTimer() override; virtual void OnRaise() override; - virtual void OnItemFinish(CItemState&, action_t&); + virtual auto OnItemFinish(CItemState&, action_t&) -> bool; auto getCharVar(const std::string& varName) const -> int32; auto getCharVarsWithPrefix(const std::string& prefix) -> std::vector>; From 337aff5f1bdfe703507394cdcdf9739af16f3e98 Mon Sep 17 00:00:00 2001 From: sruon Date: Fri, 6 Feb 2026 20:27:29 -0700 Subject: [PATCH 20/20] float to uint8 cast UB in worldAngle --- src/common/utils.cpp | 5 +-- .../s2c/0x063_miscdata_status_icons.cpp | 35 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 8c2201b9160..70519d7f3e6 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -134,8 +134,9 @@ uint8 worldAngle(const position_t& A, const position_t& B) return A.rotation; } - float radians = atan2f(B.z - A.z, B.x - A.x); - return static_cast(radians * -(128.0f / M_PI)); + float radians = atan2f(B.z - A.z, B.x - A.x); + int16 rawAngle = static_cast(radians * -(128.0f / M_PI)); + return static_cast((rawAngle % 256 + 256) % 256); } uint8 relativeAngle(uint8 world, int16 diff) diff --git a/src/map/packets/s2c/0x063_miscdata_status_icons.cpp b/src/map/packets/s2c/0x063_miscdata_status_icons.cpp index ae60cb29922..d769ed6b2bf 100644 --- a/src/map/packets/s2c/0x063_miscdata_status_icons.cpp +++ b/src/map/packets/s2c/0x063_miscdata_status_icons.cpp @@ -38,21 +38,22 @@ GP_SERV_COMMAND_MISCDATA::STATUS_ICONS::STATUS_ICONS(const CCharEntity* PChar) constexpr uint32 NO_TIMER = 0x7FFFFFFF; int i = 0; - PChar->StatusEffectContainer->ForEachEffect([&packet, &i](CStatusEffect* PEffect) - { - if (PEffect->GetIcon() != 0) - { - uint32 timestamp = NO_TIMER; - if (PEffect->GetDuration() > 0s && !PEffect->HasEffectFlag(EFFECTFLAG_HIDE_TIMER)) - { - // this value overflows, but the client expects the overflowed timestamp and corrects it - uint32 seconds = timer::count_seconds(PEffect->GetStartTime() - timer::now() + PEffect->GetDuration()); - seconds += earth_time::vanadiel_timestamp(); - timestamp = seconds * 60; - } - packet.icons[i] = PEffect->GetIcon(); - packet.timestamps[i] = timestamp; - ++i; - } - }); + PChar->StatusEffectContainer->ForEachEffect( + [&packet, &i](CStatusEffect* PEffect) + { + if (PEffect->GetIcon() != 0) + { + uint32 timestamp = NO_TIMER; + if (PEffect->GetDuration() > 0s && !PEffect->HasEffectFlag(EFFECTFLAG_HIDE_TIMER)) + { + // this value overflows, but the client expects the overflowed timestamp and corrects it + uint32 seconds = timer::count_seconds(PEffect->GetStartTime() - timer::now() + PEffect->GetDuration()); + seconds += earth_time::vanadiel_timestamp(); + timestamp = seconds * 60; + } + packet.icons[i] = PEffect->GetIcon(); + packet.timestamps[i] = timestamp; + ++i; + } + }); }