696 lines
23 KiB
C#
696 lines
23 KiB
C#
#if UNITY_EDITOR
|
||
using System;
|
||
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; by default the file extension is removed from the key.
|
||
/// Optional EditorPrefs: extra excluded extensions (never addressable), and saved address-tail presets with a per-run choice
|
||
/// of which tail to append to every address (after stripping the asset’s real extension).
|
||
/// Text files, extensionless files, and built-in excluded extensions (e.g. .pk) are skipped; existing addressable entries for those are removed.
|
||
/// </summary>
|
||
public class FolderToAddressablesWindow : EditorWindow
|
||
{
|
||
private const string MenuPath = "Tools/Addressable/Folder To Addressables…";
|
||
private const string EditorPrefsUserExcludedExtensionsKey = "BrewMonster.FolderToAddressables.UserExcludedExtensions";
|
||
private const string EditorPrefsAddressTailPresetsKey = "BrewMonster.FolderToAddressables.AddressTailPresets";
|
||
private const string EditorPrefsSelectedAddressTailKey = "BrewMonster.FolderToAddressables.SelectedAddressTail";
|
||
private const string EditorPrefsLegacyKeepExtensionInAddressKey = "BrewMonster.FolderToAddressables.KeepExtensionInAddress";
|
||
|
||
private static readonly HashSet<string> TextExtensions = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".ini", ".cfg", ".html", ".htm",
|
||
};
|
||
|
||
/// <summary>Extensions we never mark as Addressable (project-specific binary / sidecar types).</summary>
|
||
private static readonly HashSet<string> NeverAddressableExtensions = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".pk", ".sfk", ".sw",
|
||
};
|
||
|
||
private DefaultAsset _parentFolder;
|
||
private bool _includeFolderName;
|
||
private bool _overrideExisting;
|
||
private readonly List<string> _userExcludedExtensions = new List<string>();
|
||
private readonly HashSet<string> _userExcludedExtensionSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
private readonly List<string> _addressTailPresets = new List<string>();
|
||
private Vector2 _scrollExcluded;
|
||
private Vector2 _scrollAddressTails;
|
||
private Vector2 _scroll;
|
||
private string _log = "";
|
||
/// <summary>Normalized tail to append to every address this run (e.g. ".gfx"), or null for none.</summary>
|
||
private string _selectedAddressTail;
|
||
|
||
[MenuItem(MenuPath)]
|
||
public static void Open()
|
||
{
|
||
var w = GetWindow<FolderToAddressablesWindow>(false, "Folder To Addressables", true);
|
||
w.minSize = new Vector2(420, 520);
|
||
w.Show();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
LoadUserExcludedExtensionsFromPrefs();
|
||
LoadAddressTailPresetsFromPrefs();
|
||
LoadSelectedAddressTailFromPrefs();
|
||
}
|
||
|
||
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. " +
|
||
"The asset’s real extension is stripped from the address; you can optionally append one chosen tail from the presets (e.g. gfx/a/b.gfx). " +
|
||
"If nothing updates, ensure the target group is not Read Only in the Addressables Groups window. " +
|
||
"Files with no extension (e.g. raw data), .pk, built-in excluded types, and extensions in Extra excluded are not made addressable.",
|
||
MessageType.Info);
|
||
|
||
EditorGUILayout.Space(4);
|
||
|
||
EditorGUILayout.LabelField("Extra excluded extensions (EditorPrefs)", EditorStyles.boldLabel);
|
||
EditorGUILayout.HelpBox(
|
||
"Optional tails such as .gfx or .wav. These files are skipped and removed from Addressables like .pk. " +
|
||
"The list is stored in Editor preferences on this machine.",
|
||
MessageType.None);
|
||
_scrollExcluded = EditorGUILayout.BeginScrollView(_scrollExcluded, GUILayout.MaxHeight(120));
|
||
EditorGUI.BeginChangeCheck();
|
||
for (int i = 0; i < _userExcludedExtensions.Count; i++)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
_userExcludedExtensions[i] = EditorGUILayout.TextField(_userExcludedExtensions[i]);
|
||
if (GUILayout.Button("Remove", GUILayout.Width(64)))
|
||
{
|
||
_userExcludedExtensions.RemoveAt(i);
|
||
i--;
|
||
SaveUserExcludedExtensionsToPrefs();
|
||
continue;
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
if (EditorGUI.EndChangeCheck())
|
||
SaveUserExcludedExtensionsToPrefs();
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
if (GUILayout.Button("Add excluded type"))
|
||
{
|
||
_userExcludedExtensions.Add("");
|
||
SaveUserExcludedExtensionsToPrefs();
|
||
}
|
||
|
||
EditorGUILayout.Space(8);
|
||
|
||
EditorGUILayout.LabelField("Address tail presets (EditorPrefs)", EditorStyles.boldLabel);
|
||
EditorGUILayout.HelpBox(
|
||
"Add tails you reuse (e.g. .wav, .gfx). Then pick one below: that suffix is appended to every address for this run " +
|
||
"after the real file extension is removed (e.g. …/a/b.png with Include folder name and tail .gfx → gfx/a/b.gfx).",
|
||
MessageType.None);
|
||
_scrollAddressTails = EditorGUILayout.BeginScrollView(_scrollAddressTails, GUILayout.MaxHeight(100));
|
||
EditorGUI.BeginChangeCheck();
|
||
for (int i = 0; i < _addressTailPresets.Count; i++)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
_addressTailPresets[i] = EditorGUILayout.TextField(_addressTailPresets[i]);
|
||
if (GUILayout.Button("Remove", GUILayout.Width(64)))
|
||
{
|
||
_addressTailPresets.RemoveAt(i);
|
||
i--;
|
||
SaveAddressTailPresetsToPrefs();
|
||
NormalizeSelectedAddressTailAgainstPresets();
|
||
continue;
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
SaveAddressTailPresetsToPrefs();
|
||
NormalizeSelectedAddressTailAgainstPresets();
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
if (GUILayout.Button("Add tail preset"))
|
||
{
|
||
_addressTailPresets.Add("");
|
||
SaveAddressTailPresetsToPrefs();
|
||
}
|
||
|
||
List<string> orderedTails = GetOrderedNormalizedAddressTailPresets();
|
||
NormalizeSelectedAddressTailAgainstPresets();
|
||
string[] tailPopupOptions = new string[orderedTails.Count + 1];
|
||
tailPopupOptions[0] = "None (no added tail)";
|
||
for (int i = 0; i < orderedTails.Count; i++)
|
||
tailPopupOptions[i + 1] = orderedTails[i];
|
||
|
||
int tailPopupIndex = 0;
|
||
if (!string.IsNullOrEmpty(_selectedAddressTail))
|
||
{
|
||
for (int i = 0; i < orderedTails.Count; i++)
|
||
{
|
||
if (string.Equals(orderedTails[i], _selectedAddressTail, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
tailPopupIndex = i + 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
tailPopupIndex = EditorGUILayout.Popup(
|
||
new GUIContent(
|
||
"Address tail for this run",
|
||
"Appended to every generated address. The asset’s on-disk extension is always stripped first."),
|
||
tailPopupIndex,
|
||
tailPopupOptions);
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
_selectedAddressTail = tailPopupIndex <= 0 ? null : orderedTails[tailPopupIndex - 1];
|
||
SaveSelectedAddressTailToPrefs();
|
||
}
|
||
|
||
EditorGUILayout.Space(6);
|
||
|
||
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);
|
||
|
||
_includeFolderName = EditorGUILayout.Toggle(
|
||
new GUIContent(
|
||
"Include folder name in address",
|
||
"When on, the dropped folder name is prefixed to every address (e.g. gfx/a/b instead of a/b)."),
|
||
_includeFolderName);
|
||
|
||
_overrideExisting = EditorGUILayout.Toggle(
|
||
new GUIContent(
|
||
"Override existing addressables",
|
||
"When on, all assets under the folder are moved to this group and addresses are set. When off, assets that already have an Addressables entry are skipped."),
|
||
_overrideExisting);
|
||
|
||
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 = "";
|
||
RebuildUserExcludedExtensionSet();
|
||
NormalizeSelectedAddressTailAgainstPresets();
|
||
|
||
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 + "/";
|
||
|
||
int processed = 0;
|
||
int skippedText = 0;
|
||
int skippedNoExtension = 0;
|
||
int skippedExcludedExtension = 0;
|
||
int removedSkippedAddressable = 0;
|
||
int skippedOther = 0;
|
||
int skippedAlreadyAddressable = 0;
|
||
int errors = 0;
|
||
|
||
// --- Pass 1: Remove blocked entries already in this group ---
|
||
// AssetDatabase.FindAssets does NOT return extensionless files, so they would never reach
|
||
// the removal logic in the main loop. We clean them up here by iterating group.entries directly.
|
||
// Snapshot entries first to avoid modifying the collection while iterating.
|
||
if (group.entries != null)
|
||
{
|
||
var snapshot = new List<AddressableAssetEntry>(group.entries);
|
||
foreach (var existing in snapshot)
|
||
{
|
||
if (existing == null) continue;
|
||
string ap = existing.AssetPath.Replace('\\', '/');
|
||
if (!ap.StartsWith(parentPrefix, System.StringComparison.OrdinalIgnoreCase)) continue;
|
||
if (AssetDatabase.IsValidFolder(ap)) continue;
|
||
if (ShouldSkipExtensionless(ap) || ShouldSkipNeverAddressableExtension(ap))
|
||
{
|
||
if (settings.RemoveAssetEntry(existing.guid, false))
|
||
{
|
||
removedSkippedAddressable++;
|
||
AppendLog($"REMOVED (blocked type): {ap}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Pass 2: Collect processable GUIDs ---
|
||
// FindAssets returns recognized asset files. When overriding, also fold in group entries
|
||
// so assets already in this group get their addresses refreshed.
|
||
var guidsToProcess = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
||
foreach (var g in AssetDatabase.FindAssets("", new[] { parentPath }))
|
||
guidsToProcess.Add(g);
|
||
|
||
if (_overrideExisting && 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);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (ShouldSkipNeverAddressableExtension(assetPath))
|
||
{
|
||
TryRemoveAddressableIfPresent(settings, guid, ref removedSkippedAddressable);
|
||
skippedExcludedExtension++;
|
||
continue;
|
||
}
|
||
|
||
if (ShouldSkipExtensionless(assetPath))
|
||
{
|
||
TryRemoveAddressableIfPresent(settings, guid, ref removedSkippedAddressable);
|
||
skippedNoExtension++;
|
||
continue;
|
||
}
|
||
|
||
string relative = GetRelativeAddress(parentPath, assetPath);
|
||
if (string.IsNullOrEmpty(relative))
|
||
{
|
||
skippedOther++;
|
||
continue;
|
||
}
|
||
|
||
if (!_overrideExisting && settings.FindAssetEntry(guid) != null)
|
||
{
|
||
skippedAlreadyAddressable++;
|
||
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;
|
||
}
|
||
|
||
if (_includeFolderName)
|
||
relative = groupName + "/" + relative;
|
||
string baseAddress = ToAddressWithoutExtension(relative);
|
||
string newAddress = string.IsNullOrEmpty(_selectedAddressTail)
|
||
? baseAddress
|
||
: baseAddress + _selectedAddressTail;
|
||
entry.SetAddress(newAddress, false);
|
||
processed++;
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
AppendLog($"ERROR: {assetPath} — {ex.Message}");
|
||
errors++;
|
||
}
|
||
}
|
||
|
||
if (processed > 0 || removedSkippedAddressable > 0)
|
||
{
|
||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||
EditorUtility.SetDirty(settings);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
|
||
AppendLog("");
|
||
AppendLog(
|
||
$"Done. Processed: {processed}, removed (skipped types): {removedSkippedAddressable}, " +
|
||
$"skipped (already addressable): {skippedAlreadyAddressable}, " +
|
||
$"skipped (text): {skippedText}, skipped (excluded ext): {skippedExcludedExtension}, " +
|
||
$"skipped (no extension): {skippedNoExtension}, skipped (other): {skippedOther}, errors: {errors}");
|
||
|
||
EditorUtility.DisplayDialog(
|
||
"Folder To Addressables",
|
||
$"Processed: {processed}\nRemoved addressable (skipped types): {removedSkippedAddressable}\n" +
|
||
$"Skipped (already addressable): {skippedAlreadyAddressable}\n" +
|
||
$"Skipped (text): {skippedText}\nSkipped (excluded ext, e.g. .pk): {skippedExcludedExtension}\n" +
|
||
$"Skipped (no extension): {skippedNoExtension}\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 void LoadUserExcludedExtensionsFromPrefs()
|
||
{
|
||
_userExcludedExtensions.Clear();
|
||
string raw = EditorPrefs.GetString(EditorPrefsUserExcludedExtensionsKey, "");
|
||
if (string.IsNullOrEmpty(raw))
|
||
{
|
||
RebuildUserExcludedExtensionSet();
|
||
return;
|
||
}
|
||
|
||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var part in raw.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
|
||
{
|
||
string n = NormalizeExtensionToken(part);
|
||
if (n == null || !seen.Add(n))
|
||
continue;
|
||
_userExcludedExtensions.Add(n);
|
||
}
|
||
|
||
RebuildUserExcludedExtensionSet();
|
||
}
|
||
|
||
private void SaveUserExcludedExtensionsToPrefs()
|
||
{
|
||
var deduped = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
var lines = new List<string>();
|
||
foreach (string raw in _userExcludedExtensions)
|
||
{
|
||
string n = NormalizeExtensionToken(raw);
|
||
if (n == null || !deduped.Add(n))
|
||
continue;
|
||
lines.Add(n);
|
||
}
|
||
|
||
lines.Sort(StringComparer.OrdinalIgnoreCase);
|
||
EditorPrefs.SetString(EditorPrefsUserExcludedExtensionsKey, string.Join("\n", lines));
|
||
RebuildUserExcludedExtensionSet();
|
||
}
|
||
|
||
private void RebuildUserExcludedExtensionSet()
|
||
{
|
||
_userExcludedExtensionSet.Clear();
|
||
foreach (string raw in _userExcludedExtensions)
|
||
{
|
||
string n = NormalizeExtensionToken(raw);
|
||
if (n != null)
|
||
_userExcludedExtensionSet.Add(n);
|
||
}
|
||
}
|
||
|
||
private void LoadAddressTailPresetsFromPrefs()
|
||
{
|
||
_addressTailPresets.Clear();
|
||
string raw = EditorPrefs.GetString(EditorPrefsAddressTailPresetsKey, "");
|
||
if (string.IsNullOrEmpty(raw))
|
||
{
|
||
string legacy = EditorPrefs.GetString(EditorPrefsLegacyKeepExtensionInAddressKey, "");
|
||
if (!string.IsNullOrEmpty(legacy))
|
||
{
|
||
EditorPrefs.SetString(EditorPrefsAddressTailPresetsKey, legacy);
|
||
EditorPrefs.DeleteKey(EditorPrefsLegacyKeepExtensionInAddressKey);
|
||
raw = legacy;
|
||
}
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(raw))
|
||
return;
|
||
|
||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var part in raw.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
|
||
{
|
||
string n = NormalizeExtensionToken(part);
|
||
if (n == null || !seen.Add(n))
|
||
continue;
|
||
_addressTailPresets.Add(n);
|
||
}
|
||
}
|
||
|
||
private void SaveAddressTailPresetsToPrefs()
|
||
{
|
||
var deduped = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
var lines = new List<string>();
|
||
foreach (string raw in _addressTailPresets)
|
||
{
|
||
string n = NormalizeExtensionToken(raw);
|
||
if (n == null || !deduped.Add(n))
|
||
continue;
|
||
lines.Add(n);
|
||
}
|
||
|
||
lines.Sort(StringComparer.OrdinalIgnoreCase);
|
||
EditorPrefs.SetString(EditorPrefsAddressTailPresetsKey, string.Join("\n", lines));
|
||
}
|
||
|
||
private void LoadSelectedAddressTailFromPrefs()
|
||
{
|
||
string raw = EditorPrefs.GetString(EditorPrefsSelectedAddressTailKey, "");
|
||
_selectedAddressTail = string.IsNullOrWhiteSpace(raw) ? null : NormalizeExtensionToken(raw);
|
||
NormalizeSelectedAddressTailAgainstPresets();
|
||
}
|
||
|
||
private void SaveSelectedAddressTailToPrefs()
|
||
{
|
||
EditorPrefs.SetString(EditorPrefsSelectedAddressTailKey, _selectedAddressTail ?? "");
|
||
}
|
||
|
||
private List<string> GetOrderedNormalizedAddressTailPresets()
|
||
{
|
||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
var list = new List<string>();
|
||
foreach (string raw in _addressTailPresets)
|
||
{
|
||
string n = NormalizeExtensionToken(raw);
|
||
if (n == null || !seen.Add(n))
|
||
continue;
|
||
list.Add(n);
|
||
}
|
||
|
||
return list;
|
||
}
|
||
|
||
private void NormalizeSelectedAddressTailAgainstPresets()
|
||
{
|
||
if (string.IsNullOrEmpty(_selectedAddressTail))
|
||
return;
|
||
List<string> ordered = GetOrderedNormalizedAddressTailPresets();
|
||
foreach (string t in ordered)
|
||
{
|
||
if (string.Equals(t, _selectedAddressTail, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_selectedAddressTail = t;
|
||
return;
|
||
}
|
||
}
|
||
|
||
_selectedAddressTail = null;
|
||
SaveSelectedAddressTailToPrefs();
|
||
}
|
||
|
||
private static string NormalizeExtensionToken(string s)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(s))
|
||
return null;
|
||
s = s.Trim();
|
||
if (s.Length == 0)
|
||
return null;
|
||
if (s[0] != '.')
|
||
s = "." + s;
|
||
return s;
|
||
}
|
||
|
||
private bool ShouldSkipNeverAddressableExtension(string assetPath)
|
||
{
|
||
string ext = Path.GetExtension(assetPath);
|
||
if (string.IsNullOrEmpty(ext))
|
||
return false;
|
||
if (NeverAddressableExtensions.Contains(ext))
|
||
return true;
|
||
return _userExcludedExtensionSet.Contains(ext);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip generic/extensionless files (e.g. raw audio bytes) that show as plain assets in the Project window.
|
||
/// </summary>
|
||
private static bool ShouldSkipExtensionless(string assetPath)
|
||
{
|
||
return string.IsNullOrEmpty(Path.GetExtension(assetPath));
|
||
}
|
||
|
||
private static void TryRemoveAddressableIfPresent(AddressableAssetSettings settings, string guid, ref int removedCount)
|
||
{
|
||
if (settings.FindAssetEntry(guid) != null && settings.RemoveAssetEntry(guid, false))
|
||
removedCount++;
|
||
}
|
||
|
||
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
|