Commit graph

22 commits

Author SHA1 Message Date
Kelsi
d138269a35 fix(movement): reject server teleports to corrupted near-origin positions
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
The server can persist a corrupted near-origin position on map 0 (from a
faulty area-trigger destination) across sessions. On re-login it sends the
bad position via LOGIN_VERIFY_WORLD; if the player walks into the offending
trigger again the server re-teleports there, and our heartbeats reinforce
the bad save — creating a permanent teleport loop.

Defenses added:
- handleTeleportAck rejects MSG_MOVE_TELEPORT to near-origin on map 0
  (no position update, no ACK, no world reload)
- applyPlayerTransportState rejects player UPDATE_OBJECT MOVEMENT blocks
  pushing the same bad position
- sendMovement blocks heartbeats originating from near-origin so the
  server cannot persist the bad save
- 10-second area-trigger cooldown after teleport / world entry / login
  (replaces the one-shot suppress flag that re-fired on jitter)
- Immediate STOP+HEARTBEAT after teleport ACK / WORLDPORT ACK / login
  to sync the real position with the server promptly
- CMSG_AREATRIGGER firing now logged at WARNING level for diagnosis
2026-04-24 17:48:49 -07:00
Pavel Okhlopkov
535cc20afe fix state gate races and robust spline
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 23:30:55 +03:00
Kelsi
758f7b27b9 fix(logging): downgrade remaining emote registry diagnostics to DEBUG
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Remove temporary NPC death diagnostic from entity_controller and
downgrade emote override/load-count messages from WARNING to DEBUG.
2026-04-05 20:58:11 -07:00
Kelsi
17c1e3ea3b fix(entities): add diagnostic for NPC death callback chain 2026-04-05 20:41:27 -07:00
Kelsi
069dd36698 fix(parsing): bail on suspicious maskBlockCount in CREATE_OBJECT blocks
When spline parsing consumes the wrong number of bytes, the subsequent
blockCount read lands on garbage data (e.g. 71 instead of ~5 for UNIT).
Previously the parser logged a warning but continued, reading garbage
mask/field data until hitting truncation. Now it returns false for
CREATE_OBJECT blocks with suspicious counts, letting the block loop
skip cleanly to the next entity.

Also downgrade ~44 diagnostic LOG_WARNING messages to LOG_DEBUG across
17 files (equipment, transport, DBC, heartbeat, chat, GO raypick, etc.)
to reduce log noise and make real warnings visible.
2026-04-05 20:12:17 -07:00
Paul
65839287b4 feat(game): introduce GameHandler domain interfaces and eliminate friend declarations
Add game_interfaces.hpp with five narrow domain contracts that GameHandler now
publishes to its domain handlers, replacing the previous friend-class anti-pattern.

Changes:
- include/game/game_interfaces.hpp (new): IConnectionState, ITargetingState,
  IEntityAccess, ISocialState, IPvpState — each interface exposes only the state
  its consumer legitimately needs
- include/game/game_handler.hpp: GameHandler inherits all five interfaces;
  include of game_interfaces.hpp added
- include/game/movement_handler.hpp: remove `friend class GameHandler`; add
  public named accessors for previously-private fields (monsterMovePacketsThisTickRef,
  timeSinceLastMoveHeartbeatRef, resetMovementClock, setFalling, setFallStartMs)
- include/game/spell_handler.hpp: remove `friend class GameHandler/InventoryHandler/
  CombatHandler/EntityController`; promote private packet handlers (handlePetSpells,
  handleListStabledPets, pet stable commands, DBC loaders) to public; add accessor
  methods for aura cache, known spells, and player aura slot mutation
- src/game/game_handler.cpp, game_handler_callbacks.cpp, game_handler_packets.cpp:
  replace direct private field access with the new accessor API
  (e.g. casting_ → isCasting(), monsterMovePacketsThisTick_ → ...ThisTickRef())
- src/game/inventory_handler.cpp, combat_handler.cpp, entity_controller.cpp:
  replace friend-class private access with public accessor calls

No behaviour change. All 13 test suites pass. Zero build warnings.
2026-04-05 20:25:02 +03:00
Paul
34c0e3ca28 chore(refactor): god-object decomposition and mega-file splits
Split all mega-files by single-responsibility concern and
partially extracting AudioCoordinator and
OverlaySystem from the Renderer facade. No behavioral changes.

Splits:
- game_handler.cpp (5,247 LOC) → core + callbacks + packets (3 files)
- world_packets.cpp (4,453 LOC) → economy/entity/social/world (4 files)
- game_screen.cpp  (5,786 LOC) → core + frames + hud + minimap (4 files)
- m2_renderer.cpp  (3,343 LOC) → core + instance + particles + render (4 files)
- chat_panel.cpp   (3,140 LOC) → core + commands + utils (3 files)
- entity_spawner.cpp (2,750 LOC) → core + player + processing (3 files)

