using System.Collections.Generic; using UnityEngine; using UnityEditor; #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else using UnityEditor.Experimental.SceneManagement; #endif namespace UnityTcp.Editor.Helpers { /// /// Central compatibility surface for Unity's instance-id → EntityId /// migration (Unity 6000.5 / Unity 6.5). /// /// Unity 6.5 deprecated every -based identity API with an /// error-level obsolete attribute (CS0619) in favour of 64-bit EntityId /// overloads that do not exist on earlier versions: /// Object.GetInstanceID() -> Object.GetEntityId() /// Selection.activeInstanceID -> Selection.activeEntityId /// EditorUtility.InstanceIDToObject(int) -> EditorUtility.EntityIdToObject(EntityId) /// AssetDatabase.GetAssetPath(int) -> AssetDatabase.GetAssetPath(Object) /// /// These helpers speak so the full 64-bit EntityId value is /// carried losslessly and can be reconstructed exactly (no 32-bit truncation). /// On pre-6.5 Unity the legacy int instance id widens implicitly to /// long, so call sites see a single consistent type across versions. The /// only version-specific code lives in the two conversion helpers at the bottom. /// public static class InstanceIdExtensions { // ---- producers: Object / Selection -> long id ------------------------- /// /// Returns the object's stable instance id as a , or 0 if /// the object is null/destroyed. Prefer this over GetInstanceID(). /// public static long GetStableInstanceId(this Object obj) { if (obj == null) return 0; #if UNITY_6000_5_OR_NEWER return ToLong(obj.GetEntityId()); #else #pragma warning disable CS0618 return obj.GetInstanceID(); #pragma warning restore CS0618 #endif } /// Active selection instance id, or 0 if nothing is selected. public static long ActiveSelectionInstanceId() { #if UNITY_6000_5_OR_NEWER return ToLong(Selection.activeEntityId); #else #pragma warning disable CS0618 return Selection.activeInstanceID; #pragma warning restore CS0618 #endif } // ---- consumers: long id -> Object / path ------------------------------ /// Resolves an instance id to its , or null. public static Object InstanceIdToObject(long instanceId) { #if UNITY_6000_5_OR_NEWER return EditorUtility.EntityIdToObject(ToEntityId(instanceId)); #else #pragma warning disable CS0618 return EditorUtility.InstanceIDToObject((int)instanceId); #pragma warning restore CS0618 #endif } /// /// Resolves an instance id to its , or null. /// Same lookup as ; kept for existing call sites. /// public static Object ObjectFromInstanceId(long instanceId) => InstanceIdToObject(instanceId); /// /// Persistent YAML fileID. Uses /// (ulong) so Tuanjie does not truncate values above Int32 /// (e.g. 60588083669363838 must not become -2068059010). /// Returns 0 if the object is not yet written to disk. /// public static long GetLocalFileId(this Object obj) { if (obj == null) return 0; var gid = GlobalObjectId.GetGlobalObjectIdSlow(obj); return unchecked((long)gid.targetObjectId); } /// /// Decimal string of , or null when unknown. /// Wire this rather than a JSON number: values above 2^53 are not safe /// in JavaScript. /// public static string FormatLocalFileId(this Object obj) { if (obj == null) return null; ulong id = GlobalObjectId.GetGlobalObjectIdSlow(obj).targetObjectId; return id == 0 ? null : id.ToString(); } /// /// Decimal string of , or null when 0. /// Wire this rather than a JSON number: Unity 6.5 EntityId values /// can exceed JavaScript's 2^53-safe integer range. /// public static string FormatStableInstanceId(this Object obj) { if (obj == null) return null; long id = obj.GetStableInstanceId(); return id == 0 ? null : id.ToString(); } /// /// Selection kind for CLI routing: /// staged / sceneInstance / subAsset / projectAsset. /// public static string GetKind(this Object obj) { if (obj == null) return null; if (obj is GameObject go) { if (PrefabStageUtility.GetPrefabStage(go) != null) return "staged"; if (go.scene.IsValid() && !string.IsNullOrEmpty(go.scene.path)) return "sceneInstance"; } if (AssetDatabase.IsSubAsset(obj)) return "subAsset"; if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(obj))) return "projectAsset"; return null; } /// /// Prefab asset path for a stage. Unity 2019 exposes /// prefabAssetPath; 2020.1+ renamed it to assetPath. /// public static string GetPrefabStageAssetPath(PrefabStage stage) { if (stage == null) return null; #if UNITY_2020_1_OR_NEWER return stage.assetPath; #else return stage.prefabAssetPath; #endif } /// /// GUID from so staged prefab /// objects still resolve to the source prefab, not an empty asset path. /// public static string FormatGlobalObjectGuid(this Object obj) { if (obj == null) return null; string guid = GlobalObjectId.GetGlobalObjectIdSlow(obj).assetGUID.ToString(); if (!IsEmptyGuid(guid)) return guid; // Staged / scene objects sometimes yield an all-zero assetGUID. // Fall back to the prefab or scene asset path. string path = null; if (obj is GameObject go) { var stage = PrefabStageUtility.GetPrefabStage(go); string stagePath = GetPrefabStageAssetPath(stage); if (!string.IsNullOrEmpty(stagePath)) path = stagePath; else if (go.scene.IsValid() && !string.IsNullOrEmpty(go.scene.path)) path = go.scene.path; } if (string.IsNullOrEmpty(path)) path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path)) return null; guid = AssetDatabase.AssetPathToGUID(path); return IsEmptyGuid(guid) ? null : guid; } private static bool IsEmptyGuid(string guid) { return string.IsNullOrEmpty(guid) || guid == "00000000000000000000000000000000"; } /// /// Hierarchy path with same-name ordinal only when a parent has more than /// one child of the same name (e.g. Cube, Sphere, Cube → second Cube is Cube[1]). /// The index is among same-name siblings, not Transform.GetSiblingIndex(). /// public static string GetHierarchyPathWithIndex(this GameObject go) { if (go == null) return ""; var segments = new List(); var t = go.transform; while (t != null) { var parent = t.parent; string seg = t.name; if (parent != null && HasSameNameSibling(parent, t)) seg = $"{t.name}[{GetSameNameOrdinal(parent, t)}]"; segments.Insert(0, seg); t = parent; } return string.Join("/", segments); } /// /// 0-based index of among siblings that share its name. /// public static int GetSameNameOrdinal(Transform parent, Transform self) { int ordinal = 0; for (int i = 0; i < parent.childCount; i++) { var child = parent.GetChild(i); if (child.name != self.name) continue; if (child == self) return ordinal; ordinal++; } return ordinal; } /// /// Parses a path segment produced by . /// is -1 when the name is unique (no suffix). /// public static bool TryParseHierarchySegment(string segment, out string name, out int ordinal) { name = segment ?? ""; ordinal = -1; if (string.IsNullOrEmpty(segment)) return false; int close = segment.LastIndexOf(']'); int open = segment.LastIndexOf('['); if (open > 0 && close == segment.Length - 1 && close > open + 1) { int n; if (int.TryParse(segment.Substring(open + 1, close - open - 1), out n) && n >= 0) { name = segment.Substring(0, open); ordinal = n; } } return true; } public static string[] GetComponentTypeNames(this GameObject go) { if (go == null) return new string[0]; var comps = go.GetComponents(); var names = new List(comps.Length); for (int i = 0; i < comps.Length; i++) { if (comps[i] != null) names.Add(comps[i].GetType().FullName); } return names.ToArray(); } private static bool HasSameNameSibling(Transform parent, Transform self) { int count = 0; for (int i = 0; i < parent.childCount; i++) { if (parent.GetChild(i).name != self.name) continue; count++; if (count > 1) return true; } return false; } /// Asset GUID from path, or null if the object is not an asset. public static string GetAssetGuid(this Object obj) { if (obj == null) return null; string guid = obj.FormatGlobalObjectGuid(); if (!string.IsNullOrEmpty(guid)) return guid; string path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path)) return null; guid = AssetDatabase.AssetPathToGUID(path); return string.IsNullOrEmpty(guid) ? null : guid; } /// Asset path for an instance id, or "" if it is not an asset. public static string AssetPathFromInstanceId(long instanceId) { var obj = InstanceIdToObject(instanceId); return obj != null ? AssetDatabase.GetAssetPath(obj) : ""; } #if UNITY_6000_5_OR_NEWER // ---- the ONLY version-specific conversions ---------------------------- // Both are pure 64-bit bit reinterpretations of EntityId.ToULong (the // canonical numeric form of an EntityId, which this project already uses), // so ToEntityId is an exact inverse of ToLong with no loss of information. /// Reinterprets an EntityId as a 64-bit . internal static long ToLong(EntityId entityId) => unchecked((long)EntityId.ToULong(entityId)); /// /// Exact inverse of : rebuilds the EntityId from /// its 64-bit numeric form via EntityId.FromULong (the counterpart of /// EntityId.ToULong), so an id survives a round-trip through storage or /// the wire without loss. /// internal static EntityId ToEntityId(long instanceId) => EntityId.FromULong(unchecked((ulong)instanceId)); #endif } }