Files
test/Documentation/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md
2026-03-13 16:03:47 +07:00

18 KiB

Hook System Flow - C++ Analysis

Date Created: 2026-02-24
Purpose: Complete flow analysis of how player skill casting spawns GFX and uses hook system to attach GFX to correct bone positions


Complete Flow Diagram

1. Player Casts Skill
   ↓
2. CECAttacksMan::AddSkillAttack() [EC_ManAttacks.cpp:657]
   ↓
3. CECAttackEvent Created (with timing: m_timeToBeFired, m_timeToDoDamage)
   ↓
4. CECAttackEvent::Tick() [EC_ManAttacks.cpp:458]
   - Updates m_timeLived
   - When m_timeToBeFired <= 0 → calls DoFire()
   ↓
5. CECAttackEvent::DoFire() [EC_ManAttacks.cpp:111]
   - For Player skills (m_idSkill != 0):
     ↓
6. A3DSkillGfxComposerMan::Play() [EC_ManAttacks.cpp:137-139]
   - Finds composer by skill ID
   ↓
7. A3DSkillGfxComposer::Play() [A3DSkillGfxComposer2.cpp:453]
   - Iterates through all targets
   - For each target → calls AddOneTarget()
   ↓
8. A3DSkillGfxComposer::AddOneTarget() [A3DSkillGfxComposer2.cpp:388]
   - Determines host/target based on TargetMode
   - Calculates scale
   - Calls AddSkillGfxEvent()
   ↓
9. A3DSkillGfxMan::AddSkillGfxEvent() [A3DSkillGfxEvent2.cpp:678]
   - Creates multiple GFX events (clustering support)
   - For each event → calls AddOneSkillGfxEvent()
   ↓
10. A3DSkillGfxMan::AddOneSkillGfxEvent() [A3DSkillGfxEvent2.cpp:603]
    - Creates A3DSkillGfxEvent object
    - Loads fly GFX and hit GFX
    - Pushes event to update list
    ↓
11. CECSkillGfxEvent::Tick() [EC_ManSkillGfx.cpp:307]
    - **THIS IS WHERE HOOK SYSTEM IS USED**
    - Updates host and target positions using hooks
    ↓
12. _get_pos_by_id() [EC_ManSkillGfx.cpp:10]
    - **HOOK LOOKUP AND POSITION CALCULATION**
    - Gets hook from skeleton by string name
    - Calculates world position using hook transform
    ↓
12a. A3DSkinModel::GetSkeletonHook() [A3DSkinModel.cpp:2357]
    - Searches in main skeleton first
    - Optionally searches child models (recursive)
    ↓
12b. A3DSkeleton::GetHook() [A3DSkeleton.cpp:876]
    - **STRING-BASED HOOK LOOKUP**
    - Iterates through m_aHooks array
    - Compares hook names using stricmp() (case-insensitive)
    - Returns A3DSkeletonHook* if found
    ↓
12c. A3DSkeletonHook::GetAbsoluteTM() [A3DSkeleton.cpp:209]
    - Returns world transform matrix
    - Updates hook transform if needed
    ↓
13. A3DSkillGfxEvent::Tick() [A3DSkillGfxEvent2.cpp:487]
    - State machine: Wait → Flying → Hit → Finished
    - Updates fly GFX position/rotation each frame
    - When hit → spawns hit GFX

Detailed Hook System Usage

Step 12b: A3DSkeleton::GetHook() - String-Based Hook Lookup

File: A3DSkeleton.cpp:876-903

This is the core function that looks up hooks by string name:

A3DSkeletonHook* A3DSkeleton::GetHook(const char* szName, int* piIndex)
{
    if (!szName)
        return NULL;

    // Try index optimization first (if index provided and valid)
    if (piIndex && *piIndex >= 0 && *piIndex < m_aHooks.GetSize())
    {
        A3DSkeletonHook* pHook = m_aHooks[*piIndex];
        if (!stricmp(pHook->GetName(), szName))  // Case-insensitive compare
            return pHook;
    }

    // Enumerate all hooks and compare names
    for (int i=0; i < m_aHooks.GetSize(); i++)
    {
        A3DSkeletonHook* pHook = m_aHooks[i];
        if (!stricmp(pHook->GetName(), szName))  // Case-insensitive string compare
        {
            if (piIndex)
                *piIndex = i;  // Update index for future optimization
            
            return pHook;  // Found hook by name!
        }
    }

    return NULL;  // Hook not found
}

Key Points:

  • Hooks are stored in m_aHooks array (APtrArray<A3DSkeletonHook*>)
  • Uses case-insensitive string comparison (stricmp)
  • Hook names are loaded from skeleton file (.ske or .bon file)
  • Each hook has a name (e.g., "hand_r", "hand_l", "weapon", "head")
  • Returns NULL if hook not found

Hook Storage:

  • Hooks are loaded from skeleton files during model loading
  • Each hook is attached to a bone (m_iBone index)
  • Hook has local transform (m_matHookTM) relative to bone
  • Hook name is stored in A3DSkeletonHook::m_strName

Step 12a: A3DSkinModel::GetSkeletonHook() - Hook Search with Child Model Support

File: A3DSkinModel.cpp:2357 (referenced in HOOK_SYSTEM_C++_REFERENCE.md)

A3DSkeletonHook* A3DSkinModel::GetSkeletonHook(const char* szName, bool bNoChild)
{
    A3DSkeletonHook* pHook = NULL;
    
    // Search in main skeleton first
    if (m_pA3DSkeleton) {
        if ((pHook = m_pA3DSkeleton->GetHook(szName, NULL)))  // Calls GetHook() by name
            return pHook;
    }
    
    // Search in child models (if bNoChild == false, recursive)
    if (!bNoChild) {
        for (int i = 0; i < m_aChildModels.GetSize(); i++) {
            A3DSkinModel* pChild = m_aChildModels[i];
            if ((pHook = pChild->GetSkeletonHook(szName, false)))  // Recursive search
                return pHook;
        }
    }
    
    return NULL;
}

Key Points:

  • First searches main skeleton using A3DSkeleton::GetHook(szName, NULL)
  • If not found and bNoChild == false, searches child models recursively
  • Child models are weapons, pets, etc. attached to main model
  • This allows hooks on weapons/pets to be found

Step 11: CECSkillGfxEvent::Tick() - Hook Position Updates

File: EC_ManSkillGfx.cpp:307-373

void CECSkillGfxEvent::Tick(DWORD dwDeltaTime)
{
    if (A3DSkillGfxComposer* pComposer = GetComposer())
    {
        const SGC_POS_INFO *pHostPos, *pTargetPos;
        
        // Determine which position info to use (reverse mode support)
        if (m_pMoveMethod->IsReverse())
        {
            pHostPos = &m_pComposer->m_FlyEndPos;  // Target becomes host
            pTargetPos = &m_pComposer->m_FlyPos;   // Host becomes target
        }
        else
        {
            pHostPos = &m_pComposer->m_FlyPos;     // Normal: host is caster
            pTargetPos = &m_pComposer->m_FlyEndPos; // Normal: target is receiver
        }

        // UPDATE HOST POSITION USING HOOK
        m_bHostExist = _get_pos_by_id(
            m_pPlayerMan,
            m_pNPCMan,
            m_nHostID,
            m_vHostPos,                    // Output: world position
            pHostPos->HitPos,              // HitPos enum (enumHitCenter, enumHitBottom)
            m_bIsGoblinSkill,
            pHostPos->szHook,              // Hook name (e.g., "hand_r", "weapon")
            pHostPos->bRelHook,            // Relative offset flag
            &pHostPos->vOffset,            // Offset vector
            pHostPos->szHanger,            // Child model name (weapon, pet)
            pHostPos->bChildHook);          // Use child model flag
        
        // UPDATE TARGET POSITION USING HOOK
        m_bTargetExist = _get_pos_by_id(
            m_pPlayerMan,
            m_pNPCMan,
            m_nTargetID,
            m_vTargetPos,                 // Output: world position
            pTargetPos->HitPos,
            false,
            pTargetPos->szHook,             // Hook name for target
            pTargetPos->bRelHook,
            &pTargetPos->vOffset,
            pTargetPos->szHanger,
            pTargetPos->bChildHook);

        // Get target direction/up for orientation
        m_bTargetDirAndUpExist = _get_dir_and_up_by_id(
            m_pPlayerMan, m_pNPCMan, m_nTargetID, 
            m_vTargetDir, m_vTargetUp);
    }
    else
    {
        // Fallback: no composer, use default positions
        m_bHostExist = _get_pos_by_id(..., m_pMoveMethod->GetHitPos(), ...);
        m_bTargetExist = _get_pos_by_id(..., m_pMoveMethod->GetHitPos());
    }

    // Call base class tick (handles movement and GFX updates)
    A3DSkillGfxEvent::Tick(dwDeltaTime);
}