Extractions:
- AudioCoordinator: include/audio/ + src/audio/ (owned by Renderer)
- OverlaySystem: include/rendering/ + src/rendering/overlay_system.*

CMakeLists.txt: registered all 17 new translation units.
Related handler/callback files: minor include fixups post-split.
2026-04-05 19:30:44 +03:00
Kelsi
53244d025c feat(repair): DBC-based repair cost estimation and UI display
Calculate repair costs client-side using DurabilityCosts.dbc and
DurabilityQuality.dbc. Block repair when player can't afford it and
only apply optimistic durability/gold updates when cost is verified.
Show repair cost next to the Repair All button in the vendor window.
2026-04-05 04:15:48 -07:00
Kelsi
1dd1a431f4 fix(repair): process item durability updates even when entity missing from manager
handleValuesUpdate silently dropped VALUES updates for item GUIDs not in
entityManager, causing repair-all durability changes to be lost. Fall
through to updateItemOnValuesUpdate for items tracked in onlineItems_.
2026-04-05 03:15:03 -07:00
Paul
b4989dc11f feat(animation): decompose AnimationController into FSM-based architecture
Replace the 2,200-line monolithic AnimationController (goto-driven,
single class, untestable) with a composed FSM architecture per
refactor.md.

New subsystem (src/rendering/animation/ — 16 headers, 10 sources):
- CharacterAnimator: FSM composer implementing ICharacterAnimator
- LocomotionFSM: idle/walk/run/sprint/jump/swim/strafe
- CombatFSM: melee/ranged/spell cast/stun/hit reaction/charge
- ActivityFSM: emote/loot/sit-down/sitting/sit-up
- MountFSM: idle/run/flight/taxi/fidget/rear-up (per-instance RNG)
- AnimCapabilitySet + AnimCapabilityProbe: probe once at model load,
  eliminate per-frame hasAnimation() linear search
- AnimationManager: registry of CharacterAnimator by GUID
- EmoteRegistry: DBC-backed emote command → animId singleton
- FootstepDriver, SfxStateDriver: extracted from AnimationController

animation_ids.hpp/.cpp moved to animation/ subdirectory (452 named
constants); all include paths updated.

AnimationController retained as thin adapter (~400 LOC): collects
FrameInput, delegates to CharacterAnimator, applies AnimOutput.

Priority order: Mount > Stun > HitReaction > Spell > Charge >
Melee/Ranged > CombatIdle > Emote > Loot > Sit > Locomotion.
STAY_IN_STATE policy when all FSMs return valid=false.

Bugs fixed:
- Remove static mt19937 in mount fidget (shared state across all
  mounted units) — replaced with per-instance seeded RNG
- Remove goto from mounted animation branch (skipped init)
- Remove per-frame hasAnimation() calls (now one probe at load)
- Fix VK_INDEX_TYPE_UINT16 → UINT32 in shadow pass

Tests (4 new suites, all ASAN+UBSan clean):
- test_locomotion_fsm: 167 assertions
- test_combat_fsm: 125 cases
- test_activity_fsm: 112 cases
- test_anim_capability: 56 cases

docs/ANIMATION_SYSTEM.md added (architecture reference).
2026-04-05 12:27:35 +03:00
Paul
e58f9b4b40 feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.

Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.

Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.

Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.

Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.

Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
Kelsi
5538655383 fix(chat): resolve /r reply target when name arrives after whisper
Whisper sender name may not be in the player name cache when the packet
arrives. Store the sender GUID and lazily resolve the name from the
cache in getLastWhisperSender(). Also backfill lastWhisperSender_ when
the SMSG_NAME_QUERY_RESPONSE arrives.
2026-04-04 00:03:19 -07:00
Paul
0e6aaeb44e fix warnings, remove phases from commentaries 2026-03-31 20:11:28 +03:00
Kelsi
e8a4a7402f fix: clamp player percentage stats, add scale field why-comment
- 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
2026-03-30 15:48:30 -07:00
Kelsi
e5b4e86600 fix: misleading indentation on BAG_UPDATE/UNIT_INVENTORY_CHANGED emits
The two emit calls were indented 12 spaces (suggesting a nested block)
instead of 8 (matching the enclosing if). Same class of maintenance
trap as the PLAYER_ALIVE/PLAYER_UNGHOST fix in b3abf04d.
2026-03-29 19:31:29 -07:00
Kelsi
35b952bc6f fix: SMSG_IGNORE_LIST read phantom string field after each GUID
The packet only contains uint8 count + count×uint64 GUIDs, but the
handler called readString() after each GUID. This consumed raw bytes of
subsequent GUIDs as a string, corrupting all entries after the first.
Now stores GUIDs in ignoreListGuids_ and resolves names asynchronously
via SMSG_NAME_QUERY_RESPONSE, matching the friends list pattern.

