Files
test/Assets/PerfectWorld/Scripts/Utils/Editor/FolderToAddressablesWindow.cs
T
2026-04-10 16:35:07 +07:00

318 lines
9.6 KiB
C#

#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// Assign a parent folder, then register all assets under it into an Addressables group named after that folder.
/// Addresses are relative to the parent folder, without the file extension (e.g. .png, .wav). Text files are skipped.
/// </summary>
public class FolderToAddressablesWindow : EditorWindow
{
private const string MenuPath = "Tools/Addressable/Folder To Addressables…";
private static readonly HashSet<string> TextExtensions = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
{
".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".ini", ".cfg", ".html", ".htm",
};
private DefaultAsset _parentFolder;
private Vector2 _scroll;
private string _log = "";
[MenuItem(MenuPath)]
public static void Open()
{
var w = GetWindow<FolderToAddressablesWindow>(false, "Folder To Addressables", true);
w.minSize = new Vector2(420, 280);
w.Show();
}
private void OnGUI()
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Folder To Addressables", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Group name = last segment of the folder path. Two different paths with the same folder name share one group. " +
"Address keys omit file extensions (e.g. Sub/Sprite not Sub/Sprite.png). " +
"If nothing updates, ensure the target group is not Read Only in the Addressables Groups window.",
MessageType.Info);
EditorGUILayout.Space(4);
var newFolder = (DefaultAsset)EditorGUILayout.ObjectField(
"Parent folder", _parentFolder, typeof(DefaultAsset), false);
if (newFolder != _parentFolder)
{
_parentFolder = newFolder;
_log = "";
}
string parentPath = _parentFolder != null ? AssetDatabase.GetAssetPath(_parentFolder) : null;
bool validFolder = !string.IsNullOrEmpty(parentPath) && AssetDatabase.IsValidFolder(parentPath);
if (_parentFolder != null && !validFolder)
{
EditorGUILayout.HelpBox("Assign a folder from the Project window (not a file).", MessageType.Warning);
}
string groupName = validFolder ? Path.GetFileName(parentPath.TrimEnd('/')) : "";
EditorGUILayout.LabelField("Addressables group", string.IsNullOrEmpty(groupName) ? "—" : groupName);
EditorGUILayout.Space(8);
EditorGUI.BeginDisabledGroup(!validFolder);
if (GUILayout.Button("Start", GUILayout.Height(28)))
{
Run(parentPath, groupName);
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("Log", EditorStyles.boldLabel);
_scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.ExpandHeight(true));
EditorGUILayout.TextArea(string.IsNullOrEmpty(_log) ? "(empty)" : _log, EditorStyles.textArea, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void Run(string parentPath, string groupName)
{
_log = "";
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
AppendLog("ERROR: Addressable settings not found. Install/configure the Addressables package.");
EditorUtility.DisplayDialog("Folder To Addressables", "Addressable settings not found.", "OK");
return;
}
if (string.IsNullOrEmpty(groupName))
{
AppendLog("ERROR: Could not derive group name from folder path.");
return;
}
var group = GetOrCreateGroup(settings, groupName);
if (group == null)
{
AppendLog($"ERROR: Could not find or create group: {groupName}");
EditorUtility.DisplayDialog("Folder To Addressables", $"Could not find or create group: {groupName}", "OK");
return;
}
if (group.ReadOnly)
{
AppendLog(
"ERROR: The target Addressables group is Read Only. Unity will not apply CreateOrMoveEntry/SetAddress. " +
"Open Window > Asset Management > Addressables > Groups, select the group, and disable Read Only in the Inspector.");
EditorUtility.DisplayDialog(
"Folder To Addressables",
$"Group \"{groupName}\" is Read Only. Disable Read Only on that group, then run this tool again.",
"OK");
return;
}
parentPath = parentPath.Replace('\\', '/').TrimEnd('/');
string parentPrefix = parentPath + "/";
// Merge GUIDs from the project tree with entries already in this group under the folder.
// Ensures we refresh addresses for assets that are already addressable (not ignored when the group already existed).
var guidsToProcess = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
foreach (var g in AssetDatabase.FindAssets("", new[] { parentPath }))
guidsToProcess.Add(g);
if (group.entries != null)
{
foreach (var existing in group.entries)
{
if (existing == null)
continue;
string ap = existing.AssetPath;
if (string.IsNullOrEmpty(ap))
continue;
ap = ap.Replace('\\', '/');
if (AssetDatabase.IsValidFolder(ap))
continue;
if (!ap.StartsWith(parentPrefix, System.StringComparison.OrdinalIgnoreCase))
continue;
guidsToProcess.Add(existing.guid);
}
}
int processed = 0;
int skippedText = 0;
int skippedOther = 0;
int errors = 0;
foreach (string guid in guidsToProcess)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(assetPath))
{
skippedOther++;
continue;
}
assetPath = assetPath.Replace('\\', '/');
if (AssetDatabase.IsValidFolder(assetPath))
{
skippedOther++;
continue;
}
// Require path under parent folder (avoid matching sibling folders e.g. .../A vs .../AB).
if (!assetPath.StartsWith(parentPrefix, System.StringComparison.OrdinalIgnoreCase))
{
skippedOther++;
continue;
}
if (ShouldSkipAsText(assetPath))
{
skippedText++;
continue;
}
string relative = GetRelativeAddress(parentPath, assetPath);
if (string.IsNullOrEmpty(relative))
{
skippedOther++;
continue;
}
try
{
var existingEntry = settings.FindAssetEntry(guid);
if (existingEntry != null && existingEntry.parentGroup != null && existingEntry.parentGroup.ReadOnly)
{
AppendLog($"ERROR: Cannot move or edit (source group is Read Only): {assetPath}");
errors++;
continue;
}
var entry = settings.CreateOrMoveEntry(guid, group, false, false);
if (entry == null)
{
AppendLog($"ERROR: CreateOrMoveEntry failed: {assetPath}");
errors++;
continue;
}
string newAddress = ToAddressWithoutExtension(relative);
entry.SetAddress(newAddress, false);
processed++;
}
catch (System.Exception ex)
{
AppendLog($"ERROR: {assetPath} — {ex.Message}");
errors++;
}
}
if (processed > 0)
{
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
}
AppendLog("");
AppendLog($"Done. Processed: {processed}, skipped (text): {skippedText}, skipped (other): {skippedOther}, errors: {errors}");
EditorUtility.DisplayDialog(
"Folder To Addressables",
$"Processed: {processed}\nSkipped (text): {skippedText}\nSkipped (other): {skippedOther}\nErrors: {errors}\nGroup: {groupName}",
"OK");
}
private static bool ShouldSkipAsText(string assetPath)
{
var mainType = AssetDatabase.GetMainAssetTypeAtPath(assetPath);
if (mainType == typeof(TextAsset))
return true;
string ext = Path.GetExtension(assetPath);
return !string.IsNullOrEmpty(ext) && TextExtensions.Contains(ext);
}
private static string GetRelativeAddress(string parentPath, string assetPath)
{
assetPath = assetPath.Replace('\\', '/');
parentPath = parentPath.Replace('\\', '/').TrimEnd('/');
if (string.Equals(assetPath, parentPath, System.StringComparison.OrdinalIgnoreCase))
return null;
string prefix = parentPath + "/";
if (!assetPath.StartsWith(prefix, System.StringComparison.OrdinalIgnoreCase))
return null;
return assetPath.Substring(prefix.Length);
}
/// <summary>
/// Address key: relative path with the final file extension removed (.png, .wav, .prefab, etc.).
/// </summary>
private static string ToAddressWithoutExtension(string relativePath)
{
if (string.IsNullOrEmpty(relativePath))
return relativePath;
relativePath = relativePath.Replace('\\', '/');
int lastSlash = relativePath.LastIndexOf('/');
string filePart = lastSlash >= 0 ? relativePath.Substring(lastSlash + 1) : relativePath;
string dirPart = lastSlash >= 0 ? relativePath.Substring(0, lastSlash) : "";
string fileNoExt = Path.GetFileNameWithoutExtension(filePart);
if (string.IsNullOrEmpty(fileNoExt))
fileNoExt = filePart;
if (string.IsNullOrEmpty(dirPart))
return fileNoExt;
return dirPart + "/" + fileNoExt;
}
private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
{
if (settings == null || string.IsNullOrEmpty(groupName))
return null;
AddressableAssetGroup group = settings.FindGroup(groupName);
if (group != null)
return group;
try
{
group = settings.CreateGroup(groupName, false, false, true, null);
if (group != null)
{
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
}
return group;
}
catch
{
return null;
}
}
private void AppendLog(string line)
{
if (string.IsNullOrEmpty(_log))
_log = line;
else
_log += "\n" + line;
Debug.Log($"[Folder To Addressables] {line}");
Repaint();
}
}
}
#endif