Key Points:

  • Hook information comes from SGC_POS_INFO structure in composer
  • SGC_POS_INFO contains: szHook, vOffset, bRelHook, szHanger, bChildHook, HitPos
  • Position is updated every frame during GFX event lifetime

Step 12: _get_pos_by_id() - Hook Lookup and Position Calculation

Complete Hook Lookup Chain:

_get_pos_by_id(szHook="hand_r")
  ↓
A3DSkinModel::GetSkeletonHook("hand_r", true)
  ↓
A3DSkeleton::GetHook("hand_r", NULL)
  ↓
Iterates m_aHooks[] array
  ↓
stricmp(pHook->GetName(), "hand_r") == 0
  ↓
Returns A3DSkeletonHook* pointer
  ↓
pHook->GetAbsoluteTM() → World transform matrix
  ↓
Calculate position: vPos = pHook->GetAbsoluteTM() * offset

File: EC_ManSkillGfx.cpp:10-122

Player Branch (Lines 24-87)

if (ISPLAYERID(nID))
{
    CECPlayer* pPlayer = pPlayerMan->GetPlayer(nID);
    if (pPlayer)
    {
        // Hook lookup loop (only if szHook is provided)
        while (1)
        {
            if (!szHook)
                break;  // No hook specified, use default position

            // 1. Get player model
            CECModel* pModel = pPlayer->GetPlayerModel();
            if (!pModel)
                break;
            
            // 2. Handle child model (weapon/pet) if specified
            if (szHanger && bChildHook)
                pModel = pModel->GetChildModel(szHanger);
            
            if (!pModel)
                break;

            // 3. Get skeleton from model
            A3DSkinModel* pSkin = pModel->GetA3DSkinModel();
            
            // 4. LOOKUP HOOK BY NAME (non-recursive)
            A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true);  // true = non-recursive
            
            if (!pHook)
                break;

            // 5. CALCULATE POSITION USING HOOK TRANSFORM
            if (bRelHook)
            {
                // Relative offset: transform offset in hook's local space
                // C++: pHook->GetAbsoluteTM() * (*pOffset)
                vPos = pHook->GetAbsoluteTM() * (*pOffset);
            }
            else
            {
                // Absolute offset: transform offset in model space, then translate to hook
                // C++: vPos = pSkin->GetAbsoluteTM() * (*pOffset);
                //      vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
                vPos = pSkin->GetAbsoluteTM() * (*pOffset);
                vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
            }
            
            return true;  // Success: position calculated from hook
        }

        // Fallback: No hook or hook lookup failed, use default position
        if (HitPos == enumHitBottom)
            vPos = pPlayer->GetPos();
        else
        {
            const A3DAABB& aabb = pPlayer->GetPlayerAABB();
            vPos = aabb.Center;
            vPos.y += aabb.Extents.y * .5f;
        }
    }
}