Also fixes unsafe static_pointer_cast in ready check (no type guard)
and removes redundant packetHasRemaining wrapper (duplicates Packet API).
2026-03-29 18:11:29 -07:00
Kelsi
298974ebc2 refactor: extract markPlayerDead to deduplicate death/corpse caching
Both the health==0 and dynFlags UNIT_DYNFLAG_DEAD paths duplicated the
same corpse-position caching and death-state logic with a subtle
asymmetry (only health path called stopAutoAttack). Extracted into
markPlayerDead() so coordinate swapping and state changes happen in one
place. stopAutoAttack remains at the health==0 call site since the
dynFlags path doesn't need it.
2026-03-29 17:59:44 -07:00
Kelsi
0aff4b155c fix: dismount cleared all indefinite auras instead of just mount aura
The dismount path wiped every aura with maxDurationMs < 0, which
includes racial passives, tracking, and zone buffs — not just the mount
spell. Now only clears the specific mountAuraSpellId_ so the buff bar
stays accurate without waiting for a server aura resync.
2026-03-29 17:56:59 -07:00
Kelsi
b3abf04dbb fix: misleading indentation on PLAYER_ALIVE/PLAYER_UNGHOST event emits
The emit calls were indented at a level suggesting they were outside the
if/else blocks, but braces placed them inside. Fixed to match the actual
control flow, preventing a future maintainer from "correcting" the
indentation and accidentally changing the logic.
2026-03-29 17:52:56 -07:00
Kelsi
d32b35c583 fix: restore Classic aura flag normalization and clean up EntityController
- Restore 0x02→0x80 Classic harmful-to-WotLK debuff bit mapping in
  syncClassicAurasFromFields so downstream checks work across expansions
- Extract handleDisplayIdChange helper to deduplicate identical logic
  in onValuesUpdateUnit and onValuesUpdatePlayer
- Remove unused newItemCreated parameter from handleValuesUpdate
- Fix indentation on PLAYER_DEAD/PLAYER_ALIVE/PLAYER_UNGHOST emit calls
2026-03-29 16:29:56 -07:00
Paul
b0a07c2472 refactor(game): apply SOLID phases 2-6 to EntityController
- split applyUpdateObjectBlock into handleCreateObject,
  handleValuesUpdate, handleMovementUpdate
-  extract concern helpers — createEntityFromBlock,
  applyPlayerTransportState, applyUnitFieldsOnCreate/OnUpdate,
  applyPlayerStatFields, dispatchEntitySpawn, trackItemOnCreate,
  updateItemOnValuesUpdate, syncClassicAurasFromFields,
  detectPlayerMountChange, updateNonPlayerTransportAttachment
- UnitFieldIndices, PlayerFieldIndices, UnitFieldUpdateResult
  structs with static resolve() — eliminate repeated fieldIndex() calls
- IObjectTypeHandler strategy interface; concrete handlers
  UnitTypeHandler, PlayerTypeHandler, GameObjectTypeHandler,
  ItemTypeHandler, CorpseTypeHandler registered in typeHandlers_ map;
  handleCreateObject and handleValuesUpdate now dispatch via
  getTypeHandler() — adding a new object type requires zero changes
  to existing handler methods
- PendingEvents member bus; all 27 inline owner_.fireAddonEvent()
  calls in the update path replaced with pendingEvents_.emit(); events
  flushed via flushPendingEvents() at the end of each handler, decoupling
  field-parse logic from the addon callback system

entity_controller.cpp: 1520-line monolith → longest method ~200 lines,
cyclomatic complexity ~180 → ~5; zero duplicated CREATE/VALUES blocks
2026-03-29 14:42:38 +03:00
Paul
f5757aca83 refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).

What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
  field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())

8 opcodes re-registered by EntityController::registerOpcodes():
  SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
  SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
  SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
  SMSG_PAGE_TEXT_QUERY_RESPONSE

Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.

Also included:
- .clang-tidy: add modernize-use-nodiscard,
  modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
  silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs

Line counts:
  entity_controller.hpp  147 lines  (new)
  entity_controller.cpp  2172 lines (new)
  game_handler.cpp       8095 lines (was 10143, −2048)

Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00