diff --git a/Assets/ModelRenderer/Editor.meta b/Assets/ModelRenderer/Editor.meta new file mode 100644 index 0000000000..0a8faaa99c --- /dev/null +++ b/Assets/ModelRenderer/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ace5809ec565f244b96e257e231b4abc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs b/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs new file mode 100644 index 0000000000..e7040e863f --- /dev/null +++ b/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs @@ -0,0 +1,264 @@ +// UTF-8 with BOM — required for Chinese character paths +using System.Collections.Generic; +using System.IO; +using BrewMonster.Scripts.ECModel; +using UnityEditor; +using UnityEngine; + +public class CombineActionSOAssigner : EditorWindow +{ + // Fixed — this is always where the weapon prefabs live + private const string PrefabRootPath = "Assets/ModelRenderer/Art/Models/models/weapons"; + + private string _soRootPath = "Assets/ModelRenderer/Art/Models/models/weapons"; + private bool _isDryRun = true; + private Vector2 _scrollPos; + private readonly List _log = new(); + + private static readonly GUIStyle HeaderStyle = new(EditorStyles.boldLabel) + { + fontSize = 14 + }; + + [MenuItem("Tools/Brew Monster/Combine Action SO Assigner")] + public static void ShowWindow() + { + var win = GetWindow(false, "SO Assigner", true); + win.minSize = new Vector2(520, 480); + } + + private void OnGUI() + { + DrawHeader(); + DrawConfig(); + DrawButtons(); + DrawLog(); + } + + // ── UI sections ────────────────────────────────────────────────────────── + + private void DrawHeader() + { + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("Combine Action SO Assigner", HeaderStyle); + EditorGUILayout.LabelField("Batch-assigns CombinedActionSO assets to weapon prefabs.", EditorStyles.miniLabel); + DrawSeparator(); + } + + private void DrawConfig() + { + EditorGUILayout.LabelField("Paths", EditorStyles.boldLabel); + + using (new EditorGUI.DisabledScope(true)) + EditorGUILayout.TextField("Prefab Root (fixed)", PrefabRootPath); + + _soRootPath = EditorGUILayout.TextField("SO Root Path", _soRootPath); + + EditorGUILayout.HelpBox( + "SO Root must mirror the prefab folder tree.\n" + + "e.g. SO: {SO Root}/人物/刀剑/15品单刀/15品单刀.asset\n" + + " Prefab: " + PrefabRootPath + "/人物/刀剑/15品单刀/15品单刀.prefab", + MessageType.Info); + + DrawSeparator(); + + EditorGUILayout.LabelField("Options", EditorStyles.boldLabel); + _isDryRun = EditorGUILayout.Toggle("Dry Run (no writes)", _isDryRun); + + if (_isDryRun) + EditorGUILayout.HelpBox("Dry Run ON — matches will be logged but nothing will be saved to disk.", MessageType.Warning); + else + EditorGUILayout.HelpBox("Dry Run OFF — prefabs WILL be modified and saved.", MessageType.Error); + + DrawSeparator(); + } + + private void DrawButtons() + { + EditorGUILayout.BeginHorizontal(); + + if (GUILayout.Button("Orphan Check", GUILayout.Height(30))) + RunOrphanCheck(); + + GUI.backgroundColor = _isDryRun ? Color.yellow : Color.red; + if (GUILayout.Button(_isDryRun ? "Run (Dry)" : "Run (LIVE — writes disk)", GUILayout.Height(30))) + RunAssignment(); + GUI.backgroundColor = Color.white; + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(4); + } + + private void DrawLog() + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Log", EditorStyles.boldLabel); + if (GUILayout.Button("Clear", GUILayout.Width(60))) + _log.Clear(); + EditorGUILayout.EndHorizontal(); + + _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true)); + foreach (var line in _log) + EditorGUILayout.LabelField(line, EditorStyles.wordWrappedLabel); + EditorGUILayout.EndScrollView(); + } + + private static void DrawSeparator() + { + EditorGUILayout.Space(4); + Rect r = EditorGUILayout.GetControlRect(false, 1); + EditorGUI.DrawRect(r, new Color(0.5f, 0.5f, 0.5f, 0.5f)); + EditorGUILayout.Space(4); + } + + // ── Core logic ─────────────────────────────────────────────────────────── + + private void RunOrphanCheck() + { + _log.Clear(); + _log.Add("=== ORPHAN CHECK ==="); + + string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { PrefabRootPath }); + int orphans = 0; + + foreach (string guid in guids) + { + string prefabPath = AssetDatabase.GUIDToAssetPath(guid); + string soPath = BuildSOPath(prefabPath); + + if (AssetDatabase.LoadAssetAtPath(soPath) == null) + { + _log.Add($"[ORPHAN] {Path.GetFileNameWithoutExtension(prefabPath)}"); + _log.Add($" expected SO: {soPath}"); + orphans++; + } + } + + _log.Add($"=== {orphans} orphan(s) out of {guids.Length} prefab(s) ==="); + Repaint(); + } + + private void RunAssignment() + { + _log.Clear(); + _log.Add(_isDryRun ? "=== DRY RUN ===" : "=== LIVE RUN ==="); + + string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { PrefabRootPath }); + int matched = 0, skipped = 0, failed = 0; + + try + { + for (int i = 0; i < guids.Length; i++) + { + string prefabPath = AssetDatabase.GUIDToAssetPath(guids[i]); + string prefabName = Path.GetFileNameWithoutExtension(prefabPath); + string soPath = BuildSOPath(prefabPath); + + EditorUtility.DisplayProgressBar( + "Assigning CombinedActionSO", + prefabName, + (float)i / guids.Length); + + CombinedActionSO so = AssetDatabase.LoadAssetAtPath(soPath); + if (so == null) + { + _log.Add($"[SKIP] {prefabName} → no SO at: {soPath}"); + skipped++; + continue; + } + + if (_isDryRun) + { + _log.Add($"[MATCH] {prefabName} → {soPath}"); + matched++; + } + else + { + if (!TryAssign(prefabPath, so, out string error)) + { + _log.Add($"[ERROR] {prefabName} — {error}"); + failed++; + } + else + { + _log.Add($"[OK] {prefabName}"); + matched++; + } + } + + // Memory management every 100 items + if (i > 0 && i % 100 == 0) + { + Resources.UnloadUnusedAssets(); + System.GC.Collect(); + } + } + } + finally + { + EditorUtility.ClearProgressBar(); + } + + if (!_isDryRun) + AssetDatabase.SaveAssets(); + + _log.Add($"=== DONE — matched: {matched}, skipped: {skipped}, failed: {failed} (total: {guids.Length}) ==="); + Repaint(); + } + + private static bool TryAssign(string prefabPath, CombinedActionSO so, out string error) + { + error = null; + GameObject root = null; + try + { + root = PrefabUtility.LoadPrefabContents(prefabPath); + + CombineActHolder holder = root.GetComponent(); + if (holder == null) + { + error = "CombineActHolder not found on root GameObject"; + return false; + } + + using SerializedObject serializedHolder = new(holder); + SerializedProperty prop = serializedHolder.FindProperty("combinedActionSO"); + if (prop == null) + { + error = "SerializedProperty 'combinedActionSO' not found"; + return false; + } + + prop.objectReferenceValue = so; + serializedHolder.ApplyModifiedPropertiesWithoutUndo(); + + PrefabUtility.SaveAsPrefabAsset(root, prefabPath); + return true; + } + catch (System.Exception ex) + { + error = ex.Message; + return false; + } + finally + { + if (root != null) + PrefabUtility.UnloadPrefabContents(root); + } + } + + // ── Path helpers ───────────────────────────────────────────────────────── + + /// + /// Maps a prefab path to its expected SO path using mirror structure. + /// Prefab: Assets/ModelRenderer/Art/Models/models/weapons/X/Y/Z.prefab + /// SO: {_soRootPath}/X/Y/Z.asset + /// + private string BuildSOPath(string prefabPath) + { + string relative = prefabPath.Substring(PrefabRootPath.Length).TrimStart('/'); + string assetRelative = Path.ChangeExtension(relative, ".asset").Replace('\\', '/'); + return _soRootPath.TrimEnd('/') + "/" + assetRelative; + } +} diff --git a/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs.meta b/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs.meta new file mode 100644 index 0000000000..c280205fb8 --- /dev/null +++ b/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9a82642463c35e244802b6f20f754c7a \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs index fe1d405ddd..a776135f1f 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs @@ -681,20 +681,21 @@ public class CECModel { foreach(var eventInfo in eventInfoList) { - if(eventInfo.m_nType == 0) + //0 is sound event + if (eventInfo is FX_BASE_INFO sfx) { - //0 is sound event - if (eventInfo is FX_BASE_INFO sfx) + if(sfx.m_strFilePaths != null && sfx.m_strFilePaths.Count > 0) { - if(sfx.m_strFilePaths != null && sfx.m_strFilePaths.Count > 0) + string soundpath = AFile.NormalizePath(sfx.m_strFilePaths[0],true); + //we need to determine sfx and gfx. now we dont have logic for this path + if(soundpath.Contains("gfx")) { - string soundpath = AFile.NormalizePath(sfx.m_strFilePaths[0],true); - soundpath = soundpath.ToLower(); - SFXManager.Instance.PlaySkillSfxAtPointAsync(soundpath, Vector3.zero).Forget(); + continue; } + soundpath = soundpath.ToLower(); + SFXManager.Instance.PlaySkillSfxAtPointAsync(soundpath, Vector3.zero).Forget(); } } - } } return true;