From d5e854eef7cbc3c1988d7eb250d4ebc9b847681f Mon Sep 17 00:00:00 2001 From: VDH Date: Tue, 3 Mar 2026 15:27:03 +0700 Subject: [PATCH] hook system convert plan --- HOOK_SYSTEM_C++_REFERENCE.md | 408 +++++++++++++++++++++++++++++++++ HOOK_SYSTEM_CONVERSION_PLAN.md | 221 ++++++++++++++++-- 2 files changed, 616 insertions(+), 13 deletions(-) create mode 100644 HOOK_SYSTEM_C++_REFERENCE.md diff --git a/HOOK_SYSTEM_C++_REFERENCE.md b/HOOK_SYSTEM_C++_REFERENCE.md new file mode 100644 index 0000000000..47880c79a3 --- /dev/null +++ b/HOOK_SYSTEM_C++_REFERENCE.md @@ -0,0 +1,408 @@ +# Hook System C++ Reference & Conversion Verification + +This document shows the actual C++ implementation of the Hook System and verifies the conversion plan matches it correctly. + +**Date Created:** 2026-02-24 + +--- + +## 1. C++ Hook System Architecture + +### 1.1 Core Classes + +#### A3DSkeletonHook (Angelica2/Angelica3D/Source/A3DSkeleton.cpp) + +```cpp +class A3DSkeletonHook { + A3DSkeleton* m_pA3DSkeleton; // Parent skeleton + int m_iBone; // Bone index (-1 = root) + A3DMATRIX4 m_matHookTM; // Local hook transform (relative to bone) + A3DMATRIX4 m_matScaledHookTM; // Scaled hook transform + A3DMATRIX4 m_matAbs; // Cached absolute world matrix + A3DMATRIX4 m_matNoScaleAbs; // Cached absolute matrix (no scale) + bool m_bFixDir; // Fix direction to skeleton + DWORD m_dwUpdateCnt; // Update counter + + // Get world transform matrix + const A3DMATRIX4& GetAbsoluteTM() { + Update(false); // Update if needed + return m_matAbs; + } + + // Update hook transform (called automatically) + void Update(bool bForce) { + if (m_iBone < 0) { + // Root bone hook + m_matAbs = m_matScaledHookTM * m_pA3DSkeleton->GetAbsoluteTM(); + } else { + // Bone-attached hook + A3DBone* pBone = m_pA3DSkeleton->GetBone(m_iBone); + m_matAbs = matWholeScale * (m_matScaledHookTM * pBone->GetNoScaleAbsTM()); + } + } +}; +``` + +**Key Points:** +- Hook has local transform (`m_matHookTM`) relative to bone +- `GetAbsoluteTM()` returns world-space transform matrix +- Automatically updates when skeleton animates +- Supports bone scaling + +#### A3DSkinModel::GetSkeletonHook() (A3DSkinModel.cpp:2357) + +```cpp +A3DSkeletonHook* A3DSkinModel::GetSkeletonHook(const char* szName, bool bNoChild) +{ + A3DSkeletonHook* pHook = NULL; + + // Search in main skeleton + if (m_pA3DSkeleton) { + if ((pHook = m_pA3DSkeleton->GetHook(szName, NULL))) + 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))) + return pHook; + } + } + + return NULL; +} +``` + +**Key Points:** +- `bNoChild = false` means **recursive search** (searches child models) +- `bNoChild = true` means **non-recursive** (only main skeleton) +- Searches child models (weapons, pets) if recursive + +--- + +## 2. C++ Hook Usage in Skill GFX + +### 2.1 Player Hook Lookup (CECSkillGfxEvent::get_pos_by_id) + +**C++ Code (from commented C# code):** +```cpp +// Player branch +CECModel* pModel = pPlayer->GetPlayerModel(); +if (!pModel) + break; + +// Handle child model (hanger) if specified +if (szHanger && bChildHook) + pModel = pModel->GetChildModel(szHanger); + +if (!pModel) + break; + +A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); +A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); // true = non-recursive + +if (!pHook) + break; + +// Calculate position +if (bRelHook) { + // Relative offset: transform offset in hook's local space + vPos = pHook->GetAbsoluteTM() * pOffset; // Matrix * Vector3 +} else { + // Absolute offset: offset in model space, then translate to hook position + vPos = pSkin->GetAbsoluteTM() * pOffset; + vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); +} + +return true; +``` + +**Logic Breakdown:** +1. Get player model +2. If `szHanger` and `bChildHook` are set, get child model (weapon/pet) +3. Get skeleton hook by name (non-recursive search) +4. Calculate position: + - **Relative (`bRelHook = true`)**: `hookWorldMatrix * offset` → offset in hook's local space + - **Absolute (`bRelHook = false`)**: `modelWorldMatrix * offset` then translate to hook position + +### 2.2 NPC Hook Lookup (CECNPC::GetSgcHook) + +**C++ Code (EC_NPCModel.cpp:505):** +```cpp +A3DSkeletonHook* CECNPCModelDefaultPolicy::GetSgcHook( + const char* szHanger, + bool bChildHook, + const char* szHook) +{ + if (A3DSkinModel* pSkinModel = GetSgcSkinModel(szHanger, bChildHook, szHook)) { + return pSkinModel->GetSkeletonHook(szHook, true); // true = non-recursive + } + return NULL; +} +``` + +**Usage in get_pos_by_id (NPC branch):** +```cpp +// NPC branch +A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); +if (!pHook) + break; + +A3DSkinModel* pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); +if (bRelHook) { + vPos = pHook->GetAbsoluteTM() * pOffset; +} else { + vPos = pSkin->GetAbsoluteTM() * pOffset; + vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); +} +return true; +``` + +--- + +## 3. C++ to C# Conversion Mapping + +### 3.1 Transform Matrix Operations + +| C++ Operation | C# Equivalent | Notes | +|---------------|---------------|-------| +| `pHook->GetAbsoluteTM()` | `hookTransform` (Unity Transform) | World transform | +| `pHook->GetAbsoluteTM() * pOffset` | `hookTransform.TransformPoint(offset)` | Relative offset | +| `pSkin->GetAbsoluteTM() * pOffset` | `skinTransform.TransformPoint(offset)` | Model-space offset | +| `pHook->GetAbsoluteTM().GetRow(3)` | `hookTransform.position` | Hook world position | +| `pSkin->GetAbsoluteTM().GetRow(3)` | `skinTransform.position` | Model world position | + +### 3.2 Hook Lookup + +| C++ | C# (Plan) | Status | +|-----|-----------|--------| +| `pSkin->GetSkeletonHook(szHook, true)` | `skeletonBuilder.GetHook(szHook, false)` | ⚠️ **INVERTED FLAG** | +| `pModel->GetChildModel(szHanger)` | `pModel.GetChildModel(szHanger)` | ✅ Matches | +| `pNPC->GetSgcHook(...)` | `pNPC.GetHook(...)` | ✅ Simplified | + +**⚠️ IMPORTANT: Flag Inversion** + +C++ `GetSkeletonHook(szHook, true)` means **non-recursive** (don't search children). +C# plan uses `GetHook(szHook, false)` which means **non-recursive** (don't recurse). + +**The conversion plan is CORRECT** - Unity's `recursive = false` matches C++'s `bNoChild = true`. + +### 3.3 Position Calculation + +**C++ Relative Offset:** +```cpp +vPos = pHook->GetAbsoluteTM() * pOffset; +``` + +**C# Equivalent (from plan):** +```csharp +vPos = hookTransform.TransformPoint(offset); // ✅ CORRECT +``` + +**C++ Absolute Offset:** +```cpp +vPos = pSkin->GetAbsoluteTM() * pOffset; +vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); +``` + +**C# Equivalent (from plan):** +```csharp +Vector3 hookWorldPos = hookTransform.position; +Vector3 offsetWorld = hookTransform.TransformDirection(offset); +vPos = hookWorldPos + offsetWorld; // ⚠️ NEEDS VERIFICATION +``` + +**Verification Needed:** The C++ absolute offset logic: +1. Transforms offset in **model's world space** (`pSkin->GetAbsoluteTM() * pOffset`) +2. Subtracts model position +3. Adds hook position + +This is equivalent to: **offset in model's local space, then translate to hook position**. + +**C# Correct Implementation:** +```csharp +// Get model transform (equivalent to pSkin) +Transform modelTransform = pModel.transform; // Or get from SkeletonBuilder root + +// Transform offset in model's world space +Vector3 offsetWorld = modelTransform.TransformPoint(offset); + +// Translate to hook position +vPos = offsetWorld - modelTransform.position + hookTransform.position; +``` + +**OR simpler (if offset is in model local space):** +```csharp +// Transform offset from model local to world +Vector3 offsetWorld = modelTransform.TransformDirection(offset); +// Add hook position +vPos = hookTransform.position + offsetWorld; +``` + +--- + +## 4. Conversion Plan Verification + +### 4.1 ✅ Correct Aspects + +1. **Hook Storage**: Plan correctly stores hooks in `Dictionary` ✅ +2. **Hook Lookup**: Plan correctly implements `GetHook()` with recursive flag ✅ +3. **Relative Offset**: Plan correctly uses `TransformPoint()` ✅ +4. **Child Model Support**: Plan correctly implements `GetChildModel()` ✅ +5. **Caching**: Plan correctly adds hook cache for performance ✅ + +### 4.2 ⚠️ Needs Correction + +1. **Absolute Offset Calculation** (HookUtils.cs:400-409) + +**Current Plan:** +```csharp +Vector3 hookWorldPos = hookTransform.position; +Vector3 offsetWorld = hookTransform.TransformDirection(offset); +return hookWorldPos + offsetWorld; +``` + +**Should Be:** +```csharp +// Need model transform reference +// C++: pSkin->GetAbsoluteTM() * pOffset +Vector3 offsetWorld = modelTransform.TransformPoint(offset); +// C++: vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3) +return offsetWorld - modelTransform.position + hookTransform.position; +``` + +**Problem:** `HookUtils.GetHookWorldPosition()` doesn't have access to model transform. + +**Solution:** Pass model transform as parameter, OR get it from hook's root parent. + +**Updated HookUtils:** +```csharp +public static Vector3 GetHookWorldPosition( + Transform hookTransform, + Vector3 offset, + bool bRelative, + Transform modelTransform = null) // Add model transform parameter +{ + if (hookTransform == null) + return Vector3.zero; + + if (bRelative) { + return hookTransform.TransformPoint(offset); + } else { + // Absolute offset: transform in model space, then translate to hook + if (modelTransform == null) { + // Fallback: use hook's root (skeleton root) + modelTransform = hookTransform.root; + } + + Vector3 offsetWorld = modelTransform.TransformPoint(offset); + return offsetWorld - modelTransform.position + hookTransform.position; + } +} +``` + +2. **GetSkeletonHook Flag Usage** + +**C++:** `GetSkeletonHook(szHook, true)` = non-recursive +**Plan:** `GetHook(szHook, true)` = recursive (inverted!) + +**Correction:** The plan's `recursive` parameter should match C++'s `bNoChild`: +- C++ `bNoChild = true` → C# `recursive = false` +- C++ `bNoChild = false` → C# `recursive = true` + +**Current plan uses `recursive = true` in calls, which is CORRECT** (matches C++ `bNoChild = false` for recursive search). + +**Verification:** +- C++: `GetSkeletonHook(szHook, true)` = non-recursive (only main skeleton) +- Plan: `GetHook(szHook, false)` = non-recursive ✅ **CORRECT** + +--- + +## 5. Updated Conversion Plan Corrections + +### 5.1 Fix HookUtils.GetHookWorldPosition() + +**File:** `Assets/PerfectWorld/Scripts/Utility/HookUtils.cs` + +**Update absolute offset calculation:** +```csharp +public static Vector3 GetHookWorldPosition( + Transform hookTransform, + Vector3 offset, + bool bRelative, + Transform modelTransform = null) // Add optional model transform +{ + if (hookTransform == null) + return Vector3.zero; + + if (bRelative) { + // Relative offset: transform offset from hook's local space to world space + // C++: pHook->GetAbsoluteTM() * pOffset + return hookTransform.TransformPoint(offset); + } else { + // Absolute offset: transform offset in model space, then translate to hook position + // C++: vPos = pSkin->GetAbsoluteTM() * pOffset; + // vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + + // Get model transform (skeleton root or provided) + if (modelTransform == null) { + // Fallback: use hook's root bone (skeleton root) + // Find SkeletonBuilder component in hierarchy + SkeletonBuilder skeleton = hookTransform.GetComponentInParent(); + modelTransform = skeleton != null ? skeleton.rootBone : hookTransform.root; + } + + // Transform offset in model's world space + Vector3 offsetWorld = modelTransform.TransformPoint(offset); + + // Translate to hook position + return offsetWorld - modelTransform.position + hookTransform.position; + } +} +``` + +### 5.2 Update get_pos_by_id() Calls + +**File:** `Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs` + +**Pass model transform for absolute offset:** +```csharp +// In player branch: +CECModel pModel = pPlayer.GetPlayerModel(); +Transform modelTransform = pModel.transform; // Or get from SkeletonBuilder + +Transform pHook = pModel.GetHook(szHook, true); +vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); +``` + +--- + +## 6. Summary + +### ✅ Conversion Plan is Mostly Correct + +The conversion plan correctly: +- Maps C++ hook lookup to Unity Transform system +- Implements recursive search correctly +- Handles relative offset correctly +- Supports child models + +### ⚠️ One Correction Needed + +**Absolute Offset Calculation:** +- Current plan uses `TransformDirection()` which is incorrect +- Should use `TransformPoint()` on model transform, then translate +- Need to pass model transform to `HookUtils.GetHookWorldPosition()` + +### 📝 Implementation Notes + +1. **Hook Lookup Flag**: C++ `bNoChild = true` → C# `recursive = false` ✅ +2. **Relative Offset**: `TransformPoint()` is correct ✅ +3. **Absolute Offset**: Needs model transform reference ⚠️ +4. **Child Models**: Plan correctly implements `GetChildModel()` ✅ + +--- + +**End of Document** diff --git a/HOOK_SYSTEM_CONVERSION_PLAN.md b/HOOK_SYSTEM_CONVERSION_PLAN.md index bc43a80459..716125cc56 100644 --- a/HOOK_SYSTEM_CONVERSION_PLAN.md +++ b/HOOK_SYSTEM_CONVERSION_PLAN.md @@ -9,6 +9,9 @@ This document provides a comprehensive plan to convert the Perfect World Hook Sy **Complexity:** Medium — Requires integration with existing skeleton system **Dependencies:** SkeletonBuilder.cs (✅ Complete), CECSkillGfxMan.cs (⚠️ Hook logic stubbed) +**Related Documents:** +- `HOOK_SYSTEM_C++_REFERENCE.md` - C++ implementation reference and conversion verification + --- ## Table of Contents @@ -265,6 +268,63 @@ Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook); m_skeletonBuilder = builder; m_hookCache?.Clear(); // Invalidate cache on model change } + + /// + /// Get the SkeletonBuilder component + /// 获取SkeletonBuilder组件 + /// + public SkeletonBuilder GetSkeletonBuilder() + { + return m_skeletonBuilder; + } + ``` + +2. **Add Auto-Initialization Logic:** + ```csharp + /// + /// Initialize SkeletonBuilder reference - call this after model is loaded + /// 初始化SkeletonBuilder引用 - 在模型加载后调用 + /// + public void InitializeSkeletonBuilder() + { + // Try to find SkeletonBuilder in children (where it's typically attached) + // 尝试在子对象中查找SkeletonBuilder(通常附加在那里) + m_skeletonBuilder = GetComponentInChildren(true); + + if (m_skeletonBuilder == null) + { + // Fallback: search in parent hierarchy (for NPCs/Players) + // 回退:在父层次结构中搜索(用于NPC/玩家) + m_skeletonBuilder = GetComponentInParent(); + } + + if (m_skeletonBuilder == null) + { + BMLogger.LogWarning($"[CECModel] SkeletonBuilder not found for {gameObject.name}. Hooks will not be available."); + } + else + { + // Clear cache when skeleton is set + // 设置骨架时清除缓存 + m_hookCache?.Clear(); + BMLogger.Log($"[CECModel] SkeletonBuilder initialized for {gameObject.name}"); + } + } + + /// + /// Auto-initialize on Awake if SkeletonBuilder is already available + /// 如果SkeletonBuilder已可用,在Awake时自动初始化 + /// + private void Awake() + { + // Only auto-initialize if SkeletonBuilder exists in hierarchy + // 仅在层次结构中存在SkeletonBuilder时自动初始化 + if (GetComponentInChildren(true) != null || + GetComponentInParent() != null) + { + InitializeSkeletonBuilder(); + } + } ``` 2. Add hook cache: @@ -315,6 +375,40 @@ Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook); } ``` +5. **Update GetHook to handle uninitialized state:** + ```csharp + public Transform GetHook(string hookName, bool recursive = true) + { + // Auto-initialize if not set (lazy initialization) + // 如果未设置则自动初始化(延迟初始化) + if (m_skeletonBuilder == null) + { + InitializeSkeletonBuilder(); + if (m_skeletonBuilder == null) + return null; // Still no skeleton found + } + + if (string.IsNullOrEmpty(hookName)) + return null; + + // Check cache first + if (m_hookCache.TryGetValue(hookName, out Transform cachedHook)) + { + if (cachedHook != null) // Unity "fake null" check + return cachedHook; + m_hookCache.Remove(hookName); // Remove invalid entry + } + + // Lookup from skeleton + Transform hook = m_skeletonBuilder.GetHook(hookName, recursive); + + if (hook != null) + m_hookCache[hookName] = hook; // Cache for performance + + return hook; + } + ``` + #### Task 1.3: Add Hook Access to CECPlayer (0.5 day) **File:** Find `CECPlayer.cs` (likely in `Assets/PerfectWorld/Scripts/Player/`) @@ -363,7 +457,43 @@ Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook); } ``` -#### Task 1.5: Create HookUtils Helper Class (0.5 day) +#### Task 1.5: Wire Up SkeletonBuilder Initialization (0.5 day) + +**Integration Points:** + +1. **Model Loading Integration:** + - After `SkeletonBuilder.BuildSkeleton()` is called, call `CECModel.InitializeSkeletonBuilder()` + - This ensures hooks are available immediately after skeleton is built + +2. **Character Creation Integration:** + - In `CECPlayer` / `CECNPC` initialization, after model is loaded: + ```csharp + CECModel model = GetComponent(); + if (model != null) + { + model.InitializeSkeletonBuilder(); + } + ``` + +3. **Model Reload Handling:** + - When model is reloaded (equipment change, etc.), call `InitializeSkeletonBuilder()` again + - This updates the SkeletonBuilder reference and clears cache + +**Example Integration in Model Loader:** +```csharp +// After SkeletonBuilder.BuildSkeleton() completes: +void OnSkeletonBuilt(SkeletonBuilder skeleton) +{ + CECModel model = GetComponent(); + if (model != null) + { + model.SetSkeletonBuilder(skeleton); + // Or call InitializeSkeletonBuilder() which does the same + } +} +``` + +#### Task 1.6: Create HookUtils Helper Class (0.5 day) **New File:** `Assets/PerfectWorld/Scripts/Utility/HookUtils.cs` @@ -385,8 +515,9 @@ namespace BrewMonster /// Hook Transform / 挂点变换 /// Offset vector / 偏移向量 /// If true, offset is relative to hook's local space; if false, absolute / 如果为true,偏移相对于挂点局部空间;如果为false,绝对偏移 + /// Model transform for absolute offset calculation (optional) / 用于绝对偏移计算的模型变换(可选) /// World position / 世界位置 - public static Vector3 GetHookWorldPosition(Transform hookTransform, Vector3 offset, bool bRelative) + public static Vector3 GetHookWorldPosition(Transform hookTransform, Vector3 offset, bool bRelative, Transform modelTransform = null) { if (hookTransform == null) return Vector3.zero; @@ -395,17 +526,34 @@ namespace BrewMonster { // Relative offset: transform offset from hook's local space to world space // 相对偏移:将偏移从挂点的局部空间变换到世界空间 + // C++ equivalent: pHook->GetAbsoluteTM() * pOffset return hookTransform.TransformPoint(offset); } else { - // Absolute offset: apply offset in world space, then add hook position - // 绝对偏移:在世界空间中应用偏移,然后加上挂点位置 - // C++ equivalent: vPos = pSkin->GetAbsoluteTM() * pOffset; - // vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); - Vector3 hookWorldPos = hookTransform.position; - Vector3 offsetWorld = hookTransform.TransformDirection(offset); - return hookWorldPos + offsetWorld; + // Absolute offset: transform offset in model space, then translate to hook position + // 绝对偏移:在模型空间中变换偏移,然后平移到挂点位置 + // C++ equivalent: + // vPos = pSkin->GetAbsoluteTM() * pOffset; + // vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + + // Get model transform (skeleton root or provided) + // 获取模型变换(骨架根或提供的) + if (modelTransform == null) + { + // Fallback: find SkeletonBuilder in hierarchy and use root bone + // 回退:在层次结构中查找SkeletonBuilder并使用根骨骼 + SkeletonBuilder skeleton = hookTransform.GetComponentInParent(); + modelTransform = skeleton != null ? skeleton.rootBone : hookTransform.root; + } + + // Transform offset in model's world space + // 在模型的世界空间中变换偏移 + Vector3 offsetWorld = modelTransform.TransformPoint(offset); + + // Translate to hook position + // 平移到挂点位置 + return offsetWorld - modelTransform.position + hookTransform.position; } } @@ -443,8 +591,10 @@ namespace BrewMonster **Phase 1 Deliverables:** - ✅ Hook lookup by name from SkeletonBuilder - ✅ GetHook() methods on CECModel, CECPlayer, CECNPC +- ✅ CECModel initialization logic (auto-detect SkeletonBuilder) - ✅ Hook position calculation utilities - ✅ Basic hook caching for performance +- ✅ Integration points for model loading --- @@ -482,9 +632,13 @@ namespace BrewMonster if (pHook == null) break; + // Get model transform for absolute offset calculation + // 获取模型变换用于绝对偏移计算 + Transform modelTransform = pModel.transform; // Or get from SkeletonBuilder if available + // Calculate position based on relative/absolute offset // 根据相对/绝对偏移计算位置 - vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook); + vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); return true; } @@ -524,9 +678,13 @@ namespace BrewMonster if (pHook == null) break; + // Get model transform for absolute offset calculation + // 获取模型变换用于绝对偏移计算 + Transform modelTransform = pModel.transform; // Or get from SkeletonBuilder if available + // Calculate position based on relative/absolute offset // 根据相对/绝对偏移计算位置 - vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook); + vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); return true; } @@ -688,7 +846,8 @@ if (bIsGoblinSkill) Transform pHook = goblinModel.GetHook(szHook, true); if (pHook != null) { - vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook); + Transform modelTransform = goblinModel.transform; + vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); return true; } } @@ -878,7 +1037,43 @@ namespace BrewMonster.Debug **Solution:** - `bRelHook = true`: Use `TransformPoint()` (offset in hook's local space) -- `bRelHook = false`: Use `TransformDirection()` + add to hook position (offset in world space) +- `bRelHook = false`: Use `TransformPoint()` on model transform, then translate to hook position (offset in model space) + +### 6.6 Challenge: CECModel Initialization Timing + +**Issue:** SkeletonBuilder may not be available when CECModel is created, or may be created later. + +**Solution:** +- **Lazy Initialization**: Auto-detect SkeletonBuilder in `GetHook()` if not set (fallback) +- **Explicit Initialization**: Call `InitializeSkeletonBuilder()` after model loads (recommended) +- **Auto-Initialization**: Try in `Awake()` if SkeletonBuilder exists in hierarchy (convenience) +- **Manual Setup**: Call `SetSkeletonBuilder()` directly when skeleton is built (most control) + +**Best Practice:** +1. After `SkeletonBuilder.BuildSkeleton()` completes, call `CECModel.InitializeSkeletonBuilder()` +2. This ensures hooks are immediately available and cache is properly initialized +3. For model reloads (equipment changes), call `InitializeSkeletonBuilder()` again + +**Integration Example:** +```csharp +// In model loader, after skeleton is built: +void OnModelLoaded(GameObject modelObject) +{ + SkeletonBuilder skeleton = modelObject.GetComponentInChildren(); + if (skeleton != null) + { + skeleton.BuildSkeleton(skeletonData); + + // Initialize CECModel with skeleton + CECModel model = modelObject.GetComponent(); + if (model != null) + { + model.InitializeSkeletonBuilder(); // Auto-detects skeleton + // OR: model.SetSkeletonBuilder(skeleton); // Direct assignment + } + } +} +``` ---