using System; using System.Collections.Generic; using System.IO; using System.Reflection; using AlicizaX.InputGlyph; using UnityEditor; using UnityEngine; using UnityEngine.U2D; using TMPro; [CustomEditor(typeof(InputGlyphDatabase))] public class InputGlyphDatabaseEditor : Editor { SerializedProperty tablesProp; SerializedProperty placeholderSpriteProp; InputGlyphDatabase db; int tabIndex = 0; bool showAddField = false; string newTableName = ""; List searchStrings = new List(); List currentPages = new List(); const int itemsPerPage = 10; const int previewSize = 52; void OnEnable() { db = target as InputGlyphDatabase; tablesProp = serializedObject.FindProperty("tables"); placeholderSpriteProp = serializedObject.FindProperty("placeholderSprite"); if (tablesProp == null) { Debug.LogError("Could not find serialized property 'tables' on InputGlyphDatabase. Check field name."); return; } EnsureDefaultTable("Keyboard"); EnsureDefaultTable("Xbox"); EnsureDefaultTable("PlayStation"); SyncEditorListsWithTables(); } public override void OnInspectorGUI() { serializedObject.Update(); if (db == null || tablesProp == null) return; EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Space(4); if (GUILayout.Button("Save Asset", EditorStyles.toolbarButton)) { serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); } GUILayout.FlexibleSpace(); if (GUILayout.Button(showAddField ? "Cancel +" : "+ Add Table", EditorStyles.toolbarButton, GUILayout.Width(110))) { showAddField = !showAddField; newTableName = ""; } int settingsIndex = tablesProp != null ? tablesProp.arraySize : 0; bool settingsSelected = (tabIndex == settingsIndex); if (GUILayout.Toggle(settingsSelected, "Settings", EditorStyles.toolbarButton, GUILayout.Width(90)) != settingsSelected) { tabIndex = (tabIndex == settingsIndex) ? 0 : settingsIndex; } EditorGUILayout.EndHorizontal(); if (showAddField) { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Label("Name:", GUILayout.Width(40)); newTableName = EditorGUILayout.TextField(newTableName); if (GUILayout.Button("Add", EditorStyles.toolbarButton, GUILayout.Width(80))) { string trimmed = newTableName != null ? newTableName.Trim() : ""; if (string.IsNullOrEmpty(trimmed)) { EditorUtility.DisplayDialog("Invalid Name", "Table name cannot be empty.", "OK"); } else { bool exists = false; for (int i = 0; i < tablesProp.arraySize; ++i) { var t = tablesProp.GetArrayElementAtIndex(i); var nameProp = t.FindPropertyRelative("deviceName"); if (nameProp != null && string.Equals(nameProp.stringValue, trimmed, StringComparison.OrdinalIgnoreCase)) { exists = true; break; } } if (exists) { EditorUtility.DisplayDialog("Duplicate", "A table with that name already exists.", "OK"); } else { int newIndex = tablesProp.arraySize; tablesProp.InsertArrayElementAtIndex(newIndex); var newTable = tablesProp.GetArrayElementAtIndex(newIndex); var nameProp = newTable.FindPropertyRelative("deviceName"); if (nameProp != null) nameProp.stringValue = trimmed; var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset"); if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null; var atlasProp = newTable.FindPropertyRelative("spriteAtlas"); if (atlasProp != null) atlasProp.objectReferenceValue = null; var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); if (sheetProp != null) sheetProp.objectReferenceValue = null; var entriesProp = newTable.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); SyncEditorListsWithTables(); showAddField = false; tabIndex = tablesProp.arraySize - 1; } } } if (GUILayout.Button("Cancel", EditorStyles.toolbarButton, GUILayout.Width(80))) { showAddField = false; newTableName = ""; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.Space(6); EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); int tablesCount = tablesProp.arraySize; for (int i = 0; i < tablesCount; ++i) { var t = tablesProp.GetArrayElementAtIndex(i); var nameProp = t.FindPropertyRelative("deviceName"); string name = nameProp != null ? nameProp.stringValue : ("Table " + i); bool selected = (tabIndex == i); if (GUILayout.Toggle(selected, name, EditorStyles.toolbarButton, GUILayout.MinWidth(60))) { tabIndex = i; } if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22))) { if (EditorUtility.DisplayDialog("Delete Table?", $"Delete table '{name}' and all its entries? This cannot be undone.", "Delete", "Cancel")) { tablesProp.DeleteArrayElementAtIndex(i); serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); SyncEditorListsWithTables(); tabIndex = Mathf.Clamp(tabIndex, 0, Math.Max(0, tablesProp.arraySize - 1)); return; } } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(8); EditorGUILayout.BeginVertical("box"); if (tabIndex == tablesProp.arraySize) { // Settings EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel); EditorGUILayout.Space(4); EditorGUILayout.PropertyField(placeholderSpriteProp, new GUIContent("Placeholder Sprite")); Sprite placeholder = placeholderSpriteProp.objectReferenceValue as Sprite; EditorGUILayout.Space(6); EditorGUILayout.LabelField("Preview", EditorStyles.miniBoldLabel); if (placeholder != null) { Texture2D preview = AssetPreview.GetAssetPreview(placeholder); if (preview == null) preview = AssetPreview.GetMiniThumbnail(placeholder); if (preview != null) GUILayout.Label(preview, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); else EditorGUILayout.ObjectField(placeholder, typeof(Sprite), false, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); } else { EditorGUILayout.HelpBox("No placeholder sprite assigned. If FindEntryByControlPath receives an empty path, it will return null.", MessageType.Info); } } else { if (tabIndex < 0 || tabIndex >= tablesProp.arraySize) { EditorGUILayout.HelpBox("Invalid table index.", MessageType.Error); } else { var tableProp = tablesProp.GetArrayElementAtIndex(tabIndex); EnsureEditorListsLength(); GUILayout.BeginHorizontal(); GUIStyle searchStyle = EditorStyles.toolbarSearchField ?? EditorStyles.textField; searchStrings[tabIndex] = GUILayout.TextField(searchStrings[tabIndex] ?? "", searchStyle); GUILayout.EndHorizontal(); EditorGUILayout.Space(6); // TMP Asset row var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset"); EditorGUILayout.BeginHorizontal(); GUILayout.Label("TMP Sprite Asset", GUILayout.Width(140)); EditorGUILayout.PropertyField(tmpAssetProp, GUIContent.none, GUILayout.ExpandWidth(true)); if (GUILayout.Button("Parse TMP Asset", GUILayout.Width(120))) { ParseTMPAssetIntoTableSerialized(tableProp); } if (GUILayout.Button("Clear", GUILayout.Width(80))) { var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; var nameProp = tableProp.FindPropertyRelative("deviceName"); if (nameProp != null && db != null) { var deviceName = nameProp.stringValue; var table = db.GetTable(deviceName); if (table != null) table.entries.Clear(); } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); currentPages[tabIndex] = 0; } EditorGUILayout.EndHorizontal(); // SpriteAtlas row var atlasProp = tableProp.FindPropertyRelative("spriteAtlas"); EditorGUILayout.BeginHorizontal(); GUILayout.Label("Sprite Atlas", GUILayout.Width(140)); EditorGUILayout.PropertyField(atlasProp, GUIContent.none, GUILayout.ExpandWidth(true)); if (GUILayout.Button("Parse Sprite Atlas", GUILayout.Width(120))) { ParseSpriteAtlasIntoTableSerialized(tableProp); } if (GUILayout.Button("Clear", GUILayout.Width(80))) { var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; var nameProp = tableProp.FindPropertyRelative("deviceName"); if (nameProp != null && db != null) { var deviceName = nameProp.stringValue; var table = db.GetTable(deviceName); if (table != null) table.entries.Clear(); } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); currentPages[tabIndex] = 0; } EditorGUILayout.EndHorizontal(); // SpriteSheet (Texture2D with Multiple) row var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); EditorGUILayout.BeginHorizontal(); GUILayout.Label("Sprite Sheet (Texture2D)", GUILayout.Width(140)); EditorGUILayout.PropertyField(sheetProp, GUIContent.none, GUILayout.ExpandWidth(true)); if (GUILayout.Button("Parse Sprite Sheet", GUILayout.Width(120))) { ParseSpriteSheetIntoTableSerialized(tableProp); } if (GUILayout.Button("Clear", GUILayout.Width(80))) { var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; var nameProp = tableProp.FindPropertyRelative("deviceName"); if (nameProp != null && db != null) { var deviceName = nameProp.stringValue; var table = db.GetTable(deviceName); if (table != null) table.entries.Clear(); } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); currentPages[tabIndex] = 0; } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(6); var entries = tableProp.FindPropertyRelative("entries"); if (entries != null) { int total = entries.arraySize; List matchedIndices = new List(); string query = (searchStrings[tabIndex] ?? "").Trim(); for (int i = 0; i < total; ++i) { var eProp = entries.GetArrayElementAtIndex(i); if (eProp == null) continue; var spriteProp = eProp.FindPropertyRelative("Sprite"); Sprite s = spriteProp.objectReferenceValue as Sprite; string name = s != null ? s.name : ""; if (string.IsNullOrEmpty(query) || name.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0) { matchedIndices.Add(i); } } int matchedTotal = matchedIndices.Count; int totalPages = Mathf.Max(1, (matchedTotal + itemsPerPage - 1) / itemsPerPage); currentPages[tabIndex] = Mathf.Clamp(currentPages[tabIndex], 0, totalPages - 1); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("<<", EditorStyles.miniButtonLeft, GUILayout.Width(36))) { currentPages[tabIndex] = 0; } if (GUILayout.Button("<", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Max(0, currentPages[tabIndex] - 1); } GUILayout.FlexibleSpace(); EditorGUILayout.LabelField(string.Format("Page {0}/{1}", currentPages[tabIndex] + 1, totalPages), GUILayout.Width(120)); GUILayout.FlexibleSpace(); if (GUILayout.Button(">", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Min(totalPages - 1, currentPages[tabIndex] + 1); } if (GUILayout.Button(">>", EditorStyles.miniButtonRight, GUILayout.Width(36))) { currentPages[tabIndex] = totalPages - 1; } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); int start = currentPages[tabIndex] * itemsPerPage; int end = Math.Min(start + itemsPerPage, matchedTotal); for (int mi = start; mi < end; ++mi) { int i = matchedIndices[mi]; var eProp = entries.GetArrayElementAtIndex(i); if (eProp == null) continue; using (new EditorGUILayout.HorizontalScope("box")) { using (new EditorGUILayout.VerticalScope(GUILayout.Width(80))) { var spriteProp = eProp.FindPropertyRelative("Sprite"); Sprite s = spriteProp.objectReferenceValue as Sprite; EditorGUILayout.LabelField(s != null ? s.name : "", EditorStyles.boldLabel); if (s != null) { Texture2D preview = AssetPreview.GetAssetPreview(s); if (preview == null) preview = AssetPreview.GetMiniThumbnail(s); if (preview != null) { GUILayout.Label(preview, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); } else { EditorGUILayout.PropertyField(spriteProp, GUIContent.none, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); } } else { EditorGUILayout.PropertyField(spriteProp, GUIContent.none, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); } } EditorGUILayout.BeginVertical(); var actionProp = eProp.FindPropertyRelative("action"); EditorGUILayout.Space(2); EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true)); EditorGUILayout.EndVertical(); } EditorGUILayout.Space(4); } if (matchedTotal == 0) { EditorGUILayout.HelpBox("No entries match the search.", MessageType.Info); } } } } EditorGUILayout.EndVertical(); EditorGUILayout.Space(6); serializedObject.ApplyModifiedProperties(); } void EnsureDefaultTable(string name) { if (tablesProp == null) return; for (int i = 0; i < tablesProp.arraySize; ++i) { var t = tablesProp.GetArrayElementAtIndex(i); var nameProp = t.FindPropertyRelative("deviceName"); if (nameProp != null && string.Equals(nameProp.stringValue, name, StringComparison.OrdinalIgnoreCase)) return; } int idx = tablesProp.arraySize; tablesProp.InsertArrayElementAtIndex(idx); var newTable = tablesProp.GetArrayElementAtIndex(idx); var deviceNameProp = newTable.FindPropertyRelative("deviceName"); if (deviceNameProp != null) deviceNameProp.stringValue = name; var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset"); if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null; var atlasProp = newTable.FindPropertyRelative("spriteAtlas"); if (atlasProp != null) atlasProp.objectReferenceValue = null; var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); if (sheetProp != null) sheetProp.objectReferenceValue = null; var entriesProp = newTable.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); } void SyncEditorListsWithTables() { int count = tablesProp != null ? tablesProp.arraySize : 0; if (searchStrings == null) searchStrings = new List(); if (currentPages == null) currentPages = new List(); while (searchStrings.Count < count) searchStrings.Add(""); while (currentPages.Count < count) currentPages.Add(0); while (searchStrings.Count > count) searchStrings.RemoveAt(searchStrings.Count - 1); while (currentPages.Count > count) currentPages.RemoveAt(currentPages.Count - 1); } void EnsureEditorListsLength() { if (tablesProp == null) return; SyncEditorListsWithTables(); } // ----- Parse TMP SpriteAsset(增强版) ----- void ParseTMPAssetIntoTableSerialized(SerializedProperty tableProp) { if (tableProp == null) return; var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset"); var asset = tmpAssetProp.objectReferenceValue as TMP_SpriteAsset; if (asset == null) { Debug.LogWarning("[InputGlyphDatabase] TMP Sprite Asset is null for table."); return; } var nameProp = tableProp.FindPropertyRelative("deviceName"); string deviceName = nameProp != null ? nameProp.stringValue : ""; int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); if (tableIndex < 0) { Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); return; } var tableObj = db.tables[tableIndex]; tableObj.entries.Clear(); var chars = asset.spriteCharacterTable; SpriteAtlas atlas = GetSpriteAtlasFromTMP(asset); string assetPath = AssetDatabase.GetAssetPath(asset); string assetFolder = !string.IsNullOrEmpty(assetPath) ? Path.GetDirectoryName(assetPath) : null; int foundCount = 0; for (int i = 0; i < chars.Count; ++i) { var ch = chars[i]; if (ch == null) continue; string name = ch.name; if (string.IsNullOrEmpty(name)) name = $"glyph_{i}"; Sprite s = null; // 1) 尝试从 glyph / TMP_SpriteGlyph 中取 sprite try { var glyph = ch.glyph as TMP_SpriteGlyph; if (glyph != null) { var possible = typeof(TMP_SpriteGlyph).GetProperty("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (possible != null) { s = possible.GetValue(glyph, null) as Sprite; } else { var f = typeof(TMP_SpriteGlyph).GetField("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (f != null) s = f.GetValue(glyph) as Sprite; } } } catch { s = null; } // 2) atlas 查找 if (s == null && atlas != null) { try { s = atlas.GetSprite(name); } catch { s = null; } if (s == null) { try { var m = typeof(SpriteAtlas).GetMethod("GetSprite", new Type[] { typeof(string) }); if (m != null) s = m.Invoke(atlas, new object[] { name }) as Sprite; } catch { s = null; } } } // 3) asset folder scope 查找 if (s == null && !string.IsNullOrEmpty(assetFolder)) { try { string[] scoped = AssetDatabase.FindAssets($"\"{name}\" t:Sprite", new[] { assetFolder }); if (scoped != null && scoped.Length > 0) { foreach (var g in scoped) { var p = AssetDatabase.GUIDToAssetPath(g); var sp = AssetDatabase.LoadAssetAtPath(p); if (sp != null && sp.name == name) { s = sp; break; } } } } catch { s = null; } } // 4) 全项目查找 if (s == null) { try { string[] all = AssetDatabase.FindAssets($"{name} t:Sprite"); if (all != null && all.Length > 0) { foreach (var g in all) { var p = AssetDatabase.GUIDToAssetPath(g); var sp = AssetDatabase.LoadAssetAtPath(p); if (sp != null && sp.name == name) { s = sp; break; } } } } catch { s = null; } } // 5) LoadAllAssetsAtPath (TMP asset 本身) 作为最后手段 if (s == null && !string.IsNullOrEmpty(assetPath)) { try { var allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath); if (allAssets != null) { foreach (var obj in allAssets) { if (obj is Sprite sp && sp.name == name) { s = sp; break; } } } } catch { s = null; } } GlyphEntry entry = new GlyphEntry(); entry.Sprite = s; entry.action = null; tableObj.entries.Add(entry); if (s != null) foundCount++; else Debug.LogWarning($"[InputGlyphDatabase] Failed to resolve sprite '{name}' for TMP asset '{asset.name}' (table '{deviceName}')."); } // 按名字逐字符排序(不区分大小写) tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name)); EditorUtility.SetDirty(db); serializedObject.Update(); serializedObject.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); Debug.Log($"[InputGlyphDatabase] Parsed TMP '{asset.name}' into table '{deviceName}'. chars={chars.Count}, resolvedSprites={foundCount}"); } // ----- Parse SpriteAtlas(Unity SpriteAtlas) ----- void ParseSpriteAtlasIntoTableSerialized(SerializedProperty tableProp) { if (tableProp == null) return; var atlasProp = tableProp.FindPropertyRelative("spriteAtlas"); var atlas = atlasProp != null ? atlasProp.objectReferenceValue as SpriteAtlas : null; if (atlas == null) { Debug.LogWarning("[InputGlyphDatabase] SpriteAtlas is null for table."); return; } var nameProp = tableProp.FindPropertyRelative("deviceName"); string deviceName = nameProp != null ? nameProp.stringValue : ""; int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); if (tableIndex < 0) { Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); return; } var tableObj = db.tables[tableIndex]; tableObj.entries.Clear(); string[] guids = AssetDatabase.FindAssets("t:Sprite"); int added = 0; try { for (int gi = 0; gi < guids.Length; ++gi) { var guid = guids[gi]; var path = AssetDatabase.GUIDToAssetPath(guid); var sp = AssetDatabase.LoadAssetAtPath(path); if (sp == null) continue; bool belongs = false; try { var got = atlas.GetSprite(sp.name); if (got != null) belongs = true; } catch { try { var m = typeof(SpriteAtlas).GetMethod("GetSprite", new Type[] { typeof(string) }); if (m != null) { var got2 = m.Invoke(atlas, new object[] { sp.name }) as Sprite; if (got2 != null) belongs = true; } } catch { } } if (belongs) { GlyphEntry e = new GlyphEntry(); e.Sprite = sp; e.action = null; tableObj.entries.Add(e); added++; } } } catch (Exception ex) { Debug.LogError("[InputGlyphDatabase] Exception while scanning sprites for atlas: " + ex); } // 按名字逐字符排序(不区分大小写) tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name)); EditorUtility.SetDirty(db); serializedObject.Update(); serializedObject.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); Debug.Log($"[InputGlyphDatabase] Parsed SpriteAtlas '{atlas.name}' into table '{deviceName}'. foundSprites={added}"); } // ----- Parse Sprite Sheet (Texture2D with Multiple) ----- void ParseSpriteSheetIntoTableSerialized(SerializedProperty tableProp) { if (tableProp == null) return; var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); var tex = sheetProp != null ? sheetProp.objectReferenceValue as Texture2D : null; if (tex == null) { Debug.LogWarning("[InputGlyphDatabase] spriteSheetTexture is null for table."); return; } var nameProp = tableProp.FindPropertyRelative("deviceName"); string deviceName = nameProp != null ? nameProp.stringValue : ""; int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); if (tableIndex < 0) { Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); return; } var tableObj = db.tables[tableIndex]; tableObj.entries.Clear(); string path = AssetDatabase.GetAssetPath(tex); if (string.IsNullOrEmpty(path)) { Debug.LogWarning("[InputGlyphDatabase] Could not get asset path for texture."); return; } var assets = AssetDatabase.LoadAllAssetsAtPath(path); if (assets == null || assets.Length == 0) { Debug.LogWarning("[InputGlyphDatabase] No sub-assets found at path: " + path); return; } List sprites = new List(); foreach (var a in assets) { if (a is Sprite sp) { sprites.Add(sp); } } // 之前按视觉位置排序,改为先按名字逐字符排序(不区分大小写) sprites.Sort((a, b) => CompareSpriteNames(a?.name, b?.name)); foreach (var sp in sprites) { GlyphEntry e = new GlyphEntry(); e.Sprite = sp; e.action = null; tableObj.entries.Add(e); } // 额外在 runtime list 里也用相同排序(上面已经排序过 sprites) tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name)); EditorUtility.SetDirty(db); serializedObject.Update(); serializedObject.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); Debug.Log($"[InputGlyphDatabase] Parsed sprite sheet '{tex.name}' into table '{deviceName}'. foundSprites={sprites.Count}"); } int MapSerializedTableToRuntimeIndex(string deviceName) { if (db == null || db.tables == null) return -1; for (int ti = 0; ti < db.tables.Count; ++ti) { if (string.Equals(db.tables[ti].deviceName, deviceName, StringComparison.OrdinalIgnoreCase)) return ti; } return -1; } SpriteAtlas GetSpriteAtlasFromTMP(TMP_SpriteAsset asset) { if (asset == null) return null; var t = asset.GetType(); foreach (var f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (typeof(SpriteAtlas).IsAssignableFrom(f.FieldType)) { var val = f.GetValue(asset) as SpriteAtlas; if (val != null) return val; } } foreach (var p in t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (typeof(SpriteAtlas).IsAssignableFrom(p.PropertyType)) { try { var val = p.GetValue(asset, null) as SpriteAtlas; if (val != null) return val; } catch { } } } return null; } int CompareSpriteNames(string a, string b) { // normalize null/empty bool aEmpty = string.IsNullOrEmpty(a); bool bEmpty = string.IsNullOrEmpty(b); if (aEmpty && bEmpty) return 0; if (aEmpty) return -1; if (bEmpty) return 1; int la = a.Length; int lb = b.Length; int n = Math.Min(la, lb); for (int i = 0; i < n; ++i) { char ca = char.ToUpperInvariant(a[i]); char cb = char.ToUpperInvariant(b[i]); if (ca != cb) return ca - cb; } return la - lb; } }