384 lines
13 KiB
C#
384 lines
13 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 CombineActHolderFolderAssigner : EditorWindow
|
|
{
|
|
private const string MenuPath = "Tools/Brew Monster/Combine Act Holder Folder Assigner";
|
|
private const int ProgressBarInterval = 15;
|
|
private const int MemoryCleanupInterval = 250;
|
|
|
|
private static readonly string LogDir = Path.GetFullPath("Logs/CombineActHolderAssigner");
|
|
|
|
private DefaultAsset _folder;
|
|
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(MenuPath)]
|
|
public static void ShowWindow()
|
|
{
|
|
var win = GetWindow<CombineActHolderFolderAssigner>(false, "Combine Act Holder Assigner", true);
|
|
win.minSize = new Vector2(520, 480);
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
DrawHeader();
|
|
DrawFolderInput();
|
|
DrawOptions();
|
|
DrawButtons();
|
|
DrawLog();
|
|
}
|
|
|
|
private void DrawHeader()
|
|
{
|
|
EditorGUILayout.Space(6);
|
|
EditorGUILayout.LabelField("Combine Act Holder Folder Assigner", HeaderStyle);
|
|
EditorGUILayout.LabelField(
|
|
"Scans CombinedActionSO assets under a folder and pairs each with a same-name prefab in the same directory.",
|
|
EditorStyles.miniLabel);
|
|
DrawSeparator();
|
|
}
|
|
|
|
private void DrawFolderInput()
|
|
{
|
|
EditorGUILayout.LabelField("Folder", EditorStyles.boldLabel);
|
|
DrawFolderDropArea();
|
|
|
|
var newFolder = (DefaultAsset)EditorGUILayout.ObjectField("Project Folder", _folder, typeof(DefaultAsset), false);
|
|
if (newFolder != _folder)
|
|
{
|
|
_folder = newFolder;
|
|
_log.Clear();
|
|
_lastLogFile = null;
|
|
}
|
|
|
|
string folderPath = GetFolderPath();
|
|
bool validFolder = !string.IsNullOrEmpty(folderPath);
|
|
if (_folder != null && !validFolder)
|
|
EditorGUILayout.HelpBox("Assign a folder from the Project window (not a file).", MessageType.Warning);
|
|
|
|
if (validFolder)
|
|
EditorGUILayout.LabelField("Scan root", folderPath, EditorStyles.miniLabel);
|
|
|
|
DrawSeparator();
|
|
}
|
|
|
|
private void DrawFolderDropArea()
|
|
{
|
|
Event evt = Event.current;
|
|
Rect dropRect = GUILayoutUtility.GetRect(0f, 50f, GUILayout.ExpandWidth(true));
|
|
GUI.Box(dropRect, "Drag Project Folder Here");
|
|
|
|
if (!dropRect.Contains(evt.mousePosition))
|
|
return;
|
|
|
|
if (evt.type != EventType.DragUpdated && evt.type != EventType.DragPerform)
|
|
return;
|
|
|
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
|
if (evt.type == EventType.DragPerform)
|
|
{
|
|
DragAndDrop.AcceptDrag();
|
|
foreach (Object obj in DragAndDrop.objectReferences)
|
|
{
|
|
if (TrySetFolderFromObject(obj))
|
|
break;
|
|
}
|
|
}
|
|
|
|
evt.Use();
|
|
}
|
|
|
|
private void DrawOptions()
|
|
{
|
|
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()
|
|
{
|
|
string folderPath = GetFolderPath();
|
|
bool validFolder = !string.IsNullOrEmpty(folderPath);
|
|
|
|
EditorGUI.BeginDisabledGroup(!validFolder);
|
|
GUI.backgroundColor = _isDryRun ? Color.yellow : Color.red;
|
|
if (GUILayout.Button(_isDryRun ? "Run (Dry)" : "Run (LIVE - writes disk)", GUILayout.Height(30)))
|
|
RunAssignment(folderPath);
|
|
GUI.backgroundColor = Color.white;
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
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);
|
|
}
|
|
|
|
private string GetFolderPath()
|
|
{
|
|
if (_folder == null)
|
|
return null;
|
|
|
|
string path = AssetDatabase.GetAssetPath(_folder);
|
|
if (string.IsNullOrEmpty(path) || !AssetDatabase.IsValidFolder(path))
|
|
return null;
|
|
|
|
return path.Replace('\\', '/');
|
|
}
|
|
|
|
private bool TrySetFolderFromObject(Object obj)
|
|
{
|
|
if (obj is not DefaultAsset)
|
|
return false;
|
|
|
|
string path = AssetDatabase.GetAssetPath(obj);
|
|
if (string.IsNullOrEmpty(path) || !AssetDatabase.IsValidFolder(path))
|
|
return false;
|
|
|
|
_folder = (DefaultAsset)obj;
|
|
_log.Clear();
|
|
_lastLogFile = null;
|
|
return true;
|
|
}
|
|
|
|
private void RunAssignment(string folderPath)
|
|
{
|
|
_log.Clear();
|
|
string header = _isDryRun ? "=== DRY RUN ===" : "=== LIVE RUN ===";
|
|
_log.Add(header);
|
|
_log.Add($"Folder: {folderPath}");
|
|
|
|
string[] guids = AssetDatabase.FindAssets("t:CombinedActionSO", new[] { folderPath });
|
|
int matched = 0, skippedNoPrefab = 0, skippedAlready = 0, failed = 0;
|
|
|
|
var fileLines = new List<string> { $"{header} {Timestamp()}", $"Folder: {folderPath}" };
|
|
|
|
bool liveRun = !_isDryRun;
|
|
if (liveRun)
|
|
AssetDatabase.StartAssetEditing();
|
|
|
|
try
|
|
{
|
|
for (int i = 0; i < guids.Length; i++)
|
|
{
|
|
string soPath = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
string soName = Path.GetFileNameWithoutExtension(soPath);
|
|
string prefabPath = BuildMatchingPrefabPath(soPath);
|
|
|
|
if (i % ProgressBarInterval == 0 || i == guids.Length - 1)
|
|
{
|
|
EditorUtility.DisplayProgressBar(
|
|
"Combine Act Holder Folder Assigner",
|
|
soName,
|
|
guids.Length > 0 ? (float)(i + 1) / guids.Length : 1f);
|
|
}
|
|
|
|
if (!PrefabExistsAtPath(prefabPath))
|
|
{
|
|
string skipLine = $"[SKIP] {soName} → no prefab: {prefabPath}";
|
|
fileLines.Add(skipLine);
|
|
_log.Add(skipLine);
|
|
skippedNoPrefab++;
|
|
continue;
|
|
}
|
|
|
|
if (_isDryRun)
|
|
{
|
|
string matchLine = $"[MATCH] {soName} → {prefabPath}";
|
|
fileLines.Add(matchLine);
|
|
_log.Add(matchLine);
|
|
matched++;
|
|
continue;
|
|
}
|
|
|
|
if (IsAlreadyAssignedOnPrefabAsset(prefabPath, soPath))
|
|
{
|
|
string okLine = $"[OK] {soName} (already assigned)";
|
|
fileLines.Add(okLine);
|
|
skippedAlready++;
|
|
continue;
|
|
}
|
|
|
|
CombinedActionSO so = AssetDatabase.LoadAssetAtPath<CombinedActionSO>(soPath);
|
|
if (so == null)
|
|
{
|
|
string errLine = $"[ERROR] {soName} - could not load SO: {soPath}";
|
|
_log.Add(errLine);
|
|
fileLines.Add(errLine);
|
|
failed++;
|
|
continue;
|
|
}
|
|
|
|
if (!TryAssign(prefabPath, so, out string error))
|
|
{
|
|
string errLine = $"[ERROR] {soName} - {error}";
|
|
_log.Add(errLine);
|
|
fileLines.Add(errLine);
|
|
failed++;
|
|
}
|
|
else
|
|
{
|
|
fileLines.Add($"[OK] {soName}");
|
|
matched++;
|
|
}
|
|
|
|
if (i > 0 && i % MemoryCleanupInterval == 0)
|
|
Resources.UnloadUnusedAssets();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (liveRun)
|
|
AssetDatabase.StopAssetEditing();
|
|
|
|
EditorUtility.ClearProgressBar();
|
|
}
|
|
|
|
if (liveRun)
|
|
AssetDatabase.SaveAssets();
|
|
|
|
string summary =
|
|
$"matched: {matched}, no prefab: {skippedNoPrefab}, already assigned: {skippedAlready}, failed: {failed} (total SOs: {guids.Length})";
|
|
string summaryLine = $"=== DONE - {summary} ===";
|
|
_log.Add(summaryLine);
|
|
fileLines.Add(summaryLine);
|
|
|
|
_lastLogFile = WriteLogFile(_isDryRun ? "DryRun" : "LiveRun", fileLines);
|
|
|
|
Debug.Log($"[Combine Act Holder Assigner] {(_isDryRun ? "DRY RUN" : "LIVE")} - {summary} | Log: {_lastLogFile}");
|
|
|
|
if (skippedNoPrefab > 0)
|
|
Debug.LogWarning(
|
|
$"[Combine Act Holder Assigner] {skippedNoPrefab} SO(s) without a matching prefab - see log: {_lastLogFile}");
|
|
|
|
Repaint();
|
|
}
|
|
|
|
private static bool PrefabExistsAtPath(string prefabPath) =>
|
|
!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(prefabPath));
|
|
|
|
private static bool IsAlreadyAssignedOnPrefabAsset(string prefabPath, string soPath)
|
|
{
|
|
if (AssetDatabase.GetMainAssetTypeAtPath(prefabPath) != typeof(GameObject))
|
|
return false;
|
|
|
|
GameObject prefabAsset = (GameObject)AssetDatabase.LoadMainAssetAtPath(prefabPath);
|
|
if (prefabAsset == null)
|
|
return false;
|
|
|
|
CombineActHolder holder = prefabAsset.GetComponent<CombineActHolder>();
|
|
if (holder == null || holder.ActionSO == null)
|
|
return false;
|
|
|
|
string assignedPath = AssetDatabase.GetAssetPath(holder.ActionSO);
|
|
if (string.IsNullOrEmpty(assignedPath))
|
|
return false;
|
|
|
|
return assignedPath.Replace('\\', '/') == soPath.Replace('\\', '/');
|
|
}
|
|
|
|
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>() ?? root.AddComponent<CombineActHolder>();
|
|
|
|
if (holder.ActionSO == so)
|
|
return true;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
private static string BuildMatchingPrefabPath(string soPath)
|
|
{
|
|
soPath = soPath.Replace('\\', '/');
|
|
string dir = Path.GetDirectoryName(soPath)?.Replace('\\', '/');
|
|
string name = Path.GetFileNameWithoutExtension(soPath);
|
|
return $"{dir}/{name}.prefab";
|
|
}
|
|
|
|
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");
|
|
} |