diff --git a/UnityCtl.Cli/ScriptCommands.cs b/UnityCtl.Cli/ScriptCommands.cs index 8c86184..3a79646 100644 --- a/UnityCtl.Cli/ScriptCommands.cs +++ b/UnityCtl.Cli/ScriptCommands.cs @@ -613,15 +613,15 @@ internal static string ExtractLeadingUsingDirectives(string expression, List(); + var ids = new List(); foreach (var part in parts) { var trimmed = part.Trim(); if (trimmed.Length == 0) continue; - if (!int.TryParse(trimmed, out var id)) + if (!long.TryParse(trimmed, out var id)) throw new ArgumentException($"Invalid instance ID: '{trimmed}' — must be an integer"); ids.Add(id); } @@ -630,14 +630,14 @@ internal static int[] ParseInstanceIds(string idArg) return ids.ToArray(); } - internal static string BuildInstanceIdPreamble(int[] ids) + internal static string BuildInstanceIdPreamble(long[] ids) { const string pad = "\n "; var sb = new System.Text.StringBuilder(); if (ids.Length == 1) { - sb.Append($"var target = (GameObject)UnityEditor.EditorUtility.InstanceIDToObject({ids[0]});"); + sb.Append($"var target = (GameObject)UnityCtl.UnityCtlClient.ResolveObjectById({ids[0]});"); sb.Append($"{pad}if (target == null) throw new System.Exception(\"Object {ids[0]} not found (destroyed?)\");"); } else @@ -645,7 +645,7 @@ internal static string BuildInstanceIdPreamble(int[] ids) sb.Append($"var targets = new GameObject[{ids.Length}];"); for (int i = 0; i < ids.Length; i++) { - sb.Append($"{pad}targets[{i}] = (GameObject)UnityEditor.EditorUtility.InstanceIDToObject({ids[i]});"); + sb.Append($"{pad}targets[{i}] = (GameObject)UnityCtl.UnityCtlClient.ResolveObjectById({ids[i]});"); sb.Append($"{pad}if (targets[{i}] == null) throw new System.Exception(\"Object {ids[i]} not found (destroyed?)\");"); } } diff --git a/UnityCtl.Cli/SnapshotCommand.cs b/UnityCtl.Cli/SnapshotCommand.cs index 58fa304..c2d512a 100644 --- a/UnityCtl.Cli/SnapshotCommand.cs +++ b/UnityCtl.Cli/SnapshotCommand.cs @@ -16,7 +16,7 @@ public static Command CreateCommand() { var snapshotCommand = new Command("snapshot", "Snapshot the scene hierarchy as a compact, LLM-friendly tree with instance IDs"); - var idOption = new Option( + var idOption = new Option( "--id", "Drill into a specific object by instance ID" ); diff --git a/UnityCtl.Cli/UICommands.cs b/UnityCtl.Cli/UICommands.cs index b2de53a..f952a9d 100644 --- a/UnityCtl.Cli/UICommands.cs +++ b/UnityCtl.Cli/UICommands.cs @@ -20,7 +20,7 @@ private static Command CreateClickCommand() { var clickCommand = new Command("click", "Click a UI element by instance ID, name, or screen coordinates"); - var idOption = new Option("--id", "Instance ID of the UI element to click"); + var idOption = new Option("--id", "Instance ID of the UI element to click"); var nameOption = new Option("--name", "Find and click a GameObject by name (uses GameObject.Find)"); var xArg = new Argument("x", () => null, "Screen X coordinate"); var yArg = new Argument("y", () => null, "Screen Y coordinate"); diff --git a/UnityCtl.Protocol/DTOs.cs b/UnityCtl.Protocol/DTOs.cs index e7acf2e..2d97c21 100644 --- a/UnityCtl.Protocol/DTOs.cs +++ b/UnityCtl.Protocol/DTOs.cs @@ -473,7 +473,7 @@ public class SnapshotResult public bool? HasUnsavedChanges { get; init; } [JsonProperty("openedFromInstanceId", NullValueHandling = NullValueHandling.Ignore)] - public int? OpenedFromInstanceId { get; init; } + public long? OpenedFromInstanceId { get; init; } [JsonProperty("isPlaying")] public required bool IsPlaying { get; init; } @@ -521,7 +521,7 @@ public class SnapshotSceneInfo public class SnapshotObject { [JsonProperty("instanceId")] - public int InstanceId { get; set; } + public long InstanceId { get; set; } [JsonProperty("name")] public string Name { get; set; } = ""; @@ -583,7 +583,7 @@ public class SnapshotObject public bool? Hittable { get; set; } [JsonProperty("blockedBy", NullValueHandling = NullValueHandling.Ignore)] - public int? BlockedBy { get; set; } + public long? BlockedBy { get; set; } [JsonProperty("childCount")] public int ChildCount { get; set; } @@ -646,7 +646,7 @@ public class SnapshotQueryResult public class SnapshotQueryHit { [JsonProperty("instanceId")] - public int InstanceId { get; set; } + public long InstanceId { get; set; } [JsonProperty("name")] public string Name { get; set; } = ""; @@ -664,7 +664,7 @@ public class SnapshotQueryHit public class UIClickResult { [JsonProperty("instanceId")] - public int InstanceId { get; set; } + public long InstanceId { get; set; } [JsonProperty("name")] public string Name { get; set; } = ""; diff --git a/UnityCtl.Tests/Unit/Cli/EvalCodeGeneratorTests.cs b/UnityCtl.Tests/Unit/Cli/EvalCodeGeneratorTests.cs index 277a431..26e36ad 100644 --- a/UnityCtl.Tests/Unit/Cli/EvalCodeGeneratorTests.cs +++ b/UnityCtl.Tests/Unit/Cli/EvalCodeGeneratorTests.cs @@ -119,7 +119,7 @@ public void InstanceId_SingleId_InjectsTarget() { var code = ScriptCommands.BuildEvalCode("target.name", [], hasArgs: false, instanceIds: "14200"); - Assert.Contains("InstanceIDToObject(14200)", code); + Assert.Contains("ResolveObjectById(14200)", code); Assert.Contains("var target =", code); } @@ -129,8 +129,8 @@ public void InstanceId_MultipleIds_InjectsTargetsArray() var code = ScriptCommands.BuildEvalCode("targets[0].name", [], hasArgs: false, instanceIds: "14200,14210"); Assert.Contains("var targets = new GameObject[2]", code); - Assert.Contains("InstanceIDToObject(14200)", code); - Assert.Contains("InstanceIDToObject(14210)", code); + Assert.Contains("ResolveObjectById(14200)", code); + Assert.Contains("ResolveObjectById(14210)", code); } [Fact] @@ -138,7 +138,16 @@ public void InstanceId_NegativeId_Accepted() { var code = ScriptCommands.BuildEvalCode("target.name", [], hasArgs: false, instanceIds: "-1290"); - Assert.Contains("InstanceIDToObject(-1290)", code); + Assert.Contains("ResolveObjectById(-1290)", code); + } + + [Fact] + public void InstanceId_BeyondInt32_Accepted() + { + // Unity 6000.5 EntityIds are 64-bit; ids beyond int range must round-trip. + var code = ScriptCommands.BuildEvalCode("target.name", [], hasArgs: false, instanceIds: "5000000000"); + + Assert.Contains("ResolveObjectById(5000000000)", code); } [Fact] @@ -157,6 +166,14 @@ public void ParseInstanceIds_ValidInput_ReturnsInts() Assert.Equal([14200, -1290, 0], ids); } + [Fact] + public void ParseInstanceIds_BeyondInt32_ParsesAsLong() + { + var ids = ScriptCommands.ParseInstanceIds("5000000000,-5000000000"); + + Assert.Equal([5000000000L, -5000000000L], ids); + } + [Fact] public void ExtraUsings_CommaSeparated_SplitIntoMultiple() { diff --git a/UnityCtl.UnityPackage/Editor/UnityCtlClient.cs b/UnityCtl.UnityPackage/Editor/UnityCtlClient.cs index 5296828..43ff339 100644 --- a/UnityCtl.UnityPackage/Editor/UnityCtlClient.cs +++ b/UnityCtl.UnityPackage/Editor/UnityCtlClient.cs @@ -811,6 +811,36 @@ private static string GetStringArgument(RequestMessage request, string key) return null; } + private static long? GetLongArgument(RequestMessage request, string key) + { + if (request.Args == null) return null; + + try + { + if (request.Args is System.Collections.IDictionary dict && dict.Contains(key)) + { + var value = dict[key]; + if (value == null) return null; + + if (value is long longValue) return longValue; + if (value is int intValue) return intValue; + + if (value is JToken jtoken && jtoken.Type == JTokenType.Integer) + return jtoken.Value(); + + if (long.TryParse(value.ToString(), out var parsed)) return parsed; + + DebugLog($"[UnityCtl] Could not parse argument '{key}' as long: {value}"); + } + } + catch (Exception ex) + { + DebugLogError($"[UnityCtl] Failed to get long argument '{key}': {ex.Message}"); + } + + return null; + } + private static double? GetDoubleArgument(RequestMessage request, string key) { if (request.Args == null) return null; @@ -1683,7 +1713,7 @@ private object HandleScriptMembers(RequestMessage request) private object HandleSnapshot(RequestMessage request) { var depth = GetIntArgument(request, "depth") ?? 2; - var targetId = GetIntArgument(request, "id"); + var targetId = GetLongArgument(request, "id"); var includeComponents = GetBoolArgument(request, "components"); var screen = GetBoolArgument(request, "screen"); var filter = GetStringArgument(request, "filter"); @@ -1714,7 +1744,7 @@ private object HandleSnapshot(RequestMessage request) return result; } - private Protocol.SnapshotResult SnapshotPrefabAsset(string prefabPath, int? targetId, string filter, int depth, bool includeComponents, bool screen) + private Protocol.SnapshotResult SnapshotPrefabAsset(string prefabPath, long? targetId, string filter, int depth, bool includeComponents, bool screen) { if (!prefabPath.EndsWith(".prefab")) throw new ArgumentException($"Not a prefab asset: {prefabPath} (must end with .prefab)"); @@ -1755,7 +1785,7 @@ private Protocol.SnapshotResult SnapshotPrefabAsset(string prefabPath, int? targ }; } - private Protocol.SnapshotResult SnapshotSpecificScene(string scenePath, int? targetId, string filter, int depth, bool includeComponents, bool screen) + private Protocol.SnapshotResult SnapshotSpecificScene(string scenePath, long? targetId, string filter, int depth, bool includeComponents, bool screen) { if (EditorApplication.isPlaying) throw new InvalidOperationException("Cannot snapshot other scenes during play mode. Use snapshot without --scene for the active scene."); @@ -1772,7 +1802,7 @@ private Protocol.SnapshotResult SnapshotSpecificScene(string scenePath, int? tar { if (targetId.HasValue) { - var go = EditorUtility.InstanceIDToObject(targetId.Value) as GameObject; + var go = ResolveObjectById(targetId.Value) as GameObject; if (go == null || go.scene != scene) throw new ArgumentException($"No GameObject with instance ID {targetId} in scene {scenePath}"); @@ -1835,9 +1865,9 @@ private Protocol.SnapshotResult SnapshotSpecificScene(string scenePath, int? tar } } - private Protocol.SnapshotResult SnapshotDrillDown(int targetId, string filter, int depth, bool includeComponents, bool screen) + private Protocol.SnapshotResult SnapshotDrillDown(long targetId, string filter, int depth, bool includeComponents, bool screen) { - var go = EditorUtility.InstanceIDToObject(targetId) as GameObject; + var go = ResolveObjectById(targetId) as GameObject; if (go == null) throw new ArgumentException($"No GameObject with instance ID {targetId}"); @@ -1966,9 +1996,43 @@ private Protocol.SnapshotResult SnapshotCurrentStage(string filter, int depth, b }; } - private static GameObject FindInHierarchy(GameObject root, int instanceId) + // --- Unity EntityId compatibility ------------------------------------- + // Unity introduced the EntityId type in 6000.3 and deprecated (warning) + // Object.GetInstanceID() and EditorUtility.InstanceIDToObject(int) there; + // 6000.5 promotes those to obsolete-as-error and widens EntityId to 64-bit + // (EntityId.ToULong/FromULong). We adopt the new APIs from 6000.3 to stay + // warning-free, with three regimes: + // * 6000.5+ : EntityId is 64-bit; round-trip losslessly via ToULong/FromULong. + // * 6000.3/.4 : EntityId is int-backed and converts implicitly to/from int. + // * pre-6000.3: legacy GetInstanceID()/InstanceIDToObject(int). + // Handles are surfaced as 64-bit (long) through the protocol so a 6000.5 id + // is never truncated; narrower ids simply widen to long and narrow back. + + internal static long GetObjectInstanceId(UnityEngine.Object obj) + { +#if UNITY_6000_5_OR_NEWER + return unchecked((long)UnityEngine.EntityId.ToULong(obj.GetEntityId())); +#elif UNITY_6000_3_OR_NEWER + return (int)obj.GetEntityId(); +#else + return obj.GetInstanceID(); +#endif + } + + public static UnityEngine.Object ResolveObjectById(long id) + { +#if UNITY_6000_5_OR_NEWER + return EditorUtility.EntityIdToObject(UnityEngine.EntityId.FromULong(unchecked((ulong)id))); +#elif UNITY_6000_3_OR_NEWER + return EditorUtility.EntityIdToObject((UnityEngine.EntityId)unchecked((int)id)); +#else + return EditorUtility.InstanceIDToObject(unchecked((int)id)); +#endif + } + + private static GameObject FindInHierarchy(GameObject root, long instanceId) { - if (root.GetInstanceID() == instanceId) return root; + if (GetObjectInstanceId(root) == instanceId) return root; foreach (Transform child in root.transform) { var found = FindInHierarchy(child.gameObject, instanceId); @@ -1982,7 +2046,7 @@ private struct StageInfo public string Stage; public string PrefabAssetPath; public bool? HasUnsavedChanges; - public int? OpenedFromInstanceId; + public long? OpenedFromInstanceId; } private StageInfo GetStageInfo() @@ -1991,12 +2055,12 @@ private StageInfo GetStageInfo() if (prefabStage != null) { var isInContext = prefabStage.mode == PrefabStage.Mode.InContext; - int? openedFrom = null; + long? openedFrom = null; if (isInContext) { var instanceRoot = prefabStage.openedFromInstanceRoot; if (instanceRoot != null) - openedFrom = instanceRoot.GetInstanceID(); + openedFrom = GetObjectInstanceId(instanceRoot); } return new StageInfo @@ -2032,11 +2096,11 @@ private object HandlePrefabOpen(RequestMessage request) if (asset == null) throw new ArgumentException($"Prefab not found: {path}"); - var contextInstanceId = GetIntArgument(request, "context"); + var contextInstanceId = GetLongArgument(request, "context"); if (contextInstanceId.HasValue) { - var instance = EditorUtility.InstanceIDToObject(contextInstanceId.Value) as GameObject; + var instance = ResolveObjectById(contextInstanceId.Value) as GameObject; if (instance == null) throw new ArgumentException($"Context instance not found: {contextInstanceId.Value}"); if (!PrefabUtility.IsPartOfPrefabInstance(instance)) @@ -2103,7 +2167,7 @@ private static Protocol.SnapshotObject SerializeGameObject( var t = go.transform; var obj = new Protocol.SnapshotObject { - InstanceId = go.GetInstanceID(), + InstanceId = GetObjectInstanceId(go), Name = go.name, Active = go.activeSelf, Tag = go.tag != "Untagged" ? go.tag : null, @@ -2489,7 +2553,7 @@ private static void ComputeScreenSpaceInfo(GameObject go, Protocol.SnapshotObjec var centerX = (minX + maxX) / 2f; var centerY = (minY + maxY) / 2f; var hitId = GetUIHitAtPoint(new Vector2(centerX, centerY)); - if (hitId == go.GetInstanceID() || IsDescendantOf(hitId, go)) + if (hitId == GetObjectInstanceId(go) || IsDescendantOf(hitId, go)) { obj.Hittable = true; } @@ -2509,14 +2573,14 @@ private static void ComputeScreenSpaceInfo(GameObject go, Protocol.SnapshotObjec /// Returns the instance ID of the top UI element at the given screen point, or 0 if nothing. /// Requires play mode with an active EventSystem. /// - private static int GetUIHitAtPoint(Vector2 screenPoint) + private static long GetUIHitAtPoint(Vector2 screenPoint) { if (EventSystem.current == null) return 0; var pointerData = new PointerEventData(EventSystem.current) { position = screenPoint }; var results = new List(); EventSystem.current.RaycastAll(pointerData, results); - return results.Count > 0 ? results[0].gameObject.GetInstanceID() : 0; + return results.Count > 0 ? GetObjectInstanceId(results[0].gameObject) : 0; } /// @@ -2562,7 +2626,7 @@ private object HandleSnapshotQuery(RequestMessage request) { uiHits.Add(new Protocol.SnapshotQueryHit { - InstanceId = r.gameObject.GetInstanceID(), + InstanceId = GetObjectInstanceId(r.gameObject), Name = r.gameObject.name, Path = GetHierarchyPath(r.gameObject), Text = GetUIText(r.gameObject), @@ -2602,7 +2666,7 @@ private object HandleSnapshotQuery(RequestMessage request) { uiHits.Add(new Protocol.SnapshotQueryHit { - InstanceId = g.gameObject.GetInstanceID(), + InstanceId = GetObjectInstanceId(g.gameObject), Name = g.gameObject.name, Path = GetHierarchyPath(g.gameObject), Text = GetUIText(g.gameObject), @@ -2629,7 +2693,7 @@ private object HandleUIClick(RequestMessage request) if (EventSystem.current == null) throw new InvalidOperationException("No EventSystem found in scene"); - var targetId = GetIntArgument(request, "id"); + var targetId = GetLongArgument(request, "id"); var targetName = GetStringArgument(request, "name"); var x = GetIntArgument(request, "x"); var y = GetIntArgument(request, "y"); @@ -2640,7 +2704,7 @@ private object HandleUIClick(RequestMessage request) if (targetId.HasValue) { // Resolve by instance ID - target = EditorUtility.InstanceIDToObject(targetId.Value) as GameObject; + target = ResolveObjectById(targetId.Value) as GameObject; if (target == null) throw new ArgumentException($"No GameObject with instance ID {targetId.Value}"); @@ -2662,7 +2726,7 @@ private object HandleUIClick(RequestMessage request) if (topHit != target && !topHit.transform.IsChildOf(target.transform)) { throw new InvalidOperationException( - $"'{target.name}' [i:{targetId.Value}] is blocked by '{topHit.name}' [i:{topHit.GetInstanceID()}]"); + $"'{target.name}' [i:{targetId.Value}] is blocked by '{topHit.name}' [i:{GetObjectInstanceId(topHit)}]"); } } } @@ -2690,7 +2754,7 @@ private object HandleUIClick(RequestMessage request) if (topHit != target && !topHit.transform.IsChildOf(target.transform)) { throw new InvalidOperationException( - $"'{target.name}' [i:{target.GetInstanceID()}] is blocked by '{topHit.name}' [i:{topHit.GetInstanceID()}]"); + $"'{target.name}' [i:{GetObjectInstanceId(target)}] is blocked by '{topHit.name}' [i:{GetObjectInstanceId(topHit)}]"); } } } @@ -2739,7 +2803,7 @@ private object HandleUIClick(RequestMessage request) return new Protocol.UIClickResult { - InstanceId = target.GetInstanceID(), + InstanceId = GetObjectInstanceId(target), Name = target.name, Path = GetHierarchyPath(target), ScreenPosition = string.Format(System.Globalization.CultureInfo.InvariantCulture, @@ -2762,9 +2826,9 @@ private static Vector2 GetRectScreenCenter(RectTransform rt) /// /// Returns true if the object with the given instance ID is a descendant of parent. /// - private static bool IsDescendantOf(int instanceId, GameObject parent) + private static bool IsDescendantOf(long instanceId, GameObject parent) { - var obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + var obj = ResolveObjectById(instanceId) as GameObject; if (obj == null) return false; var t = obj.transform; while (t != null) diff --git a/UnityCtl.UnityPackage/Plugins/UnityCtl.Protocol.dll b/UnityCtl.UnityPackage/Plugins/UnityCtl.Protocol.dll index 3611119..f378f1b 100644 Binary files a/UnityCtl.UnityPackage/Plugins/UnityCtl.Protocol.dll and b/UnityCtl.UnityPackage/Plugins/UnityCtl.Protocol.dll differ