NPC Branch (Lines 89-118)

else if (ISNPCID(nID))
{
    CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID);
    if (pNPC)
    {
        while (true)
        {
            // NPC hook lookup (handles child models internally)
            A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook);
            if (!pHook)
                break;
            
            A3DSkinModel* pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook);
            
            // Same position calculation as player
            if (bRelHook)
                vPos = pHook->GetAbsoluteTM() * (*pOffset);
            else
            {
                vPos = pSkin->GetAbsoluteTM() * (*pOffset);
                vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
            }
            return true;
        }
        
        // Fallback: default position
        if (HitPos == enumHitBottom)
            vPos = pNPC->GetPos();
        else
        {
            const A3DAABB& aabb = pNPC->GetPickAABB();
            vPos = aabb.Center;
            vPos.y += aabb.Extents.y * .5f;
        }
    }
}

Key Points:

  • Hook lookup is optional - if szHook is NULL or lookup fails, uses default position
  • bRelHook = true: Offset is in hook's local space (rotates with hook)
  • bRelHook = false: Offset is in model space, then translated to hook position
  • bChildHook = true: Searches in child model (weapon/pet) instead of main model
  • szHanger: Name of child model to search (e.g., "weapon", "pet")

Hook Data Source: SGC_POS_INFO

File: A3DSkillGfxComposer2.cpp:43-74

The hook information is loaded from skill GFX composer files (.sgc files):

struct SGC_POS_INFO
{
    char szHook[64];        // Hook name (e.g., "hand_r", "weapon", "head")
    A3DVECTOR3 vOffset;     // Offset from hook position
    bool bRelHook;          // Relative offset flag
    char szHanger[64];      // Child model name (weapon, pet)
    bool bChildHook;        // Use child model flag
    GfxHitPos HitPos;       // Fallback hit position enum
};

// Loaded from .sgc file:
_load_sgc_pos(dwVersion, pFile, szLine, m_FlyPos);      // Host position
_load_sgc_pos(dwVersion, pFile, szLine, m_FlyEndPos);  // Target position
_load_sgc_pos(dwVersion, pFile, szLine, m_HitPos);      // Hit position

Example .sgc file content:

Hook: hand_r          # Hook name for fly GFX spawn position
Pos: 0.0, 0.0, 0.0    # Offset from hook
RelHook: 1            # Use relative offset
Hanger: weapon        # Child model name (optional)
ChildHook: 1          # Search in child model
HitPos: 0             # Fallback hit position enum

GFX Position Updates During Flight

File: A3DSkillGfxEvent2.cpp:487-568

After hook positions are calculated, the GFX event updates the fly GFX transform:

void A3DSkillGfxEvent::Tick(DWORD dwDeltaTime)
{
    // ... state machine logic ...
    
    if (m_enumState == enumFlying)
    {
        // Update movement
        if (m_pMoveMethod->TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos))
            HitTarget(GetTargetCenter());  // Target hit
        else if (m_pFlyGfx)
        {
            // UPDATE FLY GFX TRANSFORM
            A3DVECTOR3 vDir, vUp;
            
            if (m_pMoveMethod->GetMode() == enumOnTarget && 
                m_pMoveMethod->IsReverse() && 
                GetTargetDirAndUp(vDir, vUp))
            {
                // Use target's direction/up for orientation
                m_pFlyGfx->SetParentTM(a3d_TransformMatrix(vDir, vUp, m_pMoveMethod->GetPos()));
            }
            else
            {
                // Use movement direction for orientation
                m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos()));
            }
            
            m_pMoveMethod->UpdateGfxParam(m_pFlyGfx, m_vHostPos, m_vTargetPos);
            m_pFlyGfx->TickAnimation(dwDeltaTime);
        }
    }
}

