Kelsidavis-WoWee/src/rendering/animation/anim_capability_probe.cpp
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

287 lines
13 KiB
C++

// ============================================================================
// AnimCapabilityProbe
//
// Scans a model's animation capabilities once and returns an
// AnimCapabilitySet with resolved IDs (after fallback chains).
// Extracted from the scattered hasAnimation/pickFirstAvailable calls
// in AnimationController.
// ============================================================================
#include "rendering/animation/anim_capability_probe.hpp"
#include "rendering/animation/animation_ids.hpp"
#include "rendering/renderer.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/m2_renderer.hpp"
namespace wowee {
namespace rendering {
uint32_t AnimCapabilityProbe::pickFirst(Renderer* renderer, uint32_t instanceId,
const uint32_t* candidates, size_t count) {
auto* charRenderer = renderer->getCharacterRenderer();
if (!charRenderer) return 0;
for (size_t i = 0; i < count; ++i) {
if (charRenderer->hasAnimation(instanceId, candidates[i])) {
return candidates[i];
}
}
return 0;
}
AnimCapabilitySet AnimCapabilityProbe::probe(Renderer* renderer, uint32_t instanceId) {
AnimCapabilitySet caps;
if (!renderer || instanceId == 0) return caps;
auto* cr = renderer->getCharacterRenderer();
if (!cr) return caps;
auto has = [&](uint32_t id) -> bool {
return cr->hasAnimation(instanceId, id);
};
// Helper: pick first available from static array
auto pick = [&](const uint32_t* candidates, size_t count) -> uint32_t {
return pickFirst(renderer, instanceId, candidates, count);
};
// ── Locomotion ──────────────────────────────────────────────────────
// STAND is animation ID 0. Every M2 model has sequence 0 as its base idle.
// Cannot use ternary `has(0) ? 0 : 0` — both branches are 0.
caps.resolvedStand = anim::STAND;
caps.hasStand = has(anim::STAND);
{
static const uint32_t walkCands[] = {anim::WALK, anim::RUN};
caps.resolvedWalk = pick(walkCands, 2);
caps.hasWalk = (caps.resolvedWalk != 0);
}
{
static const uint32_t runCands[] = {anim::RUN, anim::WALK};
caps.resolvedRun = pick(runCands, 2);
caps.hasRun = (caps.resolvedRun != 0);
}
{
static const uint32_t sprintCands[] = {anim::SPRINT, anim::RUN, anim::WALK};
caps.resolvedSprint = pick(sprintCands, 3);
caps.hasSprint = (caps.resolvedSprint != 0);
}
{
static const uint32_t walkBackCands[] = {anim::WALK_BACKWARDS, anim::WALK};
caps.resolvedWalkBackwards = pick(walkBackCands, 2);
caps.hasWalkBackwards = (caps.resolvedWalkBackwards != 0);
}
{
static const uint32_t strafeLeftCands[] = {anim::SHUFFLE_LEFT, anim::RUN_LEFT, anim::WALK};
caps.resolvedStrafeLeft = pick(strafeLeftCands, 3);
}
{
static const uint32_t strafeRightCands[] = {anim::SHUFFLE_RIGHT, anim::RUN_RIGHT, anim::WALK};
caps.resolvedStrafeRight = pick(strafeRightCands, 3);
}
{
static const uint32_t runLeftCands[] = {anim::RUN_LEFT, anim::RUN};
caps.resolvedRunLeft = pick(runLeftCands, 2);
}
{
static const uint32_t runRightCands[] = {anim::RUN_RIGHT, anim::RUN};
caps.resolvedRunRight = pick(runRightCands, 2);
}
// ── Jump ────────────────────────────────────────────────────────────
caps.resolvedJumpStart = has(anim::JUMP_START) ? anim::JUMP_START : 0;
caps.resolvedJump = has(anim::JUMP) ? anim::JUMP : 0;
caps.resolvedJumpEnd = has(anim::JUMP_END) ? anim::JUMP_END : 0;
caps.hasJump = (caps.resolvedJumpStart != 0);
// ── Swim ────────────────────────────────────────────────────────────
caps.resolvedSwimIdle = has(anim::SWIM_IDLE) ? anim::SWIM_IDLE : 0;
caps.resolvedSwim = has(anim::SWIM) ? anim::SWIM : 0;
caps.hasSwim = (caps.resolvedSwimIdle != 0 || caps.resolvedSwim != 0);
{
static const uint32_t swimBackCands[] = {anim::SWIM_BACKWARDS, anim::SWIM};
caps.resolvedSwimBackwards = pick(swimBackCands, 2);
}
{
static const uint32_t swimLeftCands[] = {anim::SWIM_LEFT, anim::SWIM};
caps.resolvedSwimLeft = pick(swimLeftCands, 2);
}
{
static const uint32_t swimRightCands[] = {anim::SWIM_RIGHT, anim::SWIM};
caps.resolvedSwimRight = pick(swimRightCands, 2);
}
// ── Melee combat (fallback chains match resolveMeleeAnimId) ─────────
{
static const uint32_t melee1HCands[] = {
anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_UNARMED,
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
caps.resolvedMelee1H = pick(melee1HCands, 6);
}
{
static const uint32_t melee2HCands[] = {
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED,
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
caps.resolvedMelee2H = pick(melee2HCands, 6);
}
{
static const uint32_t melee2HLooseCands[] = {
anim::ATTACK_2H_LOOSE_PIERCE, anim::ATTACK_2H_LOOSE,
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED};
caps.resolvedMelee2HLoose = pick(melee2HLooseCands, 5);
}
{
static const uint32_t meleeUnarmedCands[] = {
anim::ATTACK_UNARMED, anim::ATTACK_1H, anim::ATTACK_2H,
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
caps.resolvedMeleeUnarmed = pick(meleeUnarmedCands, 6);
}
{
static const uint32_t meleeFistCands[] = {
anim::ATTACK_FIST_1H, anim::ATTACK_FIST_1H_OFF,
anim::ATTACK_1H, anim::ATTACK_UNARMED,
anim::PARRY_FIST_1H, anim::PARRY_1H};
caps.resolvedMeleeFist = pick(meleeFistCands, 6);
}
{
static const uint32_t meleePierceCands[] = {
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H, anim::ATTACK_UNARMED};
caps.resolvedMeleePierce = pick(meleePierceCands, 3);
}
{
static const uint32_t meleeOffCands[] = {
anim::ATTACK_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED};
caps.resolvedMeleeOffHand = pick(meleeOffCands, 3);
}
{
static const uint32_t meleeOffFistCands[] = {
anim::ATTACK_FIST_1H_OFF, anim::ATTACK_OFF,
anim::ATTACK_FIST_1H, anim::ATTACK_1H};
caps.resolvedMeleeOffHandFist = pick(meleeOffFistCands, 4);
}
{
static const uint32_t meleeOffPierceCands[] = {
anim::ATTACK_OFF_PIERCE, anim::ATTACK_OFF,
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H};
caps.resolvedMeleeOffHandPierce = pick(meleeOffPierceCands, 4);
}
{
static const uint32_t meleeOffUnarmedCands[] = {
anim::ATTACK_UNARMED_OFF, anim::ATTACK_UNARMED,
anim::ATTACK_OFF, anim::ATTACK_1H};
caps.resolvedMeleeOffHandUnarmed = pick(meleeOffUnarmedCands, 4);
}
caps.hasMelee = (caps.resolvedMelee1H != 0 || caps.resolvedMeleeUnarmed != 0);
// ── Ready stances ───────────────────────────────────────────────────
{
static const uint32_t ready1HCands[] = {
anim::READY_1H, anim::READY_2H, anim::READY_UNARMED};
caps.resolvedReady1H = pick(ready1HCands, 3);
}
{
static const uint32_t ready2HCands[] = {
anim::READY_2H, anim::READY_2H_LOOSE, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReady2H = pick(ready2HCands, 4);
}
{
static const uint32_t ready2HLooseCands[] = {
anim::READY_2H_LOOSE, anim::READY_2H, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReady2HLoose = pick(ready2HLooseCands, 4);
}
{
static const uint32_t readyUnarmedCands[] = {
anim::READY_UNARMED, anim::READY_1H, anim::READY_FIST};
caps.resolvedReadyUnarmed = pick(readyUnarmedCands, 3);
}
{
static const uint32_t readyFistCands[] = {
anim::READY_FIST_1H, anim::READY_FIST, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReadyFist = pick(readyFistCands, 4);
}
{
static const uint32_t readyBowCands[] = {
anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReadyBow = pick(readyBowCands, 3);
}
{
static const uint32_t readyRifleCands[] = {
anim::READY_RIFLE, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReadyRifle = pick(readyRifleCands, 4);
}
{
static const uint32_t readyCrossbowCands[] = {
anim::READY_CROSSBOW, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReadyCrossbow = pick(readyCrossbowCands, 4);
}
{
static const uint32_t readyThrownCands[] = {
anim::READY_THROWN, anim::READY_1H, anim::READY_UNARMED};
caps.resolvedReadyThrown = pick(readyThrownCands, 3);
}
// ── Ranged attacks ──────────────────────────────────────────────────
{
static const uint32_t fireBowCands[] = {anim::FIRE_BOW, anim::ATTACK_BOW};
caps.resolvedFireBow = pick(fireBowCands, 2);
}
caps.resolvedAttackRifle = has(anim::ATTACK_RIFLE) ? anim::ATTACK_RIFLE : 0;
{
static const uint32_t attackCrossbowCands[] = {anim::ATTACK_CROSSBOW, anim::ATTACK_BOW};
caps.resolvedAttackCrossbow = pick(attackCrossbowCands, 2);
}
caps.resolvedAttackThrown = has(anim::ATTACK_THROWN) ? anim::ATTACK_THROWN : 0;
caps.resolvedLoadBow = has(anim::LOAD_BOW) ? anim::LOAD_BOW : 0;
caps.resolvedLoadRifle = has(anim::LOAD_RIFLE) ? anim::LOAD_RIFLE : 0;
// ── Special attacks ─────────────────────────────────────────────────
caps.resolvedSpecial1H = has(anim::SPECIAL_1H) ? anim::SPECIAL_1H : 0;
caps.resolvedSpecial2H = has(anim::SPECIAL_2H) ? anim::SPECIAL_2H : 0;
caps.resolvedSpecialUnarmed = has(anim::SPECIAL_UNARMED) ? anim::SPECIAL_UNARMED : 0;
caps.resolvedShieldBash = has(anim::SHIELD_BASH) ? anim::SHIELD_BASH : 0;
// ── Combat idle ─────────────────────────────────────────────────────
// Base combat idle — weapon-specific stances are resolved per-frame
// using ready stance fields above
caps.resolvedCombatIdle = has(anim::READY_1H) ? anim::READY_1H
: (has(anim::READY_UNARMED) ? anim::READY_UNARMED : 0);
// ── Activity animations ─────────────────────────────────────────────
caps.resolvedStandWound = has(anim::STAND_WOUND) ? anim::STAND_WOUND : 0;
caps.resolvedDeath = has(anim::DEATH) ? anim::DEATH : 0;
caps.hasDeath = (caps.resolvedDeath != 0);
caps.resolvedLoot = has(anim::LOOT) ? anim::LOOT : 0;
caps.resolvedSitDown = has(anim::SIT_GROUND_DOWN) ? anim::SIT_GROUND_DOWN : 0;
caps.resolvedSitLoop = has(anim::SITTING) ? anim::SITTING : 0;
caps.resolvedSitUp = has(anim::SIT_GROUND_UP) ? anim::SIT_GROUND_UP : 0;
caps.resolvedKneel = has(anim::KNEEL_LOOP) ? anim::KNEEL_LOOP : 0;
// ── Stealth ─────────────────────────────────────────────────────────
caps.resolvedStealthIdle = has(anim::STEALTH_STAND) ? anim::STEALTH_STAND : 0;
caps.resolvedStealthWalk = has(anim::STEALTH_WALK) ? anim::STEALTH_WALK : 0;
caps.resolvedStealthRun = has(anim::STEALTH_RUN) ? anim::STEALTH_RUN : 0;
caps.hasStealth = (caps.resolvedStealthIdle != 0);
// ── Misc ────────────────────────────────────────────────────────────
caps.resolvedMount = has(anim::MOUNT) ? anim::MOUNT : 0;
caps.hasMount = (caps.resolvedMount != 0);
caps.resolvedUnsheathe = has(anim::UNSHEATHE) ? anim::UNSHEATHE : 0;
{
static const uint32_t sheatheCands[] = {anim::SHEATHE, anim::HIP_SHEATHE};
caps.resolvedSheathe = pick(sheatheCands, 2);
}
caps.resolvedStun = has(anim::STUN) ? anim::STUN : 0;
caps.resolvedCombatWound = has(anim::COMBAT_WOUND) ? anim::COMBAT_WOUND : 0;
return caps;
}
AnimCapabilitySet AnimCapabilityProbe::probeMountModel(Renderer* /*renderer*/, uint32_t /*mountInstanceId*/) {
// Mount models use M2Renderer, not CharacterRenderer
// For now, mount capabilities are handled separately via MountAnimSet discovery
// This stub returns an empty set — mount animations are discovered in setMounted()
AnimCapabilitySet caps;
return caps;
}
} // namespace rendering
} // namespace wowee