322 lines
12 KiB
C#
322 lines
12 KiB
C#
// UTF-8 with BOM — required for Chinese character paths
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
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";
|
|
|
|
// Log files land next to the project's Logs/ folder
|
|
private static readonly string LogDir = Path.GetFullPath("Logs/SOAssigner");
|
|
|
|
private string _soRootPath = "Assets/ModelRenderer/Art/Models/models/weapons";
|
|
private bool _isDryRun = true;
|
|
private Vector2 _scrollPos;
|
|
private readonly List<string> _log = new();
|
|
private string _lastLogFile;
|
|
|
|
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 (!string.IsNullOrEmpty(_lastLogFile) && GUILayout.Button("Open Log File", GUILayout.Width(100)))
|
|
EditorUtility.RevealInFinder(_lastLogFile);
|
|
if (GUILayout.Button("Clear", GUILayout.Width(60)))
|
|
{
|
|
_log.Clear();
|
|
_lastLogFile = null;
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
if (!string.IsNullOrEmpty(_lastLogFile))
|
|
EditorGUILayout.LabelField(_lastLogFile, EditorStyles.miniLabel);
|
|
|
|
_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;
|
|
var fileLines = new List<string> { $"=== ORPHAN CHECK {Timestamp()} ===" };
|
|
|
|
foreach (string guid in guids)
|
|
{
|
|
string prefabPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
string soPath = BuildSOPath(prefabPath);
|
|
|
|
if (AssetDatabase.LoadAssetAtPath<CombinedActionSO>(soPath) == null)
|
|
{
|
|
string line1 = $"[ORPHAN] {Path.GetFileNameWithoutExtension(prefabPath)}";
|
|
string line2 = $" expected SO: {soPath}";
|
|
_log.Add(line1);
|
|
_log.Add(line2);
|
|
fileLines.Add(line1);
|
|
fileLines.Add(line2);
|
|
orphans++;
|
|
}
|
|
}
|
|
|
|
string summary = $"=== {orphans} orphan(s) out of {guids.Length} prefab(s) ===";
|
|
_log.Add(summary);
|
|
fileLines.Add(summary);
|
|
|
|
_lastLogFile = WriteLogFile("OrphanCheck", fileLines);
|
|
Debug.Log($"[SO Assigner] Orphan Check — {orphans}/{guids.Length} orphans. Log: {_lastLogFile}");
|
|
Repaint();
|
|
}
|
|
|
|
private void RunAssignment()
|
|
{
|
|
_log.Clear();
|
|
string header = _isDryRun ? "=== DRY RUN ===" : "=== LIVE RUN ===";
|
|
_log.Add(header);
|
|
|
|
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { PrefabRootPath });
|
|
int matched = 0, skipped = 0, failed = 0;
|
|
|
|
var fileLines = new List<string> { $"{header} {Timestamp()}" };
|
|
|
|
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)
|
|
{
|
|
string skipLine = $"[SKIP] {prefabName} → {soPath}";
|
|
fileLines.Add(skipLine);
|
|
if (!_isDryRun)
|
|
_log.Add(skipLine); // window only gets skips in live mode
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (_isDryRun)
|
|
{
|
|
// File gets every match; window stays quiet
|
|
fileLines.Add($"[MATCH] {prefabName} → {soPath}");
|
|
matched++;
|
|
}
|
|
else
|
|
{
|
|
if (!TryAssign(prefabPath, so, out string error))
|
|
{
|
|
string errLine = $"[ERROR] {prefabName} — {error}";
|
|
_log.Add(errLine);
|
|
fileLines.Add(errLine);
|
|
failed++;
|
|
}
|
|
else
|
|
{
|
|
fileLines.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();
|
|
|
|
string summary = $"matched: {matched}, skipped: {skipped}, failed: {failed} (total: {guids.Length})";
|
|
string summaryLine = $"=== DONE — {summary} ===";
|
|
_log.Add(summaryLine);
|
|
fileLines.Add(summaryLine);
|
|
|
|
_lastLogFile = WriteLogFile(_isDryRun ? "DryRun" : "LiveRun", fileLines);
|
|
|
|
Debug.Log($"[SO Assigner] {(_isDryRun ? "DRY RUN" : "LIVE")} — {summary} | Log: {_lastLogFile}");
|
|
|
|
if (skipped > 0)
|
|
Debug.LogWarning($"[SO Assigner] {skipped} unmatched prefab(s) — see log file for details: {_lastLogFile}");
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ── File logging ─────────────────────────────────────────────────────────
|
|
|
|
private static string WriteLogFile(string prefix, List<string> lines)
|
|
{
|
|
Directory.CreateDirectory(LogDir);
|
|
string fileName = $"{prefix}_{System.DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log";
|
|
string fullPath = Path.Combine(LogDir, fileName);
|
|
File.WriteAllLines(fullPath, lines, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
|
return fullPath;
|
|
}
|
|
|
|
private static string Timestamp() =>
|
|
System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
|
|
// ── 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;
|
|
}
|
|
}
|