# 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**: ```cpp 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) - 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) ```cpp 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` ```cpp 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) ```cpp 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) ```cpp 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): ```cpp 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: ```cpp 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 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_INFO` → `SgcPosInfo` (C# struct) - Hook names come from `.sgc` file parsing - Hook lookup uses `SkeletonBuilder.GetHook(name, recursive)` --- **End of Document**