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_aHooksarray (APtrArray<A3DSkeletonHook*>) - Uses case-insensitive string comparison (
stricmp) - Hook names are loaded from skeleton file (
.skeor.bonfile) - Each hook has a name (e.g., "hand_r", "hand_l", "weapon", "head")
- Returns
NULLif hook not found
Hook Storage:
- Hooks are loaded from skeleton files during model loading
- Each hook is attached to a bone (
m_iBoneindex) - 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_INFOstructure in composer SGC_POS_INFOcontains: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
szHookis 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 positionbChildHook = true: Searches in child model (weapon/pet) instead of main modelszHanger: 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_vHostPosandm_vTargetPosare updated every frame viaCECSkillGfxEvent::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
-
Configuration: Hook info stored in
.sgcfiles (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")
-
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 -
Runtime Lookup:
_get_pos_by_id()called every frame inCECSkillGfxEvent::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
-
Position Calculation:
- Relative (
bRelHook = true):hookWorldMatrix * offset→ offset rotates with hook - Absolute (
bRelHook = false):modelWorldMatrix * offsetthen translate to hook position
- Relative (
-
Fallback: If hook lookup fails, uses default position (AABB center or bottom)
-
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)
- Fly GFX spawns at
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_aHooksarray - Comparison: Case-insensitive string compare using
stricmp(pHook->GetName(), szName) - Storage: Hooks stored in
APtrArray<A3DSkeletonHook*> m_aHooks - Hook Names: Loaded from skeleton files (
.skeor.bonfiles) 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:
-
_get_pos_by_id()→HookUtils.GetHookWorldPosition()- Already planned in conversion documents
- Needs model transform parameter for absolute offset
-
CECSkillGfxEvent::Tick()→CECSkillGfxEvent.Tick()- Update positions every frame using hook system
- Pass composer's
SGC_POS_INFOto hook lookup
-
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
.sgcfile parsing - Hook lookup uses
SkeletonBuilder.GetHook(name, recursive)
End of Document