Files
ichni_Official/Packages/NB_FX/NBShaders2/Editor/ParticleBaseMigrationWindow.cs
SoulliesOfficial 40fa80cd70 NB_FX
2026-07-17 17:46:16 -04:00

692 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using NBShaderEditor;
using UnityEditor;
using UnityEngine;
namespace NBShaders2.Editor
{
[InitializeOnLoad]
internal static class ParticleBaseMigrationSettingsProvider
{
static ParticleBaseMigrationSettingsProvider()
{
NBFXProjectSettings.RegisterSettingsSection(
"ParticleBaseMigration",
() => new GUIContent(Text("particleBaseMigration.providerLabel", "ParticleBase Migration")),
OnGUI,
new[]
{
"ParticleBase",
"NBShader2",
"Migration",
"Material",
"Shader GUID"
},
95);
}
private static void OnGUI(string searchContext)
{
EditorGUILayout.HelpBox(
Text(
"particleBaseMigration.settingsHelp",
"Scan material assets that reference the legacy ParticleBase shader GUID and migrate them to NBShader2 after confirmation. Back up assets or commit to version control before converting."),
MessageType.Warning);
if (GUILayout.Button(ButtonContent("particleBaseMigration.openWindow", "Open ParticleBase Migration Window")))
{
ParticleBaseMigrationWindow.Open();
}
}
private static GUIContent ButtonContent(string key, string fallback)
{
return NBShaderInspectorLocalization.MakeContent("inspector." + key + ".button", fallback);
}
private static string Text(string key, string fallback)
{
return NBShaderInspectorLocalization.GetInspectorText(key, fallback);
}
}
internal sealed class ParticleBaseMigrationWindow : EditorWindow
{
private const string LegacyShaderGuidFallback = "7184a95c20fc1a441a8815af4c795ccd";
private const string NBShader2Guid = "7787bfdacec31472ca6644d6e3616bd4";
private const string LegacyShaderAssetPath = "Packages/com.xuanxuan.nb.fx/NBShaders/Shader/ParticleBase.shader";
private const string NBShader2AssetPath = "Packages/com.xuanxuan.nb.fx/NBShaders2/Shader/NBShader.shader";
private const string LegacyShaderName = "Effects/NBShader(Legacy)";
private const string UndoNameFallback = "Migrate ParticleBase Materials To NBShader2";
private static readonly string[] LegacyFoldoutProperties =
{
"_W9ParticleShaderGUIFoldToggle",
"_W9ParticleShaderGUIFoldToggle1",
"_W9ParticleShaderGUIFoldToggle2"
};
private static readonly string[] NBShader2FoldoutProperties =
{
"_NBShaderGUIFoldToggle",
"_NBShaderGUIFoldToggle1",
"_NBShaderGUIFoldToggle2"
};
private readonly List<MaterialMigrationEntry> _entries = new List<MaterialMigrationEntry>();
private Vector2 _scrollPosition;
private bool _scanned;
private string _statusMessage;
public static void Open()
{
var window = GetWindow<ParticleBaseMigrationWindow>(Text("particleBaseMigration.windowTitle", "ParticleBase Migration"));
window.minSize = new Vector2(760f, 420f);
window.Show();
}
private void OnGUI()
{
titleContent = new GUIContent(Text("particleBaseMigration.windowTitle", "ParticleBase Migration"));
EditorGUILayout.LabelField(Text("particleBaseMigration.title", "ParticleBase -> NBShader2"), EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
Text(
"particleBaseMigration.windowWarning",
"This operation changes material shader assignments and synchronized material state. Back up assets or commit to version control before converting. The tool does not provide an automatic rollback."),
MessageType.Warning);
DrawToolbar();
DrawStatus();
DrawMaterialList();
DrawConvertButton();
}
private void DrawToolbar()
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button(ButtonContent("particleBaseMigration.scan", "Scan ParticleBase Materials"), GUILayout.Height(24f)))
{
ScanMaterials();
}
EditorGUI.BeginDisabledGroup(_entries.Count == 0);
if (GUILayout.Button(ButtonContent("particleBaseMigration.selectAll", "Select All"), GUILayout.Width(96f), GUILayout.Height(24f)))
{
SetAllSelected(true);
}
if (GUILayout.Button(ButtonContent("particleBaseMigration.selectNone", "Select None"), GUILayout.Width(96f), GUILayout.Height(24f)))
{
SetAllSelected(false);
}
EditorGUI.EndDisabledGroup();
}
}
private void DrawStatus()
{
if (!string.IsNullOrEmpty(_statusMessage))
{
EditorGUILayout.HelpBox(_statusMessage, MessageType.Info);
}
else if (!_scanned)
{
EditorGUILayout.HelpBox(
Text(
"particleBaseMigration.scanPrompt",
"Click Scan to find .mat assets that reference the ParticleBase shader asset GUID. Shader name is only used as a fallback."),
MessageType.Info);
}
else if (_entries.Count == 0)
{
EditorGUILayout.HelpBox(
Text("particleBaseMigration.noMaterials", "No ParticleBase material assets were found."),
MessageType.Info);
}
}
private void DrawMaterialList()
{
if (_entries.Count == 0)
{
return;
}
EditorGUILayout.Space();
EditorGUILayout.LabelField(
FormatText("particleBaseMigration.listSummary", "Found {0} material asset(s). Selected: {1}", _entries.Count, CountSelected()),
EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
GUILayout.Label("", GUILayout.Width(24f));
GUILayout.Label(Text("particleBaseMigration.column.material", "Material"), GUILayout.Width(240f));
GUILayout.Label(Text("particleBaseMigration.column.match", "Match"), GUILayout.Width(160f));
GUILayout.Label(Text("particleBaseMigration.column.path", "Path"), GUILayout.MinWidth(260f));
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
for (int i = 0; i < _entries.Count; i++)
{
DrawMaterialRow(_entries[i]);
}
EditorGUILayout.EndScrollView();
}
private static void DrawMaterialRow(MaterialMigrationEntry entry)
{
using (new EditorGUILayout.HorizontalScope())
{
entry.selected = EditorGUILayout.Toggle(entry.selected, GUILayout.Width(24f));
EditorGUILayout.ObjectField(entry.material, typeof(Material), false, GUILayout.Width(240f));
GUILayout.Label(entry.matchSource, EditorStyles.miniLabel, GUILayout.Width(160f));
if (GUILayout.Button(ButtonContent("particleBaseMigration.ping", "Ping"), GUILayout.Width(48f)))
{
PingAsset(entry.assetPath);
}
EditorGUILayout.SelectableLabel(
entry.assetPath,
EditorStyles.miniLabel,
GUILayout.Height(EditorGUIUtility.singleLineHeight),
GUILayout.MinWidth(220f));
}
}
private void DrawConvertButton()
{
EditorGUILayout.Space();
EditorGUI.BeginDisabledGroup(CountSelected() == 0);
if (GUILayout.Button(ButtonContent("particleBaseMigration.convertSelected", "Convert Selected Materials To NBShader2"), GUILayout.Height(30f)))
{
ConvertSelectedMaterials();
}
EditorGUI.EndDisabledGroup();
}
private void ScanMaterials()
{
_entries.Clear();
_scanned = true;
_statusMessage = null;
string legacyShaderGuid = ResolveShaderGuid(LegacyShaderAssetPath, LegacyShaderGuidFallback);
Shader legacyShader = LoadShaderByGuidOrPath(legacyShaderGuid, LegacyShaderAssetPath);
string[] materialGuids = AssetDatabase.FindAssets("t:Material");
var seenPaths = new HashSet<string>(StringComparer.Ordinal);
bool canceled = false;
try
{
for (int i = 0; i < materialGuids.Length; i++)
{
if (EditorUtility.DisplayCancelableProgressBar(
Text("particleBaseMigration.windowTitle", "ParticleBase Migration"),
FormatText("particleBaseMigration.progress.scanning", "Scanning materials {0}/{1}", i + 1, materialGuids.Length),
materialGuids.Length > 0 ? (float)i / materialGuids.Length : 1f))
{
canceled = true;
break;
}
string path = AssetDatabase.GUIDToAssetPath(materialGuids[i]);
if (string.IsNullOrEmpty(path) ||
!path.EndsWith(".mat", StringComparison.OrdinalIgnoreCase) ||
!seenPaths.Add(path))
{
continue;
}
Material material = AssetDatabase.LoadAssetAtPath<Material>(path);
string matchSource;
if (!IsLegacyParticleBaseMaterial(path, material, legacyShaderGuid, legacyShader, out matchSource))
{
continue;
}
_entries.Add(new MaterialMigrationEntry
{
assetPath = path,
material = material,
selected = true,
matchSource = matchSource
});
}
}
finally
{
EditorUtility.ClearProgressBar();
}
_entries.Sort((a, b) => string.CompareOrdinal(a.assetPath, b.assetPath));
_statusMessage = canceled
? FormatText("particleBaseMigration.scanCanceled", "Scan canceled. Found {0} material asset(s) before canceling.", _entries.Count)
: FormatText("particleBaseMigration.scanComplete", "Scan complete. Found {0} ParticleBase material asset(s).", _entries.Count);
}
private void ConvertSelectedMaterials()
{
List<MaterialMigrationEntry> selectedEntries = GetSelectedEntries();
if (selectedEntries.Count == 0)
{
return;
}
Shader targetShader = LoadShaderByGuidOrPath(NBShader2Guid, NBShader2AssetPath);
if (targetShader == null)
{
EditorUtility.DisplayDialog(
Text("particleBaseMigration.nbShader2Missing.title", "NBShader2 Missing"),
Text("particleBaseMigration.nbShader2Missing.message", "Could not load NBShader2 shader asset. Migration cannot continue."),
Text("particleBaseMigration.dialog.ok", "OK"));
return;
}
if (!ConfirmConversion(selectedEntries.Count))
{
return;
}
string legacyShaderGuid = ResolveShaderGuid(LegacyShaderAssetPath, LegacyShaderGuidFallback);
Shader legacyShader = LoadShaderByGuidOrPath(legacyShaderGuid, LegacyShaderAssetPath);
int convertedCount = 0;
bool canceled = false;
var failures = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
bool assetEditingStarted = false;
string undoName = Text("particleBaseMigration.undo", UndoNameFallback);
Undo.SetCurrentGroupName(undoName);
try
{
AssetDatabase.StartAssetEditing();
assetEditingStarted = true;
for (int i = 0; i < selectedEntries.Count; i++)
{
MaterialMigrationEntry entry = selectedEntries[i];
if (EditorUtility.DisplayCancelableProgressBar(
Text("particleBaseMigration.windowTitle", "ParticleBase Migration"),
FormatText("particleBaseMigration.progress.converting", "Converting {0}/{1}: {2}", i + 1, selectedEntries.Count, entry.assetPath),
selectedEntries.Count > 0 ? (float)i / selectedEntries.Count : 1f))
{
canceled = true;
break;
}
if (ConvertMaterialEntry(entry, targetShader, legacyShaderGuid, legacyShader, undoName, failures))
{
convertedCount++;
}
}
}
finally
{
if (assetEditingStarted)
{
AssetDatabase.StopAssetEditing();
}
EditorUtility.ClearProgressBar();
Undo.CollapseUndoOperations(undoGroup);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
if (failures.Count > 0)
{
Debug.LogWarning(
Text("particleBaseMigration.failuresLogged", "ParticleBase migration completed with failures:") +
"\n" +
string.Join("\n", failures.ToArray()));
}
_statusMessage = BuildConversionStatus(convertedCount, selectedEntries.Count, canceled, failures.Count);
}
private static bool ConvertMaterialEntry(
MaterialMigrationEntry entry,
Shader targetShader,
string legacyShaderGuid,
Shader legacyShader,
string undoName,
List<string> failures)
{
try
{
Material material = AssetDatabase.LoadAssetAtPath<Material>(entry.assetPath);
if (material == null)
{
failures.Add(FormatText("particleBaseMigration.failure.loadMaterial", "{0}: material could not be loaded.", entry.assetPath));
return false;
}
string matchSource;
if (!IsLegacyParticleBaseMaterial(entry.assetPath, material, legacyShaderGuid, legacyShader, out matchSource))
{
failures.Add(FormatText("particleBaseMigration.failure.noLongerParticleBase", "{0}: skipped because it no longer references ParticleBase.", entry.assetPath));
return false;
}
int[] legacyFoldoutValues = CaptureLegacyFoldoutValues(material, entry.assetPath);
Undo.RecordObject(material, undoName);
material.shader = targetShader;
ApplyNBShader2FoldoutValues(material, legacyFoldoutValues);
NBShaderSyncService.SyncMaterialState(material);
EditorUtility.SetDirty(material);
AssetDatabase.SaveAssetIfDirty(material);
entry.material = material;
entry.matchSource = Text("particleBaseMigration.match.converted", "Converted");
entry.selected = false;
return true;
}
catch (Exception ex)
{
failures.Add(FormatText("particleBaseMigration.failure.exception", "{0}: {1}", entry.assetPath, ex.Message));
return false;
}
}
private static bool ConfirmConversion(int count)
{
return EditorUtility.DisplayDialog(
Text("particleBaseMigration.confirm.title", "Confirm ParticleBase Migration"),
FormatText(
"particleBaseMigration.confirm.message",
"You are about to convert {0} material asset(s) from ParticleBase to NBShader2.\n\nBack up assets or commit to version control before continuing. This tool does not provide an automatic rollback.",
count),
Text("particleBaseMigration.dialog.convert", "Convert"),
Text("particleBaseMigration.dialog.cancel", "Cancel"));
}
private static string BuildConversionStatus(int converted, int selected, bool canceled, int failureCount)
{
string status = canceled
? FormatText("particleBaseMigration.conversionCanceled", "Conversion canceled. Converted {0}/{1} selected material asset(s).", converted, selected)
: FormatText("particleBaseMigration.conversionComplete", "Conversion complete. Converted {0}/{1} selected material asset(s).", converted, selected);
if (failureCount > 0)
{
status += " " + FormatText("particleBaseMigration.conversionFailures", "{0} failure(s) were logged to the Console.", failureCount);
}
return status;
}
private static int[] CaptureLegacyFoldoutValues(Material material, string assetPath)
{
var values = new int[LegacyFoldoutProperties.Length];
for (int i = 0; i < LegacyFoldoutProperties.Length; i++)
{
values[i] = int.MinValue;
if (material != null && material.HasProperty(LegacyFoldoutProperties[i]))
{
values[i] = material.GetInteger(LegacyFoldoutProperties[i]);
continue;
}
int serializedValue;
if (TryReadSerializedIntegerProperty(assetPath, LegacyFoldoutProperties[i], out serializedValue))
{
values[i] = serializedValue;
}
}
return values;
}
private static void ApplyNBShader2FoldoutValues(Material material, int[] legacyValues)
{
if (material == null || legacyValues == null)
{
return;
}
int count = Math.Min(legacyValues.Length, NBShader2FoldoutProperties.Length);
for (int i = 0; i < count; i++)
{
if (legacyValues[i] == int.MinValue || !material.HasProperty(NBShader2FoldoutProperties[i]))
{
continue;
}
material.SetInteger(NBShader2FoldoutProperties[i], legacyValues[i]);
}
}
private static bool IsLegacyParticleBaseMaterial(
string assetPath,
Material material,
string legacyShaderGuid,
Shader legacyShader,
out string matchSource)
{
if (SerializedMaterialReferencesShaderGuid(assetPath, legacyShaderGuid))
{
matchSource = Text("particleBaseMigration.match.shaderGuid", "Shader GUID");
return true;
}
if (material != null && material.shader != null)
{
if (legacyShader != null && material.shader == legacyShader)
{
matchSource = Text("particleBaseMigration.match.shaderObject", "Shader object");
return true;
}
if (string.Equals(material.shader.name, LegacyShaderName, StringComparison.Ordinal))
{
matchSource = Text("particleBaseMigration.match.shaderNameFallback", "Shader name fallback");
return true;
}
}
matchSource = null;
return false;
}
private static bool SerializedMaterialReferencesShaderGuid(string assetPath, string shaderGuid)
{
if (string.IsNullOrEmpty(assetPath) || string.IsNullOrEmpty(shaderGuid))
{
return false;
}
string physicalPath;
if (!TryGetPhysicalPath(assetPath, out physicalPath) || !File.Exists(physicalPath))
{
return false;
}
try
{
string content = File.ReadAllText(physicalPath);
return content.IndexOf("m_Shader:", StringComparison.Ordinal) >= 0 &&
content.IndexOf("guid: " + shaderGuid, StringComparison.OrdinalIgnoreCase) >= 0;
}
catch
{
return false;
}
}
private static bool TryReadSerializedIntegerProperty(string assetPath, string propertyName, out int value)
{
value = 0;
if (string.IsNullOrEmpty(assetPath) || string.IsNullOrEmpty(propertyName))
{
return false;
}
string physicalPath;
if (!TryGetPhysicalPath(assetPath, out physicalPath) || !File.Exists(physicalPath))
{
return false;
}
try
{
string prefix = "- " + propertyName + ":";
using (var reader = new StringReader(File.ReadAllText(physicalPath)))
{
string line;
while ((line = reader.ReadLine()) != null)
{
string trimmed = line.Trim();
if (!trimmed.StartsWith(prefix, StringComparison.Ordinal))
{
continue;
}
string rawValue = trimmed.Substring(prefix.Length).Trim();
return int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
}
}
}
catch
{
return false;
}
return false;
}
private static bool TryGetPhysicalPath(string assetPath, out string physicalPath)
{
physicalPath = null;
if (string.IsNullOrEmpty(assetPath))
{
return false;
}
string normalizedPath = assetPath.Replace('\\', '/');
if (normalizedPath.StartsWith("Assets/", StringComparison.Ordinal) ||
string.Equals(normalizedPath, "Assets", StringComparison.Ordinal))
{
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
physicalPath = Path.Combine(projectRoot, normalizedPath.Replace('/', Path.DirectorySeparatorChar));
return true;
}
if (!normalizedPath.StartsWith("Packages/", StringComparison.Ordinal))
{
return false;
}
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(normalizedPath);
if (packageInfo == null || string.IsNullOrEmpty(packageInfo.resolvedPath))
{
return false;
}
string packageRoot = "Packages/" + packageInfo.name + "/";
if (!normalizedPath.StartsWith(packageRoot, StringComparison.Ordinal))
{
return false;
}
string relativePath = normalizedPath.Substring(packageRoot.Length).Replace('/', Path.DirectorySeparatorChar);
physicalPath = Path.Combine(packageInfo.resolvedPath, relativePath);
return true;
}
private static string ResolveShaderGuid(string assetPath, string fallbackGuid)
{
string guid = AssetDatabase.AssetPathToGUID(assetPath);
return string.IsNullOrEmpty(guid) ? fallbackGuid : guid;
}
private static Shader LoadShaderByGuidOrPath(string guid, string fallbackAssetPath)
{
string assetPath = string.IsNullOrEmpty(guid) ? null : AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(assetPath))
{
assetPath = fallbackAssetPath;
}
return AssetDatabase.LoadAssetAtPath<Shader>(assetPath);
}
private void SetAllSelected(bool selected)
{
for (int i = 0; i < _entries.Count; i++)
{
_entries[i].selected = selected;
}
}
private int CountSelected()
{
int count = 0;
for (int i = 0; i < _entries.Count; i++)
{
if (_entries[i].selected)
{
count++;
}
}
return count;
}
private List<MaterialMigrationEntry> GetSelectedEntries()
{
var selectedEntries = new List<MaterialMigrationEntry>();
for (int i = 0; i < _entries.Count; i++)
{
if (_entries[i].selected)
{
selectedEntries.Add(_entries[i]);
}
}
return selectedEntries;
}
private static void PingAsset(string assetPath)
{
UnityEngine.Object asset = AssetDatabase.LoadMainAssetAtPath(assetPath);
if (asset == null)
{
return;
}
Selection.activeObject = asset;
EditorGUIUtility.PingObject(asset);
}
private static GUIContent ButtonContent(string key, string fallback)
{
return NBShaderInspectorLocalization.MakeContent("inspector." + key + ".button", fallback);
}
private static string Text(string key, string fallback)
{
return NBShaderInspectorLocalization.GetInspectorText(key, fallback);
}
private static string FormatText(string key, string fallback, params object[] args)
{
return string.Format(Text(key, fallback), args);
}
private sealed class MaterialMigrationEntry
{
public string assetPath;
public Material material;
public string matchSource;
public bool selected;
}
}
}