Files
test/Assets/ModelRenderer/Editor/CombineActionSOAssigner.cs
T
2026-04-25 14:48:22 +07:00

265 lines
9.0 KiB
C#

// 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<string> _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<CombineActionSOAssigner>(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<CombinedActionSO>(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<CombinedActionSO>(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<CombineActHolder>();
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 ─────────────────────────────────────────────────────────
/// <summary>
/// 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
/// </summary>
private string BuildSOPath(string prefabPath)
{
string relative = prefabPath.Substring(PrefabRootPath.Length).TrimStart('/');
string assetRelative = Path.ChangeExtension(relative, ".asset").Replace('\\', '/');
return _soRootPath.TrimEnd('/') + "/" + assetRelative;
}
}