Key Points:

  • m_vHostPos and m_vTargetPos are updated every frame via CECSkillGfxEvent::Tick()
  • Fly GFX position is calculated by movement method (m_pMoveMethod->GetPos())
  • Fly GFX rotation uses movement direction or target's direction/up
  • Host position (m_vHostPos) comes from hook lookup (if hook specified)

Summary: Hook System Integration Points

  1. Configuration: Hook info stored in .sgc files (composer files)

    • FlyPos: Hook for fly GFX spawn position (host)
    • FlyEndPos: Hook for fly GFX target position (target)
    • HitPos: Hook for hit GFX spawn position
    • Hook names are strings (e.g., "hand_r", "weapon", "head")
  2. String-Based Hook Lookup Chain:

    Hook name from .sgc file (e.g., "hand_r")
      ↓
    _get_pos_by_id(szHook="hand_r")
      ↓
    A3DSkinModel::GetSkeletonHook("hand_r", true)
      ↓
    A3DSkeleton::GetHook("hand_r", NULL)  ← **STRING LOOKUP HERE**
      ↓
    Iterates m_aHooks[] array
      ↓
    stricmp(pHook->GetName(), "hand_r") == 0  ← **CASE-INSENSITIVE COMPARE**
      ↓
    Returns A3DSkeletonHook* pointer
    
  3. Runtime Lookup: _get_pos_by_id() called every frame in CECSkillGfxEvent::Tick()

    • Looks up hook by string name from skeleton's hook array
    • Uses case-insensitive string comparison (stricmp)
    • Supports child models (weapons, pets) via recursive search
    • Calculates world position using hook transform matrix
  4. Position Calculation:

    • Relative (bRelHook = true): hookWorldMatrix * offset → offset rotates with hook
    • Absolute (bRelHook = false): modelWorldMatrix * offset then translate to hook position
  5. Fallback: If hook lookup fails, uses default position (AABB center or bottom)

  6. GFX Attachment:

    • Fly GFX spawns at m_vHostPos (from hook if specified)
    • Fly GFX moves toward m_vTargetPos (from hook if specified)
    • Hit GFX spawns at GetTargetCenter() (from hook if specified)

Key Implementation Detail: String-Based Hook Lookup

The critical missing piece in the flow is A3DSkeleton::GetHook(const char* szName, int* piIndex):

  • Location: A3DSkeleton.cpp:876-903
  • Method: Linear search through m_aHooks array
  • Comparison: Case-insensitive string compare using stricmp(pHook->GetName(), szName)
  • Storage: Hooks stored in APtrArray<A3DSkeletonHook*> m_aHooks
  • Hook Names: Loaded from skeleton files (.ske or .bon files) during model loading
  • Performance: O(n) lookup, but typically only 5-20 hooks per skeleton

Example Hook Names:

  • "hand_r" - Right hand
  • "hand_l" - Left hand
  • "weapon" - Weapon attachment point
  • "head" - Head position
  • "foot_r" - Right foot
  • "foot_l" - Left foot

C++ to C# Conversion Notes

Key Functions to Convert:

  1. _get_pos_by_id()HookUtils.GetHookWorldPosition()

    • Already planned in conversion documents
    • Needs model transform parameter for absolute offset
  2. CECSkillGfxEvent::Tick()CECSkillGfxEvent.Tick()

    • Update positions every frame using hook system
    • Pass composer's SGC_POS_INFO to hook lookup
  3. A3DSkeletonHook::GetAbsoluteTM()Transform (Unity)

    • C++ returns matrix, Unity uses Transform component
    • Hook transforms are already stored in SkeletonBuilder

Data Structures:

  • SGC_POS_INFOSgcPosInfo (C# struct)
  • Hook names come from .sgc file parsing
  • Hook lookup uses SkeletonBuilder.GetHook(name, recursive)

End of Document