diff --git a/cli/src/editor_runtime.rs b/cli/src/editor_runtime.rs index 2893e1f..0cf4fd9 100644 --- a/cli/src/editor_runtime.rs +++ b/cli/src/editor_runtime.rs @@ -470,6 +470,30 @@ fn unity_install_roots() -> Vec { } } + #[cfg(unix)] + { + if let Some(home) = std::env::var_os("HOME") { + let home = PathBuf::from(home); + roots.push(home.join("Unity").join("Hub").join("Editor")); + roots.push(home.join(".local").join("share").join("Unity").join("Hub").join("Editor")); + roots.push(home.join(".unityhub").join("Editor")); + } + roots.push(PathBuf::from("/opt/Unity").join("Hub").join("Editor")); + roots.push(PathBuf::from("/usr/local/Unity").join("Hub").join("Editor")); + + if let Ok(config_dir) = std::env::var("XDG_CONFIG_HOME") { + let hub_dir = PathBuf::from(config_dir).join("unityhub"); + if let Some(path) = read_hub_path_string(&hub_dir.join("secondaryInstallPath.json")) { + roots.push(path); + } + } else if let Some(home) = std::env::var_os("HOME") { + let hub_dir = PathBuf::from(home).join(".config").join("unityhub"); + if let Some(path) = read_hub_path_string(&hub_dir.join("secondaryInstallPath.json")) { + roots.push(path); + } + } + } + dedupe_paths(roots) } @@ -484,21 +508,52 @@ fn read_hub_path_string(path: &Path) -> Option { Some(PathBuf::from(trimmed)) } +fn hub_projects_path() -> Option { + #[cfg(windows)] + { + let app_data = std::env::var_os("APPDATA")?; + Some(PathBuf::from(app_data).join("UnityHub").join("projects-v1.json")) + } + + #[cfg(unix)] + { + let config_dir = std::env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?; + Some(config_dir.join("unityhub").join("projects-v1.json")) + } +} + fn read_hub_project_version(project: &Path) -> Option { - let app_data = std::env::var_os("APPDATA")?; - let path = PathBuf::from(app_data) - .join("UnityHub") - .join("projects-v1.json"); + let path = hub_projects_path()?; let content = fs::read_to_string(path).ok()?; let json_start = content.find('{')?; let value: serde_json::Value = serde_json::from_str(&content[json_start..]).ok()?; - let project_key = project.display().to_string().replace('/', "\\"); - value - .get("data")? - .get(&project_key)? - .get("version")? - .as_str() - .map(ToOwned::to_owned) + let project_key = project.display().to_string(); + + // Try exact match first, then with normalized separators + let data = value.get("data")?; + if let Some(version) = data + .get(&project_key) + .and_then(|entry| entry.get("version")) + .and_then(|v| v.as_str()) + { + return Some(version.to_owned()); + } + + #[cfg(windows)] + { + let alt_key = project_key.replace('/', "\\"); + data.get(&alt_key) + .and_then(|entry| entry.get("version")) + .and_then(|v| v.as_str()) + .map(ToOwned::to_owned) + } + + #[cfg(unix)] + { + None + } } fn installed_unity_versions(roots: &[PathBuf]) -> Vec { diff --git a/unity-package/com.ucp.bridge/Editor/AssemblyInfo.cs b/unity-package/com.ucp.bridge/Editor/AssemblyInfo.cs new file mode 100644 index 0000000..610cddd --- /dev/null +++ b/unity-package/com.ucp.bridge/Editor/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UCP.Bridge.Editor.Tests")] diff --git a/unity-package/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs b/unity-package/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs index b42b64b..a544a54 100644 --- a/unity-package/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +++ b/unity-package/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs @@ -1,13 +1,36 @@ using UnityEditor; using UnityEngine; +using UnityEngine.SceneManagement; namespace UCP.Bridge { internal static class UnityObjectCompat { + public static int GetId(this Object obj) + { +#if UNITY_6000_5_OR_NEWER + return unchecked((int)EntityId.ToULong(obj.GetEntityId())); +#else + return obj.GetInstanceID(); +#endif + } + + public static long GetSceneHandle(Scene scene) + { +#if UNITY_6000_5_OR_NEWER + return unchecked((long)scene.handle.GetRawData()); +#else + return scene.handle; +#endif + } + public static Object ResolveByInstanceId(int instanceId) { +#if UNITY_6000_5_OR_NEWER + return EditorUtility.EntityIdToObject(EntityId.FromULong(unchecked((ulong)instanceId))); +#else return EditorUtility.InstanceIDToObject(instanceId); +#endif } public static T ResolveByInstanceId(int instanceId) where T : Object diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/AssetController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/AssetController.cs index 8e24b38..5a65499 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/AssetController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/AssetController.cs @@ -125,7 +125,7 @@ private static object HandleInfo(string paramsJson) ["name"] = asset.name, ["type"] = asset.GetType().Name, ["fullType"] = asset.GetType().FullName, - ["instanceId"] = asset.GetInstanceID(), + ["instanceId"] = asset.GetId(), ["guid"] = AssetDatabase.AssetPathToGUID(assetPath) }; @@ -496,7 +496,7 @@ private static object HandleCreateScriptableObject(string paramsJson) ["status"] = "ok", ["path"] = assetPath, ["type"] = soType.Name, - ["instanceId"] = instance.GetInstanceID() + ["instanceId"] = instance.GetId() }; } diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/HierarchyController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/HierarchyController.cs index 9750a20..9d710c6 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/HierarchyController.cs @@ -61,7 +61,7 @@ private static object HandleCreate(string paramsJson) return new Dictionary { ["status"] = "ok", - ["instanceId"] = go.GetInstanceID(), + ["instanceId"] = go.GetId(), ["name"] = go.name }; } @@ -192,7 +192,7 @@ private static object HandleInstantiate(string paramsJson) return new Dictionary { ["status"] = "ok", - ["instanceId"] = instance.GetInstanceID(), + ["instanceId"] = instance.GetId(), ["name"] = instance.name }; } @@ -314,7 +314,7 @@ private static GameObject FindGameObject(int instanceId) private static GameObject FindInHierarchy(GameObject go, int instanceId) { - if (go.GetInstanceID() == instanceId) return go; + if (go.GetId() == instanceId) return go; for (int i = 0; i < go.transform.childCount; i++) { var found = FindInHierarchy(go.transform.GetChild(i).gameObject, instanceId); diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/MaterialController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/MaterialController.cs index b75e350..792e1f8 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/MaterialController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/MaterialController.cs @@ -50,7 +50,7 @@ private static object HandleCreate(string paramsJson) ["path"] = path, ["name"] = material.name, ["shader"] = shader.name, - ["instanceId"] = material.GetInstanceID() + ["instanceId"] = material.GetId() }; } @@ -278,7 +278,7 @@ private static object ReadMaterialValue(Material mat, string propName, UnityEngi { ["name"] = tex.name, ["path"] = texPath, - ["instanceId"] = tex.GetInstanceID() + ["instanceId"] = tex.GetId() }; } return null; diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs b/unity-package/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs index f44cc30..283fb80 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs @@ -14,7 +14,7 @@ public static Dictionary Serialize(UnityEngine.Object obj) var result = new Dictionary { - ["instanceId"] = obj.GetInstanceID(), + ["instanceId"] = obj.GetId(), ["name"] = obj.name, ["type"] = obj.GetType().Name }; diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/PrefabController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/PrefabController.cs index c5559a7..b4ab053 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/PrefabController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/PrefabController.cs @@ -179,8 +179,8 @@ out success ["status"] = "ok", ["path"] = savePath, ["name"] = prefab.name, - ["instanceId"] = prefab.GetInstanceID(), - ["sceneInstanceId"] = go.GetInstanceID(), + ["instanceId"] = prefab.GetId(), + ["sceneInstanceId"] = go.GetId(), ["isPrefabInstance"] = PrefabUtility.IsPartOfPrefabInstance(go) }; } @@ -221,7 +221,7 @@ private static object HandleOverrides(string paramsJson) added.Add(new Dictionary { ["component"] = ac.instanceComponent.GetType().Name, - ["instanceId"] = ac.instanceComponent.GetInstanceID() + ["instanceId"] = ac.instanceComponent.GetId() }); } diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/PropertyController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/PropertyController.cs index 34b63a5..5cf93f6 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/PropertyController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/PropertyController.cs @@ -538,7 +538,7 @@ private static GameObject FindGameObject(int instanceId) private static GameObject FindInHierarchy(GameObject go, int instanceId) { - if (go.GetInstanceID() == instanceId) return go; + if (go.GetId() == instanceId) return go; for (int i = 0; i < go.transform.childCount; i++) { var found = FindInHierarchy(go.transform.GetChild(i).gameObject, instanceId); diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/ReferenceController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/ReferenceController.cs index 2dfba06..1c62cec 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/ReferenceController.cs @@ -31,7 +31,7 @@ private static object HandleSerializationStatus(string paramsJson) { { "serializationMode", mode }, { "forceText", mode == 2 }, - { "visibleMetaFiles", EditorSettings.externalVersionControl == "Visible Meta Files" } + { "visibleMetaFiles", VersionControlSettings.mode == "Visible Meta Files" } }; } finally diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs b/unity-package/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs index 73691f9..37ad51f 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs @@ -17,7 +17,7 @@ private sealed class TrackedSceneChange public HashSet Components = new(); } - private static readonly Dictionary> s_changesByScene = new(); + private static readonly Dictionary> s_changesByScene = new(); static SceneChangeTracker() { @@ -33,7 +33,7 @@ public static void RecordGameObjectChange(GameObject gameObject, string componen if (gameObject == null) return; - RecordSceneChange(gameObject.scene, gameObject.GetInstanceID(), gameObject.name, componentName); + RecordSceneChange(gameObject.scene, gameObject.GetId(), gameObject.name, componentName); } public static void RecordDeletedObject(Scene scene, int instanceId, string name, string componentName) @@ -56,7 +56,7 @@ public static Dictionary DescribeSceneChanges(Scene scene, int m var modifications = new List(); var omittedCount = 0; - if (scene.IsValid() && s_changesByScene.TryGetValue(scene.handle, out var trackedChanges)) + if (scene.IsValid() && s_changesByScene.TryGetValue(UnityObjectCompat.GetSceneHandle(scene), out var trackedChanges)) { var ordered = trackedChanges.Values .OrderBy(change => change.InstanceId.HasValue ? 0 : 1) @@ -101,7 +101,7 @@ public static void ClearScene(Scene scene) if (!scene.IsValid()) return; - s_changesByScene.Remove(scene.handle); + s_changesByScene.Remove(UnityObjectCompat.GetSceneHandle(scene)); } private static UndoPropertyModification[] OnPostprocessModifications(UndoPropertyModification[] modifications) @@ -140,10 +140,10 @@ private static void RecordSceneChange(Scene scene, int? instanceId, string name, if (!scene.IsValid() || !scene.isLoaded) return; - if (!s_changesByScene.TryGetValue(scene.handle, out var sceneChanges)) + if (!s_changesByScene.TryGetValue(UnityObjectCompat.GetSceneHandle(scene), out var sceneChanges)) { sceneChanges = new Dictionary(); - s_changesByScene[scene.handle] = sceneChanges; + s_changesByScene[UnityObjectCompat.GetSceneHandle(scene)] = sceneChanges; } var key = instanceId.HasValue ? instanceId.Value.ToString() : $"scene::{name}"; diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/SceneController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/SceneController.cs index 116c190..a73add2 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/SceneController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/SceneController.cs @@ -302,7 +302,7 @@ private static GameObject FindGameObject(int instanceId) private static GameObject FindInHierarchy(GameObject gameObject, int instanceId) { - if (gameObject.GetInstanceID() == instanceId) + if (gameObject.GetId() == instanceId) return gameObject; foreach (Transform child in gameObject.transform) diff --git a/unity-package/com.ucp.bridge/Editor/Controllers/SnapshotController.cs b/unity-package/com.ucp.bridge/Editor/Controllers/SnapshotController.cs index 5848ba7..f298378 100644 --- a/unity-package/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +++ b/unity-package/com.ucp.bridge/Editor/Controllers/SnapshotController.cs @@ -86,7 +86,7 @@ private static bool SerializeGameObject( var entry = new Dictionary { - ["instanceId"] = go.GetInstanceID(), + ["instanceId"] = go.GetId(), ["name"] = go.name, ["active"] = go.activeSelf, ["tag"] = go.tag, @@ -230,7 +230,7 @@ private static void QueryHierarchy( private static Dictionary ProjectGameObject(GameObject go, HashSet fields, int depth) { var entry = new Dictionary(); - AddField(entry, fields, "instanceId", go.GetInstanceID()); + AddField(entry, fields, "instanceId", go.GetId()); AddField(entry, fields, "name", go.name); AddField(entry, fields, "active", go.activeSelf); AddField(entry, fields, "activeInHierarchy", go.activeInHierarchy); @@ -426,7 +426,7 @@ private static GameObject FindByInstanceId(int id) private static GameObject FindInHierarchy(GameObject go, int instanceId) { - if (go.GetInstanceID() == instanceId) + if (go.GetId() == instanceId) return go; for (int i = 0; i < go.transform.childCount; i++) @@ -479,7 +479,7 @@ private static Dictionary CreateGameObjectEntry(GameObject go, i { return new Dictionary { - ["instanceId"] = go.GetInstanceID(), + ["instanceId"] = go.GetId(), ["name"] = go.name, ["active"] = go.activeSelf, ["tag"] = go.tag, diff --git a/unity-package/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs b/unity-package/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs index 99d5a8b..01df18c 100644 --- a/unity-package/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +++ b/unity-package/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs @@ -544,7 +544,7 @@ public void ObjectLifecycle_CreateMutateAndDelete_WorksEndToEnd() ); Assert.That(getPosition.error, Is.Null); - var updated = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + var updated = UnityObjectCompat.ResolveByInstanceId(instanceId) as GameObject; Assert.That(updated, Is.Not.Null); var localPosition = updated.transform.localPosition; Assert.That(localPosition.x, Is.EqualTo(1f).Within(0.001f)); @@ -556,7 +556,7 @@ public void ObjectLifecycle_CreateMutateAndDelete_WorksEndToEnd() var delete = _router.Dispatch("object/delete", 1, "{\"instanceId\":" + instanceId + "}"); Assert.That(delete.error, Is.Null); - Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null); + Assert.That(UnityObjectCompat.ResolveByInstanceId(instanceId), Is.Null); } [Test] @@ -573,7 +573,7 @@ public void ObjectSetProperty_AssignsObjectReferenceByAssetPath() var response = _router.Dispatch( "object/set-property", 1, - "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}" + "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}" ); Assert.That(response.error, Is.Null); @@ -598,7 +598,7 @@ public void ObjectSetProperty_AssignsRendererMaterialArrayByAssetPath() var response = _router.Dispatch( "object/set-property", 1, - "{\"instanceId\":" + cube.GetInstanceID() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}" + "{\"instanceId\":" + cube.GetId() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}" ); Assert.That(response.error, Is.Null); @@ -616,7 +616,7 @@ public void ObjectSetProperty_RejectsUnknownObjectReference() var response = _router.Dispatch( "object/set-property", 1, - "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}" + "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}" ); Assert.That(response.error, Is.Not.Null); @@ -1071,7 +1071,7 @@ public void SceneFocus_WithAxis_AlignsSceneCameraTowardTarget() var response = _router.Dispatch( "scene/focus", 1, - "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[1,0,1]}" + "{\"instanceId\":" + cube.GetId() + ",\"axis\":[1,0,1]}" ); Assert.That(response.error, Is.Null); @@ -1102,7 +1102,7 @@ public void SceneFocus_RejectsZeroAxisVector() var response = _router.Dispatch( "scene/focus", 1, - "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[0,0,0]}" + "{\"instanceId\":" + cube.GetId() + ",\"axis\":[0,0,0]}" ); Assert.That(response.error, Is.Not.Null);