add hook system

This commit is contained in:
VDH
2026-03-03 18:06:20 +07:00
parent 2c51af9c95
commit 52ac4d4b39
4 changed files with 633 additions and 52 deletions
@@ -10,7 +10,6 @@ namespace BrewMonster.Scripts
public Transform rootBone;
public Dictionary<string, Transform> hooks = new Dictionary<string, Transform>();
/// <summary>
/// This function retrieves an array of transforms for the specified bone names.
@@ -67,12 +66,21 @@ namespace BrewMonster.Scripts
if (hooks.TryGetValue(hookName, out Transform hook))
return hook;
// Recursive search in child models (Phase 2)
// 在子模型中递归搜索(阶段2
// Recursive search in child models (if recursive flag is true)
// 在子模型中递归搜索(如果递归标志为true
if (recursive)
{
// TODO Phase 2: Search child models
// TODO 阶段2:搜索子模型
// Search in child SkeletonBuilders (weapons, pets, etc.)
// 在子SkeletonBuilder中搜索(武器、宠物等)
SkeletonBuilder[] childBuilders = GetComponentsInChildren<SkeletonBuilder>(true);
foreach (SkeletonBuilder childBuilder in childBuilders)
{
if (childBuilder == this) continue; // Skip self
Transform childHook = childBuilder.GetHook(hookName, false); // Don't recurse again
if (childHook != null)
return childHook;
}
}
return null;
@@ -365,7 +365,8 @@ namespace BrewMonster
// 这与C++逻辑匹配:当投射物击中地面(无目标)时使用m_szHitGrndGfx
bool bTargetExists = m_bTargetExist && m_nTargetID != 0;
GameObject prefab = bTargetExists ? m_pComposer.GetHitGFX() : m_pComposer.GetHitGrdGFX();
BMLogger.LogError("HitGfx : " + m_pComposer.hitGfxName);
if (prefab == null)
{
// Fallback: if ground hit GFX is null but target doesn't exist, try regular hit GFX
@@ -465,11 +466,66 @@ namespace BrewMonster
{
if (bIsGoblinSkill)
{
// TODO: Handle goblin skill position
// if (pPlayer->GetGoblinModel())
// vPos = pPlayer->GetGoblinModel()->GetModel()->GetModelAABB().Center;
// else
// return false;
// Get goblin model from player
// 从玩家获取小精灵模型
// Note: GetGoblin() method may need to be implemented in CECPlayer
// 注意:GetGoblin()方法可能需要在CECPlayer中实现
// For now, we'll try to get it via a common pattern
// 目前,我们将尝试通过通用模式获取它
// Try to find goblin model - this is a placeholder until GetGoblin() is implemented
// 尝试查找小精灵模型 - 这是占位符,直到实现GetGoblin()
// TODO: Implement GetGoblin() in CECPlayer when goblin system is available
// TODO: 当小精灵系统可用时,在CECPlayer中实现GetGoblin()
// For Phase 3, we'll search for a child model named "goblin" or similar
// 对于第3阶段,我们将搜索名为"goblin"或类似的子模型
CECModel pModel = pPlayer.GetPlayerModel();
if (pModel != null)
{
// Try common goblin hanger names
// 尝试常见的小精灵挂载者名称
CECModel goblinModel = pModel.GetChildModel("goblin") ??
pModel.GetChildModel("pet") ??
pModel.GetChildModel("_goblin");
if (goblinModel != null)
{
// Use hook if specified, otherwise use model center
// 如果指定了挂点则使用挂点,否则使用模型中心
if (!string.IsNullOrEmpty(szHook))
{
Transform pHook = goblinModel.GetHook(szHook, true);
if (pHook != null)
{
Transform modelTransform = goblinModel.transform;
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
#if UNITY_EDITOR
BMLogger.Log($"[HOOK_DEBUG] Found goblin hook '{szHook}' for player ID {nID}, position={vPos}");
#endif
return true;
}
}
// Fallback to goblin position
// 回退到小精灵位置
if (goblinModel.transform != null)
{
vPos = goblinModel.transform.position;
vPos.y += 0.5f;
#if UNITY_EDITOR
BMLogger.Log($"[HOOK_DEBUG] Using goblin center position for player ID {nID}, position={vPos}");
#endif
return true;
}
}
}
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] Goblin model not found for player ID {nID}");
#endif
return false;
}
else
@@ -481,33 +537,55 @@ namespace BrewMonster
if (string.IsNullOrEmpty(szHook))
break;
// TODO: Get player model and hook position
/*CECModel pModel = pPlayer->GetPlayerModel();
if (!pModel)
break;
if (szHanger && bChildHook)
pModel = pModel->GetChildModel(szHanger);
if (!pModel)
break;
A3DSkinModel* pSkin = pModel->GetA3DSkinModel();
A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true);
if (!pHook)
break;
if (bRelHook)
vPos = pHook->GetAbsoluteTM() * pOffset;
else
// Get player model and hook position
// 获取玩家模型和挂点位置
CECModel pModel = pPlayer.GetPlayerModel();
if (pModel == null)
{
vPos = pSkin->GetAbsoluteTM() * pOffset;
vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] Player model not found for ID {nID}, hook '{szHook}'");
#endif
break;
}
return true;*/
break;
// Handle child model (hanger) if specified
// 如果指定了子模型(挂载者),则处理
if (!string.IsNullOrEmpty(szHanger) && bChildHook)
{
pModel = pModel.GetChildModel(szHanger);
if (pModel == null)
{
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] Child model '{szHanger}' not found for player ID {nID}, hook '{szHook}'");
#endif
break;
}
}
// Get hook Transform (non-recursive search as per C++: GetSkeletonHook(szHook, true))
// 获取挂点变换(非递归搜索,对应C++GetSkeletonHook(szHook, true)
Transform pHook = pModel.GetHook(szHook, false);
if (pHook == null)
{
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] Hook '{szHook}' not found for player ID {nID}, falling back to center position");
#endif
break;
}
// Get model transform for absolute offset calculation
// 获取模型变换用于绝对偏移计算
Transform modelTransform = pModel.transform;
// Calculate position based on relative/absolute offset
// 根据相对/绝对偏移计算位置
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
#if UNITY_EDITOR
BMLogger.Log($"[HOOK_DEBUG] Found hook '{szHook}' for player ID {nID}, position={vPos}, relative={bRelHook}, offset={pOffset}");
#endif
return true;
}
if (HitPos == GfxHitPos.enumHitBottom)
@@ -539,20 +617,58 @@ namespace BrewMonster
{
while (true)
{
// TODO: Get NPC hook position
/*A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook);
if (!pHook)
if (string.IsNullOrEmpty(szHook))
break;
A3DSkinModel *pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook);
if (bRelHook)
vPos = pHook->GetAbsoluteTM() * pOffset;
else
// Get NPC model
// 获取NPC模型
CECModel pModel = pNPC.GetModel();
if (pModel == null)
{
vPos = pSkin->GetAbsoluteTM() * pOffset;
vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] NPC model not found for ID {nID}, hook '{szHook}'");
#endif
break;
}
return true;*/
break;
// Handle child model (hanger) if specified
// 如果指定了子模型(挂载者),则处理
if (!string.IsNullOrEmpty(szHanger) && bChildHook)
{
pModel = pModel.GetChildModel(szHanger);
if (pModel == null)
{
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] Child model '{szHanger}' not found for NPC ID {nID}, hook '{szHook}'");
#endif
break;
}
}
// Get hook Transform (non-recursive search as per C++: GetSkeletonHook(szHook, true))
// 获取挂点变换(非递归搜索,对应C++GetSkeletonHook(szHook, true)
Transform pHook = pModel.GetHook(szHook, false);
if (pHook == null)
{
#if UNITY_EDITOR
BMLogger.LogWarning($"[HOOK_DEBUG] Hook '{szHook}' not found for NPC ID {nID}, falling back to center position");
#endif
break;
}
// Get model transform for absolute offset calculation
// 获取模型变换用于绝对偏移计算
Transform modelTransform = pModel.transform;
// Calculate position based on relative/absolute offset
// 根据相对/绝对偏移计算位置
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
#if UNITY_EDITOR
BMLogger.Log($"[HOOK_DEBUG] Found hook '{szHook}' for NPC ID {nID}, position={vPos}, relative={bRelHook}, offset={pOffset}");
#endif
return true;
}
if (HitPos == GfxHitPos.enumHitBottom)
+70 -6
View File
@@ -367,6 +367,7 @@ public class CECModel
protected CECModelStaticData m_pMapModel;
private SkeletonBuilder m_skeletonBuilder;
private Dictionary<string, Transform> m_hookCache = new Dictionary<string, Transform>();
private Dictionary<string, CECModel> m_childModels = new Dictionary<string, CECModel>();
private Transform m_transform;
public void ClearComActFlag(bool bSignalCurrent) { ClearComActFlag(0, bSignalCurrent); }
public void ClearComActFlag(int nChannel, bool bSignalCurrent)
@@ -532,6 +533,51 @@ public class CECModel
m_hookCache.Clear();
}
/// <summary>
/// Register a child model (weapon, pet, etc.)
/// 注册子模型(武器、宠物等)
/// </summary>
/// <param name="hangerName">Hanger name / 挂载者名称</param>
/// <param name="childModel">Child model instance / 子模型实例</param>
public void RegisterChildModel(string hangerName, CECModel childModel)
{
if (!string.IsNullOrEmpty(hangerName) && childModel != null)
{
m_childModels[hangerName] = childModel;
BMLogger.Log($"[CECModel] Registered child model '{hangerName}'");
}
}
/// <summary>
/// Unregister a child model
/// 注销子模型
/// </summary>
/// <param name="hangerName">Hanger name / 挂载者名称</param>
public void UnregisterChildModel(string hangerName)
{
if (!string.IsNullOrEmpty(hangerName))
{
if (m_childModels.Remove(hangerName))
{
BMLogger.Log($"[CECModel] Unregistered child model '{hangerName}'");
}
}
}
/// <summary>
/// Get child model by hanger name
/// 根据挂载者名称获取子模型
/// </summary>
/// <param name="hangerName">Hanger name / 挂载者名称</param>
/// <returns>Child model or null if not found / 子模型,未找到返回null</returns>
public CECModel GetChildModel(string hangerName)
{
if (string.IsNullOrEmpty(hangerName))
return null;
return m_childModels.TryGetValue(hangerName, out CECModel child) ? child : null;
}
public void PlayGfx(string szPath, string szHook, float fScale, bool bFadeOut, A3DVECTOR3 vOffset, float fPitch, float fYaw, float fRot, bool bUseECMHook, uint dwFadeOutTime)
{
if (!bFadeOut)
@@ -539,16 +585,34 @@ public class CECModel
string strKey = szPath;
strKey += szHook;
// Apply hook scale factor if using ECM hook
// 如果使用ECM挂点,应用挂点缩放因子
if (bUseECMHook && !string.IsNullOrEmpty(szHook))
{
Transform pHook = GetHook(szHook, true);
if (pHook != null)
{
// Apply hook scale factor if available
// 如果可用,应用挂点缩放因子
// TODO Phase 4: Add hook scale factor support
// TODO 第4阶段:添加挂点缩放因子支持
// fScale *= pHook->GetScaleFactor();
#if UNITY_EDITOR
BMLogger.Log($"[CECModel.PlayGfx] Using hook '{szHook}' for GFX '{szPath}', scale={fScale}");
#endif
}
else
{
#if UNITY_EDITOR
BMLogger.LogWarning($"[CECModel.PlayGfx] Hook '{szHook}' not found for GFX '{szPath}', using default scale");
#endif
}
}
// PGFX_INFO pInfo;
//
// CoGfxMap::iterator it = m_CoGfxMap.find(strKey);
// if (bUseECMHook) {
// if (CECModelHook* pHook = GetECMHook(szHook))
// {
// fScale *= pHook->GetScaleFactor();
// }
// }
//
// if (it != m_CoGfxMap.end())
// {
+393
View File
@@ -0,0 +1,393 @@
# 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
- Calculates world position using hook transform
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 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
**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
2. **Runtime Lookup:** `_get_pos_by_id()` called every frame in `CECSkillGfxEvent::Tick()`
- Looks up hook by name from skeleton
- Supports child models (weapons, pets)
- Calculates world position using hook transform matrix
3. **Position Calculation:**
- **Relative (`bRelHook = true`)**: `hookWorldMatrix * offset` → offset rotates with hook
- **Absolute (`bRelHook = false`)**: `modelWorldMatrix * offset` then translate to hook position
4. **Fallback:** If hook lookup fails, uses default position (AABB center or bottom)
5. **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)
---
## 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**