using System; using System.Collections.Generic; using System.Runtime.InteropServices; using BrewMonster.Scripts; using CSNetwork.GPDataType; using UnityEngine; using ModelViewer.Common; using CombinedActMap = System.Collections.Generic.Dictionary; using CoGfxMap = System.Collections.Generic.Dictionary; using ConvexHullDataArray = System.Collections.Generic.List; using ECModelHookMap = System.Collections.Generic.Dictionary; using BrewMonster; using BrewMonster.Scripts.ECModel; using Unity.VisualScripting.FullSerializer; public enum ECMScript { enumECMScriptStartAction = 0, enumECMScriptEndActioin, enumECMScriptInit, enumECMScriptRelease, enumECMScriptModelLoaded, enumECMScriptChangeEquip, enumECMScriptChangeEquipTableInit, enumECMScriptPhysBreak, enumECMScriptCount } public enum ECMScriptVar { enumECMVarModel = 0, enumECMVarActName, enumECMVarActChannel, enumECMVarEquipId, enumECMVarEquipFlag, enumECMVarFashionMode, enumECMVarPathId, enumECMVarId, enumECMVarEquipIndex, enumECMVarBreakOffsetX, enumECMVarBreakOffsetY, enumECMVarBreakOffsetZ, enumECMVarCasterId, enumECMVarCastTargetId, enumECMVarSelf, enumECMVarCount } public static class CECModelConstants { public const int ECM_SCRIPT_MAX_VAR_COUNT = 8; public const int OUTER_DATA_COUNT = 8; } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ECM_SCRIPT_VAR { [MarshalAs(UnmanagedType.I4)] public int var_count; [MarshalAs(UnmanagedType.ByValArray, SizeConst = CECModelConstants.ECM_SCRIPT_MAX_VAR_COUNT)] public int[] var_index; } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct BoneScale { public int m_nIndex; public int m_nType; public A3DVECTOR3 m_vScale; } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct BoneScaleEx { public int m_nIndex; public float m_fLenFactor; public float m_fThickFactor; public float m_fWholeFactor; } public class ActChannelInfo { public List bone_names = new List(); public List joint_indices = new List(); public ActChannelInfo Clone() { return new ActChannelInfo { bone_names = new List(bone_names), joint_indices = new List(joint_indices) }; } } public enum A3DBLEND { A3DBLEND_SRCALPHA, A3DBLEND_INVSRCALPHA } public struct A3DSHADER { public A3DBLEND SrcBlend; public A3DBLEND DestBlend; } public class CECModelStaticData { private string m_strFilePath = string.Empty; private string m_strSkinModelPath = string.Empty; private bool m_bAutoUpdate = true; private bool m_bActMapped; public Dictionary m_ActionMap = new Dictionary(); private uint m_dwVersion; private string m_strHook = string.Empty; private string m_strCCName = string.Empty; private readonly List m_BoneScales = new List(); private readonly List m_BoneScaleExArr = new List(); private string m_strScaleBaseBone = string.Empty; private uint m_OrgColor = 0xFFFFFFFF; private uint m_EmissiveColor = 0; private float m_fDefPlaySpeed = 1.0f; private A3DSHADER m_BlendMode = new A3DSHADER { SrcBlend = A3DBLEND.A3DBLEND_SRCALPHA, DestBlend = A3DBLEND.A3DBLEND_INVSRCALPHA }; private readonly float[] m_OuterData = new float[CECModelConstants.OUTER_DATA_COUNT]; private bool m_bCanCastShadow = true; private bool m_bRenderSkinModel = true; private bool m_bRenderEdge = true; private readonly CoGfxMap m_CoGfxMap = new CoGfxMap(); private readonly ConvexHullDataArray m_ConvexHullDataArr = new ConvexHullDataArray(); private readonly A3DAABB m_CHAABB = new A3DAABB(); private readonly Dictionary m_ChannelInfoDict = new Dictionary(); private readonly Dictionary m_EventMasks = new Dictionary(); private readonly string[] m_Scripts = new string[(int)ECMScript.enumECMScriptCount]; private readonly bool[] m_ScriptEnable = new bool[(int)ECMScript.enumECMScriptCount]; private readonly CLuaMemTbl m_ScriptMemTbl = new CLuaMemTbl(); private bool m_bInitGlobalScript; private string m_strGlobalScriptName = string.Empty; public class ChildInfo { public string m_strName = string.Empty; public string m_strPath = string.Empty; public string m_strHHName = string.Empty; public string m_strCCName = string.Empty; } private readonly List m_ChildInfoArray = new List(); private readonly List m_AdditionalSkinLst = new List(); private string m_strPhysFileName = string.Empty; private object m_pPhysSyncData; private readonly ECModelHookMap m_ECModelHookMap = new ECModelHookMap(); private string m_strPixelShader = string.Empty; private string m_strShaderTex = string.Empty; private readonly List m_vecPSConsts = new List(); public CECModelStaticData() { ResetOuterData(); m_CHAABB.Clear(); } public bool LoadData(CombinedActionSO combinedAction) { foreach(var action in combinedAction.CombinedActionData) { m_ActionMap[action.m_strName] = action; } return true; } public bool Save(string szFile, CECModel pModel) { throw new NotImplementedException("CECModelStaticData.Save requires the legacy serialization pipeline to be ported."); } public bool SaveBoneScaleInfo(string szFile) { throw new NotImplementedException("CECModelStaticData.SaveBoneScaleInfo is not yet implemented."); } public void Release() { m_BoneScales.Clear(); m_BoneScaleExArr.Clear(); m_CoGfxMap.Clear(); m_ActionMap = null; m_AdditionalSkinLst.Clear(); m_ChildInfoArray.Clear(); m_ConvexHullDataArr.Clear(); m_ChannelInfoDict.Clear(); m_EventMasks.Clear(); Array.Clear(m_Scripts, 0, m_Scripts.Length); Array.Clear(m_ScriptEnable, 0, m_ScriptEnable.Length); m_ECModelHookMap.Clear(); m_vecPSConsts.Clear(); m_bInitGlobalScript = false; m_strGlobalScriptName = string.Empty; m_pPhysSyncData = null; m_CHAABB.Clear(); ResetOuterData(); } public ConvexHullDataArray GetConvexHullData() => m_ConvexHullDataArr; public bool HasCHAABB() => m_ConvexHullDataArr.Count != 0; public A3DAABB GetCHAABB() => m_CHAABB; public bool UpdateScript(int index, string strScript, bool bCompileOnly) { if (index < 0 || index >= (int)ECMScript.enumECMScriptCount) return false; if (!bCompileOnly) m_Scripts[index] = strScript ?? string.Empty; m_ScriptEnable[index] = !string.IsNullOrEmpty(m_Scripts[index]); return m_ScriptEnable[index]; } public CoGfxMap GetCoGfxMap() => m_CoGfxMap; public Dictionary GetCombinedAction() => m_ActionMap; public string GetPhysFileName() => m_strPhysFileName; public void SetPhysFileName(string szFile) => m_strPhysFileName = szFile ?? string.Empty; public object GetPhysSyncData() => m_pPhysSyncData; public void SetPhysSyncData(object p) => m_pPhysSyncData = p; public uint GetVersion() => m_dwVersion; public string GetPixelShaderPath() => m_strPixelShader; public void SetPixelShaderPath(string szPath) => m_strPixelShader = szPath ?? string.Empty; public string GetShaderTexture() => m_strShaderTex; public void SetShaderTexture(string szPath) => m_strShaderTex = szPath ?? string.Empty; public IList GetPSConstVec() => m_vecPSConsts; public void SetActionEventMask(int nChannel, uint dwMask) => m_EventMasks[nChannel] = dwMask; public uint GetActionEventMask(int nChannel) { return m_EventMasks.TryGetValue(nChannel, out uint mask) ? mask : uint.MaxValue; } public IReadOnlyDictionary GetActionEventMaskSnapshot() => m_EventMasks; public void OnScriptEndAction(CECModel pModel, int nChannel, string szActName) { if (!IsScriptEnabled(ECMScript.enumECMScriptEndActioin)) return; Debug.LogWarning($"CECModelStaticData.OnScriptEndAction invoked for {szActName}, scripting runtime not yet ported."); } public void Reset() { m_OrgColor = 0xFFFFFFFF; m_EmissiveColor = 0; m_fDefPlaySpeed = 1.0f; m_BlendMode = new A3DSHADER { SrcBlend = A3DBLEND.A3DBLEND_SRCALPHA, DestBlend = A3DBLEND.A3DBLEND_INVSRCALPHA }; ResetOuterData(); m_BoneScales.Clear(); m_BoneScaleExArr.Clear(); m_strScaleBaseBone = string.Empty; m_ChannelInfoDict.Clear(); m_strPhysFileName = string.Empty; } public void AddScriptMethod(int index, string szScript) { if (index < 0 || index >= (int)ECMScript.enumECMScriptCount) return; m_ScriptEnable[index] = !string.IsNullOrEmpty(szScript); } public void OnScriptPlayAction(CECModel pModel, int nChannel, string szActName) { if (!IsScriptEnabled(ECMScript.enumECMScriptStartAction)) return; Debug.LogWarning($"CECModelStaticData.OnScriptPlayAction triggered for {szActName}, scripting runtime not available."); } public void OnScriptChangeEquip(CECModel pModel, int nEquipId, int nEquipFlag, bool bFashionMode, int nPathId, int nEquipIndex) { if (!IsScriptEnabled(ECMScript.enumECMScriptChangeEquip)) return; Debug.LogWarning("CECModelStaticData.OnScriptChangeEquip called, scripting runtime not available."); } public void OnScriptPhysBreak(CECModel pModel, float fBreakOffsetX, float fBreakOffsetY, float fBreakOffsetZ) { if (!IsScriptEnabled(ECMScript.enumECMScriptPhysBreak)) return; Debug.LogWarning("CECModelStaticData.OnScriptPhysBreak called, scripting runtime not available."); } public void InitGlobalScript() { if (m_bInitGlobalScript) return; if (!IsScriptEnabled(ECMScript.enumECMScriptInit)) return; m_bInitGlobalScript = true; m_strGlobalScriptName = $"GlobalScriptFunc_{DateTime.UtcNow.Ticks}"; } public bool LoadAdditionalSkin(object pFile, uint dwVersion, bool bLoadAdditionalSkin) { Debug.LogWarning("CECModelStaticData.LoadAdditionalSkin stub invoked."); return true; } public IList GetAdditionalSkinList() => m_AdditionalSkinLst; public IList GetChildInfoArray() => m_ChildInfoArray; public float[] GetOuterData() => m_OuterData; public bool CanCastShadow => m_bCanCastShadow; public bool CanRenderSkinModel => m_bRenderSkinModel; public bool CanRenderEdge => m_bRenderEdge; private bool IsScriptEnabled(ECMScript scriptId) { int idx = (int)scriptId; if (idx < 0 || idx >= m_ScriptEnable.Length) return false; return m_ScriptEnable[idx]; } private void ResetOuterData() { for (int i = 0; i < m_OuterData.Length; i++) m_OuterData[i] = 1.0f; } } public class CLuaMemTbl { public void Init(string tableName) { } public bool AddMethod(string name, IList args, string script) => false; public void RemoveMethod(string name) { } public void Release() { } } public class CECModel { private bool m_bInheritParentId = true; private int m_nId = 0; public GameObject m_pPlayerModel; private const uint COMACT_FLAG_MODE_NONE = 0; protected CECModelStaticData m_pMapModel = new CECModelStaticData(); public SkeletonBuilder m_skeletonBuilder; private Dictionary m_hookCache = new Dictionary(); private Dictionary m_hangerPositionCache = new Dictionary(); private Dictionary m_childModels = new Dictionary(); private Transform m_transform; public bool InheritParentId() => m_bInheritParentId; public void SetId(int nId) => m_nId = nId; public void ClearComActFlag(bool bSignalCurrent) { ClearComActFlag(0, bSignalCurrent); } public void ClearComActFlag(int nChannel, bool bSignalCurrent) { /* ChannelAct & ca = m_ChannelActs[nChannel]; ChannelActNode* pNode = ca.GetHighestRankNode(); if (pNode) { if (pNode->m_pActFlag && bSignalCurrent) *pNode->m_pActFlag = true; //ca.m_dwFlagMode = COMACT_FLAG_MODE_NONE; pNode->m_pActFlag = NULL; }*/ } public bool HasCHAABB() { return m_pMapModel.HasCHAABB(); } /// /// Set SkeletonBuilder reference for hook lookup /// 设置用于挂点查找的SkeletonBuilder引用 /// /// SkeletonBuilder instance / SkeletonBuilder实例 public void SetSkeletonBuilder(SkeletonBuilder builder) { m_skeletonBuilder = builder; m_hookCache?.Clear(); // Invalidate cache on model change } public void SetCombinedAction(CombinedActionSO combinedAction) { //workaround: load data from combined action. C++ load from CMS file aka prefab. we already include those data inside the prefab //dunno if we lack of any imformation m_pMapModel.LoadData(combinedAction); } /// /// Get the SkeletonBuilder component /// 获取SkeletonBuilder组件 /// /// SkeletonBuilder instance or null / SkeletonBuilder实例,未找到返回null public SkeletonBuilder GetSkeletonBuilder() { return m_skeletonBuilder; } /// /// 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(通常附加在那里) if (m_transform != null) { m_skeletonBuilder = m_transform.GetComponentInChildren(true); } if (m_skeletonBuilder == null && m_transform != null) { // Fallback: search in parent hierarchy (for NPCs/Players) // 回退:在父层次结构中搜索(用于NPC/玩家) m_skeletonBuilder = m_transform.GetComponentInParent(); } if (m_skeletonBuilder == null) { BMLogger.LogWarning($"[CECModel] SkeletonBuilder not found for {(m_transform != null ? m_transform.name : "null")}. Hooks will not be available."); } else { // Clear cache when skeleton is set // 设置骨架时清除缓存 m_hookCache?.Clear(); BMLogger.Log($"[CECModel] SkeletonBuilder initialized for {(m_transform != null ? m_transform.name : "null")}"); } } /// /// Set Transform reference for this model /// 设置此模型的Transform引用 /// /// Transform instance / Transform实例 public void SetTransform(Transform transform) { m_transform = transform; } /// /// Get Transform reference for this model /// 获取此模型的Transform引用 /// public Transform transform { get { return m_transform; } } /// /// Get hook Transform by name /// 根据名称获取挂点变换 /// /// Hook name / 挂点名称 /// Search recursively / 递归搜索 /// Hook Transform or null / 挂点变换,未找到返回null 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; } /// /// Invalidate hook cache (call when model reloads) /// 使挂点缓存失效(在模型重新加载时调用) /// public void InvalidateHookCache() { m_hookCache.Clear(); } /// /// Register a child model (weapon, pet, etc.) /// 注册子模型(武器、宠物等) /// /// Hanger name / 挂载者名称 /// Child model instance / 子模型实例 public void RegisterChildModel(string hangerName, CECModel childModel) { if (!string.IsNullOrEmpty(hangerName) && childModel != null) { m_childModels[hangerName] = childModel; BMLogger.Log($"[CECModel] Registered child model '{hangerName}'"); } } /// /// Unregister a child model /// 注销子模型 /// /// Hanger name / 挂载者名称 public void UnregisterChildModel(string hangerName) { if (!string.IsNullOrEmpty(hangerName)) { if (m_childModels.Remove(hangerName)) { BMLogger.Log($"[CECModel] Unregistered child model '{hangerName}'"); } } } /// /// Get child model by hanger name /// 根据挂载者名称获取子模型 /// /// Hanger name / 挂载者名称 /// Child model or null if not found / 子模型,未找到返回null public CECModel GetChildModel(string hangerName) { if (string.IsNullOrEmpty(hangerName)) return null; return m_childModels.TryGetValue(hangerName, out CECModel child) ? child : null; } public bool AddChildModel( string szHangerName, bool bOnBone, string szHookName, CECModel pChild, string szCCName) { if (m_childModels.TryGetValue(szHangerName, out _)) return false; if (pChild == null) { return false; } Transform hook = m_skeletonBuilder.GetHook(szHookName, true); Transform hangger = pChild.m_skeletonBuilder.GetHook(szCCName, true); if (hook == null || hangger == null) { return false; } // Align child CC/hanger to parent hook in world space, then parent with world pose preserved. // 先在世界里对齐子模型挂点与父挂点,再挂接并保持世界坐标不变。 Vector3 worldAlign = hook.position - hangger.position; pChild.transform.position += worldAlign; pChild.transform.SetParent(hook.transform, true); m_hookCache.Add(szHookName, hook); m_childModels.Add(szHookName, pChild); m_hangerPositionCache.Add(szHookName, hangger); // if (m_pA3DSkinModel && pChild->m_pA3DSkinModel) // { // if (!m_pA3DSkinModel->AddChildModel( // szHangerName, // bOnBone, // szHookName, // pChild->m_pA3DSkinModel, // szCCName)) // return false; // pChild->m_pA3DSkinModel->SetAutoAABBType(A3DSkinModel::AUTOAABB_INITMESH); // pChild->m_bLoadSkinModel = false; // } // pChild->m_strHookName = szHookName; // pChild->m_strCC = szCCName; // pChild->SetGfxUseLOD(m_bGfxUseLod, false); // pChild->SetDisableCamShake(m_bDisableCamShake); // pChild->SetCreatedByGfx(m_bCreatedByGfx); if (pChild.InheritParentId()) pChild.SetId(m_nId); m_childModels[szHangerName] = pChild; return true; } public void ResetHookPosition(string szHookName) { Transform hook = m_hookCache.TryGetValue(szHookName, out Transform cachedHook) ? cachedHook : null; Transform hangger = m_hangerPositionCache.TryGetValue(szHookName, out Transform cachedHangger) ? cachedHangger : null; GameObject childObject = m_childModels.TryGetValue(szHookName, out CECModel child) ? child.m_pPlayerModel : null; if (hook != null && hangger != null && child != null) { Vector3 worldAlign = hook.position - hangger.position; child.transform.position += worldAlign; } } //ref cant not be null so seperate into two functions public bool PlayActionByName(string szActName, float fWeight=1.0f, bool bRestart=true, int nTransTime=200, bool bForceStop=true, uint dwUserData =0, bool bNoFx=false, CECAttackEvent attackEvent = null, uint dwFlagMode = 0) { return PlayActionByName(0, szActName, fWeight, bRestart, nTransTime, bForceStop, dwUserData, bNoFx, attackEvent, dwFlagMode); } //ref cant not be null so seperate into two functions public bool QueueAction(string szActName, int nTransTime=200, uint dwUserData=0, bool bForceStopPrevAct=false, bool bCheckTailDup = false, bool bNoFx = false, bool bResetSpeed = false, bool bResetActFlag=false, CECAttackEvent attackEvent=null, uint dwNewFlagMode=0) { return QueueAction(0, szActName, nTransTime, dwUserData, bForceStopPrevAct, bCheckTailDup, bNoFx, bResetSpeed ,bResetActFlag, attackEvent, dwNewFlagMode); } //Final trigger function public bool PlayActionByName(int nChannel, string szActName, float fWeight, bool bRestart, int nTransTime, bool bForceStop, uint dwUserData, bool bNoFx, CECAttackEvent attackEvent, uint dwFlagMode = 0) { if(szActName == null || szActName == string.Empty) { return false; } A3DCombinedAction combinedAction = GetComActByName(szActName); if(combinedAction == null) { return false; } var actionInfos = combinedAction.m_ActLst; var isLoop = combinedAction.m_nLoops == 1; EventBus.PublishChannel(m_nId, new PlayActionEvent(actionInfos[0].m_strName, nTransTime, bForceStop, attackEvent, isLoop)); for(int i = 1; i < actionInfos.Count; i++) { EventBus.PublishChannelClass(m_nId, new QueueActionEvent(actionInfos[i].m_strName, null, false, attackEvent, nTransTime, false, isLoop)); } var eventInfoList = combinedAction.m_EventInfoLst; if(eventInfoList != null && eventInfoList.Count > 0) { foreach(var eventInfo in eventInfoList) { if(eventInfo.m_nType == 0) { //0 is sound event if (eventInfo is FX_BASE_INFO sfx) { if(sfx.m_strFilePaths != null && sfx.m_strFilePaths.Count > 0) { string soundpath = AFile.NormalizePath(sfx.m_strFilePaths[0]); soundpath = soundpath.ToLower(); SFXManager.Instance.PlaySkillSfxAtPointAsync(soundpath, Vector3.zero).Forget(); } } } } } return true; } public A3DCombinedAction GetComActByName(string szActName) { if(m_pMapModel == null||m_pMapModel.m_ActionMap == null) { return null; } return m_pMapModel.m_ActionMap.TryGetValue(szActName, out A3DCombinedAction combinedAction) ? combinedAction : null; } //Final trigger function public bool QueueAction(int nChannel, string szActName, int nTransTime, uint dwUserData, bool bForceStopPrevAct, bool bCheckTailDup, bool bNoFx, bool bResetSpeed, bool bResetActFlag, CECAttackEvent attackEvent=null, uint dwNewFlagMode=0){ if(szActName == null || szActName == string.Empty) { return false; } A3DCombinedAction combinedAction = GetComActByName(szActName); if(combinedAction == null) { return false; } var actionInfos = combinedAction.m_ActLst; var isLoop = combinedAction.m_nLoops == 1; EventBus.PublishChannelClass(m_nId, new QueueActionEvent(actionInfos[0].m_strName, null, false, attackEvent, nTransTime, bForceStopPrevAct, isLoop)); for(int i = 1; i < actionInfos.Count; i++) { EventBus.PublishChannelClass(m_nId, new QueueActionEvent(actionInfos[i].m_strName, null, false, attackEvent, nTransTime, false, isLoop)); } return true; } public bool ClearComActFlagAllRankNodes(bool bSignalCurrent){ EventBus.PublishChannel(m_nId, new ClearComActFlagAllRankNodesEvent(bSignalCurrent)); return false; } public uint GetCurActionUserData(){ return 0; } public bool QueueAction(CECNPC.INFO iNFO, string szActName, ref bool pNewActFlag, int nTransTime = 200, uint dwUserData = 0, bool bForceStopPrevAct = false, bool bCheckTailDup = false, bool bNoFx = false, bool bResetSpeed = false /*joslian*/, bool bResetActFlag = false, uint dwNewFlagMode = COMACT_FLAG_MODE_NONE) { QueueAction(iNFO, 0, szActName, ref bResetActFlag, nTransTime, dwUserData, bForceStopPrevAct, bCheckTailDup, bNoFx, bResetSpeed/*joslian*/, pNewActFlag, dwNewFlagMode); return true; } public bool QueueAction(CECNPC.INFO iNFO, int nChannel, string szActName, ref bool pNewActFlag, int nTransTime, uint dwUserData/* 0 */, bool bForceStopPrevAct, bool bCheckTailDup, bool bNoFx, bool bResetSpeed /*joslian*/, bool bResetActFlag, uint dwNewFlagMode) { EventBus.PublishChannel(iNFO.nid, new QueueNPCActionEvent(szActName)); return true; } public void StopChannelAction(int nChannel, bool bStopAct, bool bStopFx = false) { //TODO: Implement StopChannelAction } public struct QueueNPCActionEvent { public string AnimationName; public QueueNPCActionEvent(string animationName) { AnimationName = animationName; } } 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) dwFadeOutTime = 0; 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 (it != m_CoGfxMap.end()) // { // pInfo = it->second; // A3DGFXEx* pGfx = pInfo->GetGfx(); // TransferEcmProperties(pGfx); // pGfx->SetScale(fScale * m_fGfxScaleFactor); // pInfo->SetFadeOutTime(dwFadeOutTime); // pInfo->SetModelAlpha(true); // pInfo->SetHookName(szHook); // pInfo->SetUseECMHook(bUseECMHook); // pInfo->SetScale(fScale); // pInfo->SetOffset(vOffset); // pInfo->SetPitch(fPitch); // pInfo->SetYaw(fYaw); // pInfo->SetRot(fRot); // pInfo->BuildTranMat(); // pGfx->SetSfxPriority(m_nSfxPriority); // pGfx->Start(true); // return; // } // // pInfo = new GFX_INFO(NULL); // pInfo->SetFilePath(szPath); // pInfo->SetHookName(szHook); // pInfo->SetUseECMHook(bUseECMHook); // pInfo->SetScale(fScale); // pInfo->Init(m_pA3DDevice); // pInfo->SetOffset(vOffset); // pInfo->SetPitch(fPitch); // pInfo->SetYaw(fYaw); // pInfo->SetRot(fRot); // pInfo->LoadGfx(); // // if (!pInfo->GetGfx()) // { // delete pInfo; // return; // } // // pInfo->BuildTranMat(); // pInfo->SetFadeOutTime(dwFadeOutTime); // pInfo->SetModelAlpha(true); // TransferEcmProperties(pInfo->GetGfx()); // pInfo->GetGfx()->SetScale(fScale * m_fGfxScaleFactor); // pInfo->GetGfx()->SetSfxPriority(m_nSfxPriority); // pInfo->GetGfx()->Start(true); // // m_CoGfxMap[strKey] = pInfo; } } // Action channel public enum ActionChannel { ACTCHA_WOUND = 1, };