diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs index 4600a6c345..a3651d0c5d 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisAssetBundlePipeline.cs @@ -13,6 +13,7 @@ public static class BasisAssetBundlePipeline // Define static delegates public delegate void BeforeBuildGameobjectHandler(GameObject prefab, BasisAssetBundleObject settings); public delegate void BeforeBuildSceneHandler(Scene prefab, BasisAssetBundleObject settings); + public delegate string PrepareBuildSceneAssetPathHandler(Scene scene, BasisAssetBundleObject settings); public delegate void AfterBuildHandler(string assetBundleName); public delegate void BuildErrorHandler(Exception ex, GameObject prefab, bool wasModified, string temporaryStorage); @@ -22,6 +23,7 @@ public static class BasisAssetBundlePipeline public static BuildErrorHandler OnBuildErrorPrefab; public static BeforeBuildSceneHandler OnBeforeBuildScene; + public static PrepareBuildSceneAssetPathHandler OnPrepareBuildSceneAssetPath; public static AfterBuildHandler OnAfterBuildScene; public static BuildErrorHandler OnBuildErrorScene; @@ -78,7 +80,16 @@ public static class BasisAssetBundlePipeline } OnBeforeBuildScene?.Invoke(scene, settings); - assetPath = TemporaryStorageHandler.SaveScene(scene, settings, out uniqueID); + string preparedSceneAssetPath = OnPrepareBuildSceneAssetPath?.Invoke(scene, settings); + if (!string.IsNullOrEmpty(preparedSceneAssetPath)) + { + assetPath = preparedSceneAssetPath; + uniqueID = Path.GetFileNameWithoutExtension(preparedSceneAssetPath); + } + else + { + assetPath = TemporaryStorageHandler.SaveScene(scene, settings, out uniqueID); + } } else { diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs index 5c6b7aaa85..2b5f97b57b 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/AssetBundleBuilder/BasisTemporaryStorageHandler.cs @@ -16,17 +16,15 @@ public static string SavePrefabToTemporaryStorage(GameObject prefab, BasisAssetB } public static string SaveScene(Scene sceneToCopy, BasisAssetBundleObject settings, out string uniqueID) { - // Generate a unique ID + EnsureDirectoryExists(settings.TemporaryStorage); uniqueID = BasisGenerateUniqueID.GenerateUniqueID(); + string scenePath = Path.Combine(settings.TemporaryStorage, $"{uniqueID}.unity"); - // Attempt to save the scene - if (EditorSceneManager.SaveScene(sceneToCopy)) + if (EditorSceneManager.SaveScene(sceneToCopy, scenePath, true)) { - // Return the path it was saved to - return sceneToCopy.path; + return scenePath; } - // If save fails, clear the ID and return null uniqueID = null; return null; } diff --git a/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset b/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset index 163392bd28..829411b153 100644 --- a/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset +++ b/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset @@ -193,4 +193,5 @@ MonoBehaviour: - Cilbox.CilboxAvatarBasis - Basis.Shims.BasisAvatarShim - Basis.Shims.BasisNetworkShim + - Basis.Shims.CilboxUnityEventShim - Cilbox.CilboxProxy diff --git a/Basis/Packages/com.basis.sdk/Settings/PropContentPoliceSelector.asset b/Basis/Packages/com.basis.sdk/Settings/PropContentPoliceSelector.asset index 0cf7d0b770..9ab1f0d0a6 100644 --- a/Basis/Packages/com.basis.sdk/Settings/PropContentPoliceSelector.asset +++ b/Basis/Packages/com.basis.sdk/Settings/PropContentPoliceSelector.asset @@ -358,3 +358,4 @@ MonoBehaviour: - UnityEngine.Rendering.Universal.UniversalAdditionalLightData - Basis.Scripts.BasisSdk.Interactions.BasisInteractableButton - GatorDragonGames.JigglePhysics.JiggleRig + - Basis.Shims.CilboxUnityEventShim diff --git a/Basis/Packages/com.basis.sdk/Settings/SceneContentPoliceSelector.asset b/Basis/Packages/com.basis.sdk/Settings/SceneContentPoliceSelector.asset index 397c8608ba..115c586fdf 100644 --- a/Basis/Packages/com.basis.sdk/Settings/SceneContentPoliceSelector.asset +++ b/Basis/Packages/com.basis.sdk/Settings/SceneContentPoliceSelector.asset @@ -396,3 +396,4 @@ MonoBehaviour: - Basis.Shims.VideoPlayerShim - Basis.Shims.BasisNetworkShim - Basis.Shims.BasisInteractableShim + - Basis.Shims.CilboxUnityEventShim diff --git a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs index 7e19d59afe..7ca1021e6c 100644 --- a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs +++ b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs @@ -1,8 +1,9 @@ #if UNITY_EDITOR using System; using System.Collections.Generic; -using System.Threading.Tasks; +using System.IO; using Basis.Scripts.BasisSdk; +using Basis.Shims; using Cilbox; using UnityEditor; using UnityEditor.SceneManagement; @@ -11,14 +12,48 @@ public class BasisCilboxBuildHook { + private sealed class CilboxProxyReferenceSnapshot + { + public CilboxProxy Proxy; + public Cilbox.Cilbox Box; + } + + [Serializable] + private sealed class PlayModeSceneCapture + { + public string ScenePath; + public string SceneName; + public int SceneIndex; + public bool HasPersistedCapture; + public string CapturedCallsJson; + } + + [Serializable] + private sealed class PlayModeSceneCaptureCollection + { + public PlayModeSceneCapture[] Scenes; + } + + private sealed class PlayModeSceneCaptureLoadResult + { + public bool Found; + public List CapturedCalls; + } + + private const string PlayModeCaptureSessionKey = "BasisCilboxBuildHook.PlayModeSceneCaptures"; + [InitializeOnLoadMethod] private static void Initialize() { - //Debug.Log("BasisCilboxBuildHook initialized."); BasisAssetBundlePipeline.OnBeforeBuildPrefab -= HandleBeforeBuildPrefab; BasisAssetBundlePipeline.OnBeforeBuildPrefab += HandleBeforeBuildPrefab; + BasisAssetBundlePipeline.OnPrepareBuildSceneAssetPath -= PrepareSceneAssetPathForBuild; + BasisAssetBundlePipeline.OnPrepareBuildSceneAssetPath += PrepareSceneAssetPathForBuild; + BasisAvatarSDKInspector.OnBeforeTestInEditor -= HandleBeforeTestInEditor; BasisAvatarSDKInspector.OnBeforeTestInEditor += HandleBeforeTestInEditor; + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } private static void HandleBeforeTestInEditor(GameObject prefabRoot) @@ -26,6 +61,111 @@ private static void HandleBeforeTestInEditor(GameObject prefabRoot) HandleBeforeBuildPrefab(prefabRoot, null); } + private static void OnPlayModeStateChanged(PlayModeStateChange state) + { + if (state == PlayModeStateChange.ExitingEditMode) + { + CaptureLoadedScenesForPlayMode(); + return; + } + + if (state == PlayModeStateChange.EnteredPlayMode) + { + ProcessLoadedScenesForPlayMode(); + return; + } + + if (state == PlayModeStateChange.EnteredEditMode) + { + SessionState.EraseString(PlayModeCaptureSessionKey); + } + } + + private static void CaptureLoadedScenesForPlayMode() + { + PlayModeSceneCaptureCollection collection = new PlayModeSceneCaptureCollection(); + List sceneCaptures = new List(); + + int sceneCount = SceneManager.sceneCount; + for (int sceneIndex = 0; sceneIndex < sceneCount; sceneIndex++) + { + Scene scene = SceneManager.GetSceneAt(sceneIndex); + if (!SceneNeedsCilboxProcessing(scene)) + { + continue; + } + + try + { + List capturedPersistentCalls = CilboxUnityEventRebinder.CaptureForScene(scene); + sceneCaptures.Add( + new PlayModeSceneCapture + { + ScenePath = scene.path, + SceneName = scene.name, + SceneIndex = sceneIndex, + HasPersistedCapture = true, + CapturedCallsJson = CilboxUnityEventRebinder.SerializeCapturedCalls(capturedPersistentCalls) + } + ); + Debug.Log( + $"[{nameof(BasisCilboxBuildHook)}] Prepared play-mode capture for scene {scene.name}: captured {capturedPersistentCalls.Count} cilbox UnityEvent calls." + ); + } + catch (Exception ex) + { + Debug.LogError($"Failed to capture scene {scene.name} before entering play mode: {ex}"); + } + } + + collection.Scenes = sceneCaptures.ToArray(); + SessionState.SetString(PlayModeCaptureSessionKey, JsonUtility.ToJson(collection)); + } + + private static void ProcessLoadedScenesForPlayMode() + { + CleanupStaleCilboxHelpers(); + PlayModeSceneCaptureCollection collection = LoadPlayModeCaptures(); + if (collection?.Scenes == null || collection.Scenes.Length == 0) + { + Debug.Log($"[{nameof(BasisCilboxBuildHook)}] No captured cilbox UnityEvent data was available for play mode."); + return; + } + + int sceneCount = SceneManager.sceneCount; + for (int sceneIndex = 0; sceneIndex < sceneCount; sceneIndex++) + { + Scene scene = SceneManager.GetSceneAt(sceneIndex); + if (!SceneNeedsCilboxProcessing(scene)) + { + continue; + } + + try + { + Dictionary cilboxAssemblySnapshot = CaptureCilboxAssemblySnapshot(); + PlayModeSceneCaptureLoadResult captureResult = LoadCapturedCallsForScene(collection, scene, sceneIndex); + if (!captureResult.Found) + { + Debug.LogWarning( + $"[{nameof(BasisCilboxBuildHook)}] Skipping play-mode cilbox processing for scene {scene.name} because no persisted UnityEvent capture was found." + ); + continue; + } + + List capturedPersistentCalls = captureResult.CapturedCalls; + Debug.Log( + $"[{nameof(BasisCilboxBuildHook)}] Play-mode processing scene {scene.name}: captured {capturedPersistentCalls.Count} cilbox UnityEvent calls." + ); + TryProcessCilboxScene(scene, null, cilboxAssemblySnapshot, capturedPersistentCalls, out _, out _); + } + catch (Exception ex) + { + Debug.LogError($"Failed to prepare scene {scene.name} for cilbox play mode processing: {ex}"); + } + } + } + private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBundleObject settings) { if (prefabRoot == null || !HasCilboxableComponents(prefabRoot)) @@ -33,14 +173,16 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun return; } + CleanupStaleCilboxHelpers(); Debug.Log("Basis build prehook: generating Cilbox assembly data on the isolated build clone."); + Scene originalScene = prefabRoot.scene; Transform originalParent = prefabRoot.transform.parent; int originalSiblingIndex = originalParent != null ? prefabRoot.transform.GetSiblingIndex() : -1; Dictionary cilboxAssemblySnapshot = CaptureCilboxAssemblySnapshot(); + List proxyReferenceSnapshot = CaptureProxyReferences(prefabRoot); List temporarilyDisabledRoots = new List(); Scene temporaryScene = default; - Cilbox.Cilbox temporarySceneCilbox = null; GameObject temporaryCilboxHost = null; bool detachedFromParent = false; try @@ -57,32 +199,14 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun DeactivateOtherSceneRoots(temporaryScene, temporarilyDisabledRoots); - temporarySceneCilbox = FindCilboxInScene(temporaryScene); - if (temporarySceneCilbox == null) + List capturedPersistentCalls = CilboxUnityEventRebinder.CaptureForRoot(prefabRoot); + Debug.Log( + $"[{nameof(BasisCilboxBuildHook)}] Build processing root {prefabRoot.name}: captured {capturedPersistentCalls.Count} cilbox UnityEvent calls." + ); + if (TryProcessCilboxScene(temporaryScene, prefabRoot, cilboxAssemblySnapshot, capturedPersistentCalls, out _, out temporaryCilboxHost)) { - Type fallbackCilboxType = GetFirstLoadedCilboxType(); - if (fallbackCilboxType != null) - { - temporaryCilboxHost = new GameObject("BasisCilboxTempHost"); - SceneManager.MoveGameObjectToScene(temporaryCilboxHost, temporaryScene); - temporarySceneCilbox = temporaryCilboxHost.AddComponent(fallbackCilboxType) as Cilbox.Cilbox; - if (temporarySceneCilbox != null) - { - temporarySceneCilbox.exportDebuggingData = false; - } - } + RestoreExternalCilboxAssemblyData(cilboxAssemblySnapshot, temporaryScene); } - - if (temporarySceneCilbox == null) - { - Debug.LogWarning("Basis build detected Cilboxable scripts, but no Cilbox component was found. Skipping Cilbox prebuild assembly."); - return; - } - - CilboxScenePostprocessor.OnPostprocessScene(temporaryScene); - EnsureTemporarySceneHasAssemblyData(temporarySceneCilbox, cilboxAssemblySnapshot); - RebindProxiesToTemporarySceneCilbox(prefabRoot, temporarySceneCilbox); - RestoreExternalCilboxAssemblyData(cilboxAssemblySnapshot, temporaryScene); } finally { @@ -100,6 +224,8 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun prefabRoot.transform.SetSiblingIndex(siblingIndex); } + RestoreProxyReferences(proxyReferenceSnapshot); + if (temporaryCilboxHost != null) { UnityEngine.Object.DestroyImmediate(temporaryCilboxHost); @@ -117,26 +243,233 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun } } + private static string PrepareSceneAssetPathForBuild(Scene sourceScene, BasisAssetBundleObject settings) + { + if (!sourceScene.IsValid() || !sourceScene.isLoaded || settings == null || !SceneNeedsCilboxProcessing(sourceScene)) + { + return null; + } + + TemporaryStorageHandler.EnsureDirectoryExists(settings.TemporaryStorage); + string tempScenePath = Path.Combine(settings.TemporaryStorage, $"{BasisGenerateUniqueID.GenerateUniqueID()}.unity"); + if (!EditorSceneManager.SaveScene(sourceScene, tempScenePath, true)) + { + Debug.LogError($"[{nameof(BasisCilboxBuildHook)}] Failed to create a temporary build copy for scene {sourceScene.name}."); + return null; + } + + Scene originalActiveScene = SceneManager.GetActiveScene(); + Scene tempScene = default; + GameObject temporaryCilboxHost = null; + try + { + tempScene = EditorSceneManager.OpenScene(tempScenePath, OpenSceneMode.Additive); + SceneManager.SetActiveScene(tempScene); + CleanupCilboxHelpersInScene(tempScene); + + Dictionary cilboxAssemblySnapshot = CaptureCilboxAssemblySnapshot(); + List capturedPersistentCalls = CilboxUnityEventRebinder.CaptureForScene(tempScene); + Debug.Log( + $"[{nameof(BasisCilboxBuildHook)}] Scene build processing {sourceScene.name}: captured {capturedPersistentCalls.Count} cilbox UnityEvent calls." + ); + + if (TryProcessCilboxScene(tempScene, null, cilboxAssemblySnapshot, capturedPersistentCalls, out _, out temporaryCilboxHost)) + { + RestoreExternalCilboxAssemblyData(cilboxAssemblySnapshot, tempScene); + if (!EditorSceneManager.SaveScene(tempScene)) + { + Debug.LogError($"[{nameof(BasisCilboxBuildHook)}] Failed to save processed temporary scene copy for {sourceScene.name}."); + return null; + } + } + + return tempScenePath; + } + catch (Exception ex) + { + Debug.LogError($"[{nameof(BasisCilboxBuildHook)}] Failed to prepare scene {sourceScene.name} for bundle build: {ex}"); + return null; + } + finally + { + if (tempScene.IsValid() && tempScene.isLoaded) + { + EditorSceneManager.CloseScene(tempScene, true); + } + + if (originalActiveScene.IsValid() && originalActiveScene.isLoaded) + { + SceneManager.SetActiveScene(originalActiveScene); + } + } + } + + private static bool TryProcessCilboxScene( + Scene scene, + GameObject contentRoot, + Dictionary snapshot, + List capturedPersistentCalls, + out Cilbox.Cilbox sceneCilbox, + out GameObject temporaryCilboxHost + ) + { + sceneCilbox = FindCilboxInScene(scene); + temporaryCilboxHost = null; + if (sceneCilbox == null) + { + Type fallbackCilboxType = GetFirstLoadedCilboxType(); + if (fallbackCilboxType != null) + { + temporaryCilboxHost = new GameObject(contentRoot == null ? "BasisCilboxPlayModeHost" : "BasisCilboxTempHost"); + SceneManager.MoveGameObjectToScene(temporaryCilboxHost, scene); + sceneCilbox = temporaryCilboxHost.AddComponent(fallbackCilboxType) as Cilbox.Cilbox; + if (sceneCilbox != null) + { + sceneCilbox.exportDebuggingData = false; + } + } + } + + if (sceneCilbox == null) + { + Debug.LogWarning("Basis build detected Cilboxable scripts, but no Cilbox component was found. Skipping Cilbox processing."); + return false; + } + + if (contentRoot != null) + { + CilboxUnityEventRebinder.RemoveShimsFromRoot(contentRoot); + } + else + { + CilboxUnityEventRebinder.RemoveShimsFromScene(scene); + } + + CilboxScenePostprocessor.OnPostprocessScene(scene); + EnsureTemporarySceneHasAssemblyData(sceneCilbox, snapshot); + + if (contentRoot != null) + { + RebindProxiesToTemporarySceneCilbox(contentRoot, sceneCilbox); + } + else + { + RebindSceneProxiesToTemporarySceneCilbox(scene, sceneCilbox); + } + + CilboxUnityEventRebinder.ApplyCapturedCalls(scene, capturedPersistentCalls); + return true; + } + + private static PlayModeSceneCaptureCollection LoadPlayModeCaptures() + { + string json = SessionState.GetString(PlayModeCaptureSessionKey, string.Empty); + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + + try + { + return JsonUtility.FromJson(json); + } + catch (ArgumentException ex) + { + Debug.LogError($"[{nameof(BasisCilboxBuildHook)}] Failed to deserialize {PlayModeCaptureSessionKey}: {ex}"); + SessionState.EraseString(PlayModeCaptureSessionKey); + return null; + } + catch (Exception ex) + { + Debug.LogError($"[{nameof(BasisCilboxBuildHook)}] Unexpected error while deserializing {PlayModeCaptureSessionKey}: {ex}"); + SessionState.EraseString(PlayModeCaptureSessionKey); + return null; + } + } + + private static PlayModeSceneCaptureLoadResult LoadCapturedCallsForScene(PlayModeSceneCaptureCollection collection, Scene scene, int sceneIndex) + { + PlayModeSceneCaptureLoadResult result = new PlayModeSceneCaptureLoadResult + { + Found = false, + CapturedCalls = new List() + }; + + if (collection?.Scenes == null) + { + return result; + } + + int captureCount = collection.Scenes.Length; + for (int i = 0; i < captureCount; i++) + { + PlayModeSceneCapture sceneCapture = collection.Scenes[i]; + if (sceneCapture == null) + { + continue; + } + + bool pathMatches = !string.IsNullOrEmpty(sceneCapture.ScenePath) && string.Equals(sceneCapture.ScenePath, scene.path, StringComparison.Ordinal); + bool nameAndIndexMatch = string.Equals(sceneCapture.SceneName, scene.name, StringComparison.Ordinal) && sceneCapture.SceneIndex == sceneIndex; + bool fallbackNameMatch = string.IsNullOrEmpty(scene.path) && string.IsNullOrEmpty(sceneCapture.ScenePath) && string.Equals(sceneCapture.SceneName, scene.name, StringComparison.Ordinal); + if (!pathMatches && !nameAndIndexMatch && !fallbackNameMatch) + { + continue; + } + + if (!sceneCapture.HasPersistedCapture) + { + return result; + } + + result.Found = true; + result.CapturedCalls = CilboxUnityEventRebinder.DeserializeCapturedCalls(sceneCapture.CapturedCallsJson); + return result; + } + + return result; + } + private static void CleanupStaleCilboxHelpers() { + int sceneCount = SceneManager.sceneCount; + for (int sceneIndex = 0; sceneIndex < sceneCount; sceneIndex++) + { + CleanupCilboxHelpersInScene(SceneManager.GetSceneAt(sceneIndex)); + } + } + + private static void CleanupCilboxHelpersInScene(Scene scene) + { + if (!scene.IsValid() || !scene.isLoaded) + { + return; + } + GameObject[] allObjects = Resources.FindObjectsOfTypeAll(); int length = allObjects.Length; for (int i = 0; i < length; i++) { GameObject go = allObjects[i]; - if (go == null) + if (go == null || go.scene != scene) { continue; } - if (!go.scene.IsValid() || !go.scene.isLoaded) + if (string.Equals(go.name, "CilboxDirtier", StringComparison.Ordinal) || go.name.StartsWith("CilboxAsm ", StringComparison.Ordinal)) { - continue; + UnityEngine.Object.DestroyImmediate(go); } + } - if (go.name == "CilboxDirtier" || go.name.StartsWith("CilboxAsm ")) + CilboxUnityEventShim[] staleShims = Resources.FindObjectsOfTypeAll(); + int shimCount = staleShims.Length; + for (int i = 0; i < shimCount; i++) + { + CilboxUnityEventShim shim = staleShims[i]; + if (shim != null && shim.gameObject.scene == scene) { - UnityEngine.Object.DestroyImmediate(go); + UnityEngine.Object.DestroyImmediate(shim); } } } @@ -164,9 +497,40 @@ private static bool HasCilboxableComponents(GameObject root) return true; } } + return false; } + private static bool HasCilboxableComponents(Scene scene) + { + if (!scene.IsValid() || !scene.isLoaded) + { + return false; + } + + GameObject[] roots = scene.GetRootGameObjects(); + int length = roots.Length; + for (int i = 0; i < length; i++) + { + if (HasCilboxableComponents(roots[i])) + { + return true; + } + } + + return false; + } + + private static bool SceneNeedsCilboxProcessing(Scene scene) + { + if (!scene.IsValid() || !scene.isLoaded) + { + return false; + } + + return HasCilboxableComponents(scene) || FindCilboxInScene(scene) != null; + } + private static Dictionary CaptureCilboxAssemblySnapshot() { Dictionary snapshot = new Dictionary(); @@ -312,6 +676,35 @@ private static void RebindProxiesToTemporarySceneCilbox(GameObject contentRoot, } CilboxProxy[] proxies = contentRoot.GetComponentsInChildren(true); + RebindProxiesToTemporarySceneCilbox(proxies, temporarySceneCilbox); + } + + private static void RebindSceneProxiesToTemporarySceneCilbox(Scene scene, Cilbox.Cilbox temporarySceneCilbox) + { + if (!scene.IsValid() || !scene.isLoaded || temporarySceneCilbox == null) + { + return; + } + + List proxies = new List(); + GameObject[] roots = scene.GetRootGameObjects(); + int rootCount = roots.Length; + for (int i = 0; i < rootCount; i++) + { + GameObject root = roots[i]; + if (root == null) + { + continue; + } + + proxies.AddRange(root.GetComponentsInChildren(true)); + } + + RebindProxiesToTemporarySceneCilbox(proxies.ToArray(), temporarySceneCilbox); + } + + private static void RebindProxiesToTemporarySceneCilbox(CilboxProxy[] proxies, Cilbox.Cilbox temporarySceneCilbox) + { int length = proxies.Length; for (int i = 0; i < length; i++) { @@ -355,5 +748,55 @@ private static void RestoreExternalCilboxAssemblyData(Dictionary CaptureProxyReferences(GameObject root) + { + List snapshot = new List(); + if (root == null) + { + return snapshot; + } + + CilboxProxy[] proxies = root.GetComponentsInChildren(true); + int length = proxies.Length; + for (int i = 0; i < length; i++) + { + CilboxProxy proxy = proxies[i]; + if (proxy == null) + { + continue; + } + + snapshot.Add( + new CilboxProxyReferenceSnapshot + { + Proxy = proxy, + Box = proxy.box + } + ); + } + + return snapshot; + } + + private static void RestoreProxyReferences(List snapshot) + { + if (snapshot == null) + { + return; + } + + int length = snapshot.Count; + for (int i = 0; i < length; i++) + { + CilboxProxyReferenceSnapshot entry = snapshot[i]; + if (entry?.Proxy == null || entry.Proxy.box == entry.Box) + { + continue; + } + + entry.Proxy.box = entry.Box; + EditorUtility.SetDirty(entry.Proxy); + } + } } #endif diff --git a/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs new file mode 100644 index 0000000000..a275b132ef --- /dev/null +++ b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs @@ -0,0 +1,769 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Cilbox; +using UnityEditor; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.SceneManagement; + +namespace Basis.Shims +{ + internal static class CilboxUnityEventRebinder + { + [Serializable] + private sealed class CapturedPersistentCall + { + public string HostGameObjectPath; + public string HostComponentTypeName; + public int HostComponentOrdinal; + public string PersistentCallPath; + public string TargetGameObjectPath; + public string TargetClassName; + public int TargetComponentOrdinal; + public string MethodName; + public string FullSignature; + public PersistentListenerMode ListenerMode; + public CilboxUnityEventArgumentKind ArgumentKind; + public bool BoolArgument; + public int IntArgument; + public float FloatArgument; + public string StringArgument; + public string ObjectSceneGameObjectPath; + public string ObjectSceneComponentTypeName; + public int ObjectSceneComponentOrdinal = -1; + public string ObjectGlobalId; + public string CilboxObjectArgumentGameObjectPath; + public string CilboxObjectArgumentClassName; + public int CilboxObjectArgumentComponentOrdinal = -1; + } + + [Serializable] + private sealed class CapturedPersistentCallCollection + { + public CapturedPersistentCall[] Calls; + } + + public static List CaptureForRoot(GameObject root) + { + List captured = new List(); + if (root == null) + { + return Box(captured); + } + + Component[] components = root.GetComponentsInChildren(true); + CaptureForComponents(components, captured); + return Box(captured); + } + + public static List CaptureForScene(Scene scene) + { + List captured = new List(); + if (!scene.IsValid() || !scene.isLoaded) + { + return Box(captured); + } + + List components = new List(); + GameObject[] roots = scene.GetRootGameObjects(); + int rootCount = roots.Length; + for (int i = 0; i < rootCount; i++) + { + GameObject root = roots[i]; + if (root == null) + { + continue; + } + + components.AddRange(root.GetComponentsInChildren(true)); + } + + CaptureForComponents(components.ToArray(), captured); + return Box(captured); + } + + public static string SerializeCapturedCalls(List boxedCalls) + { + if (boxedCalls == null || boxedCalls.Count == 0) + { + return string.Empty; + } + + List calls = new List(boxedCalls.Count); + int length = boxedCalls.Count; + for (int i = 0; i < length; i++) + { + if (boxedCalls[i] is CapturedPersistentCall captured) + { + calls.Add(captured); + } + } + + return JsonUtility.ToJson(new CapturedPersistentCallCollection { Calls = calls.ToArray() }); + } + + public static List DeserializeCapturedCalls(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + + CapturedPersistentCallCollection collection = JsonUtility.FromJson(json); + if (collection?.Calls == null || collection.Calls.Length == 0) + { + return new List(); + } + + List boxed = new List(collection.Calls.Length); + int length = collection.Calls.Length; + for (int i = 0; i < length; i++) + { + boxed.Add(collection.Calls[i]); + } + + return boxed; + } + + public static void RemoveShimsFromRoot(GameObject root) + { + if (root == null) + { + return; + } + + CilboxUnityEventShim[] shims = root.GetComponentsInChildren(true); + int length = shims.Length; + for (int i = 0; i < length; i++) + { + CilboxUnityEventShim shim = shims[i]; + if (shim != null) + { + UnityEngine.Object.DestroyImmediate(shim); + } + } + } + + public static void RemoveShimsFromScene(Scene scene) + { + if (!scene.IsValid() || !scene.isLoaded) + { + return; + } + + GameObject[] roots = scene.GetRootGameObjects(); + int length = roots.Length; + for (int i = 0; i < length; i++) + { + RemoveShimsFromRoot(roots[i]); + } + } + + public static void ApplyCapturedCalls(Scene scene, List boxedCalls) + { + if (!scene.IsValid() || !scene.isLoaded || boxedCalls == null || boxedCalls.Count == 0) + { + return; + } + + int length = boxedCalls.Count; + for (int i = 0; i < length; i++) + { + if (boxedCalls[i] is CapturedPersistentCall captured) + { + ApplyCapturedCall(scene, captured); + } + } + } + + private static List Box(List captured) + { + List boxed = new List(captured.Count); + int length = captured.Count; + for (int i = 0; i < length; i++) + { + boxed.Add(captured[i]); + } + + return boxed; + } + + private static void CaptureForComponents(Component[] components, List captured) + { + int length = components.Length; + for (int i = 0; i < length; i++) + { + Component component = components[i]; + if (component == null || component is CilboxUnityEventShim) + { + continue; + } + + if (component is MonoBehaviour monoBehaviour && CilboxUtil.HasCilboxableAttribute(monoBehaviour.GetType())) + { + continue; + } + + CaptureForComponent(component, captured); + } + } + + private static void CaptureForComponent(Component component, List captured) + { + SerializedObject serializedObject = new SerializedObject(component); + SerializedProperty iterator = serializedObject.GetIterator(); + HashSet seenCallArrays = new HashSet(StringComparer.Ordinal); + bool enterChildren = true; + while (iterator.NextVisible(enterChildren)) + { + enterChildren = true; + if (iterator.propertyType != SerializedPropertyType.Generic) + { + continue; + } + + SerializedProperty callsArray = iterator.FindPropertyRelative("m_PersistentCalls.m_Calls"); + if (callsArray == null || !callsArray.isArray || !seenCallArrays.Add(callsArray.propertyPath)) + { + continue; + } + + int callCount = callsArray.arraySize; + for (int callIndex = 0; callIndex < callCount; callIndex++) + { + SerializedProperty callProperty = callsArray.GetArrayElementAtIndex(callIndex); + if (callProperty != null && TryCaptureCall(component, callProperty, out CapturedPersistentCall persistentCall)) + { + captured.Add(persistentCall); + } + } + } + } + + private static bool TryCaptureCall(Component hostComponent, SerializedProperty callProperty, out CapturedPersistentCall captured) + { + captured = null; + + SerializedProperty targetProperty = callProperty.FindPropertyRelative("m_Target"); + if (targetProperty?.objectReferenceValue is not MonoBehaviour targetMonoBehaviour) + { + return false; + } + + if (!CilboxUtil.HasCilboxableAttribute(targetMonoBehaviour.GetType())) + { + return false; + } + + SerializedProperty modeProperty = callProperty.FindPropertyRelative("m_Mode"); + if (modeProperty == null) + { + return false; + } + + PersistentListenerMode listenerMode = (PersistentListenerMode)modeProperty.intValue; + if (listenerMode == PersistentListenerMode.EventDefined) + { + Debug.LogWarning( + $"[{nameof(CilboxUnityEventRebinder)}] Skipping dynamic UnityEvent listener {targetMonoBehaviour.GetType().FullName}.{callProperty.FindPropertyRelative("m_MethodName")?.stringValue} on {hostComponent.name}" + ); + return false; + } + + int hostComponentOrdinal = GetComponentOrdinal(hostComponent); + int targetComponentOrdinal = GetComponentOrdinal(targetMonoBehaviour); + if (hostComponentOrdinal < 0 || targetComponentOrdinal < 0) + { + return false; + } + + string methodName = callProperty.FindPropertyRelative("m_MethodName")?.stringValue; + string objectArgumentAssemblyTypeName = callProperty.FindPropertyRelative("m_Arguments.m_ObjectArgumentAssemblyTypeName")?.stringValue; + MethodInfo methodInfo = ResolvePersistentCallMethod(targetMonoBehaviour.GetType(), methodName, listenerMode, objectArgumentAssemblyTypeName); + + captured = new CapturedPersistentCall + { + HostGameObjectPath = GetGameObjectPath(hostComponent.gameObject), + HostComponentTypeName = hostComponent.GetType().FullName, + HostComponentOrdinal = hostComponentOrdinal, + PersistentCallPath = callProperty.propertyPath, + TargetGameObjectPath = GetGameObjectPath(targetMonoBehaviour.gameObject), + TargetClassName = targetMonoBehaviour.GetType().FullName, + TargetComponentOrdinal = targetComponentOrdinal, + MethodName = methodName, + FullSignature = methodInfo?.ToString(), + ListenerMode = listenerMode + }; + + PopulateArgumentData(callProperty, captured); + Debug.Log( + $"[{nameof(CilboxUnityEventRebinder)}] Captured {captured.HostComponentTypeName}.{captured.PersistentCallPath} -> {captured.TargetClassName}.{captured.MethodName} on {captured.TargetGameObjectPath}" + ); + return true; + } + + private static void PopulateArgumentData(SerializedProperty callProperty, CapturedPersistentCall captured) + { + switch (captured.ListenerMode) + { + case PersistentListenerMode.Void: + captured.ArgumentKind = CilboxUnityEventArgumentKind.None; + return; + case PersistentListenerMode.Bool: + captured.ArgumentKind = CilboxUnityEventArgumentKind.Bool; + captured.BoolArgument = callProperty.FindPropertyRelative("m_Arguments.m_BoolArgument")?.boolValue ?? false; + return; + case PersistentListenerMode.Int: + captured.ArgumentKind = CilboxUnityEventArgumentKind.Int; + captured.IntArgument = callProperty.FindPropertyRelative("m_Arguments.m_IntArgument")?.intValue ?? 0; + return; + case PersistentListenerMode.Float: + captured.ArgumentKind = CilboxUnityEventArgumentKind.Float; + captured.FloatArgument = callProperty.FindPropertyRelative("m_Arguments.m_FloatArgument")?.floatValue ?? 0f; + return; + case PersistentListenerMode.String: + captured.ArgumentKind = CilboxUnityEventArgumentKind.String; + captured.StringArgument = callProperty.FindPropertyRelative("m_Arguments.m_StringArgument")?.stringValue; + return; + case PersistentListenerMode.Object: + captured.ArgumentKind = CilboxUnityEventArgumentKind.Object; + CaptureObjectArgument(callProperty.FindPropertyRelative("m_Arguments.m_ObjectArgument")?.objectReferenceValue, captured); + return; + default: + captured.ArgumentKind = CilboxUnityEventArgumentKind.None; + return; + } + } + + private static void CaptureObjectArgument(UnityEngine.Object objectArgument, CapturedPersistentCall captured) + { + if (objectArgument == null) + { + return; + } + + if (objectArgument is MonoBehaviour objectMonoBehaviour && CilboxUtil.HasCilboxableAttribute(objectMonoBehaviour.GetType())) + { + int objectComponentOrdinal = GetComponentOrdinal(objectMonoBehaviour); + if (objectComponentOrdinal >= 0) + { + captured.CilboxObjectArgumentGameObjectPath = GetGameObjectPath(objectMonoBehaviour.gameObject); + captured.CilboxObjectArgumentClassName = objectMonoBehaviour.GetType().FullName; + captured.CilboxObjectArgumentComponentOrdinal = objectComponentOrdinal; + } + + return; + } + + if (objectArgument is Component component) + { + captured.ObjectSceneGameObjectPath = GetGameObjectPath(component.gameObject); + captured.ObjectSceneComponentTypeName = component.GetType().FullName; + captured.ObjectSceneComponentOrdinal = GetComponentOrdinal(component); + return; + } + + if (objectArgument is GameObject gameObject) + { + captured.ObjectSceneGameObjectPath = GetGameObjectPath(gameObject); + return; + } + + GlobalObjectId globalId = GlobalObjectId.GetGlobalObjectIdSlow(objectArgument); + captured.ObjectGlobalId = globalId.ToString(); + } + + private static void ApplyCapturedCall(Scene scene, CapturedPersistentCall captured) + { + Component hostComponent = ResolveComponent(scene, captured.HostGameObjectPath, captured.HostComponentTypeName, captured.HostComponentOrdinal); + if (hostComponent == null) + { + Debug.LogWarning( + $"[{nameof(CilboxUnityEventRebinder)}] Could not resolve host component {captured.HostComponentTypeName} at {captured.HostGameObjectPath}" + ); + return; + } + + CilboxProxy targetProxy = FindMatchingProxy(scene, captured.TargetGameObjectPath, captured.TargetClassName, captured.TargetComponentOrdinal); + if (targetProxy == null) + { + Debug.LogError( + $"[{nameof(CilboxUnityEventRebinder)}] Failed to find CilboxProxy for {captured.TargetClassName}.{captured.MethodName} at {captured.TargetGameObjectPath}" + ); + return; + } + + UnityEngine.Object objectArgument = ResolveObjectArgument(scene, captured); + if ( + captured.ArgumentKind == CilboxUnityEventArgumentKind.Object + && ( + !string.IsNullOrEmpty(captured.ObjectSceneGameObjectPath) + || !string.IsNullOrEmpty(captured.ObjectGlobalId) + || !string.IsNullOrEmpty(captured.CilboxObjectArgumentGameObjectPath) + ) + && objectArgument == null + ) + { + Debug.LogError( + $"[{nameof(CilboxUnityEventRebinder)}] Failed to resolve object argument for {captured.TargetClassName}.{captured.MethodName}" + ); + return; + } + + CilboxUnityEventShim shim = hostComponent.gameObject.AddComponent(); + shim.Configure( + targetProxy, + captured.TargetClassName, + captured.MethodName, + captured.FullSignature, + captured.ArgumentKind, + BuildScalarArgument(captured), + objectArgument + ); + EditorUtility.SetDirty(shim); + + SerializedObject serializedObject = new SerializedObject(hostComponent); + SerializedProperty callProperty = serializedObject.FindProperty(captured.PersistentCallPath); + if (callProperty == null) + { + Debug.LogWarning( + $"[{nameof(CilboxUnityEventRebinder)}] Could not reopen persistent call {captured.PersistentCallPath} on {hostComponent.name} after creating a shim." + ); + return; + } + + callProperty.FindPropertyRelative("m_Target").objectReferenceValue = shim; + callProperty.FindPropertyRelative("m_TargetAssemblyTypeName").stringValue = GetUnityTypeName(typeof(CilboxUnityEventShim)); + callProperty.FindPropertyRelative("m_MethodName").stringValue = nameof(CilboxUnityEventShim.Invoke); + callProperty.FindPropertyRelative("m_Mode").intValue = (int)PersistentListenerMode.Void; + + SerializedProperty argumentsProperty = callProperty.FindPropertyRelative("m_Arguments"); + argumentsProperty.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = null; + argumentsProperty.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = GetUnityTypeName(typeof(UnityEngine.Object)); + argumentsProperty.FindPropertyRelative("m_IntArgument").intValue = 0; + argumentsProperty.FindPropertyRelative("m_FloatArgument").floatValue = 0f; + argumentsProperty.FindPropertyRelative("m_StringArgument").stringValue = string.Empty; + argumentsProperty.FindPropertyRelative("m_BoolArgument").boolValue = false; + + serializedObject.ApplyModifiedPropertiesWithoutUndo(); + EditorUtility.SetDirty(hostComponent); + Debug.Log( + $"[{nameof(CilboxUnityEventRebinder)}] Rebound {captured.HostComponentTypeName}.{captured.PersistentCallPath} to {nameof(CilboxUnityEventShim)} on {hostComponent.gameObject.name} for {captured.TargetClassName}.{captured.MethodName}" + ); + } + + private static UnityEngine.Object ResolveObjectArgument(Scene scene, CapturedPersistentCall captured) + { + if (!string.IsNullOrEmpty(captured.CilboxObjectArgumentGameObjectPath)) + { + return FindMatchingProxy( + scene, + captured.CilboxObjectArgumentGameObjectPath, + captured.CilboxObjectArgumentClassName, + captured.CilboxObjectArgumentComponentOrdinal + ); + } + + if (!string.IsNullOrEmpty(captured.ObjectSceneComponentTypeName)) + { + return ResolveComponent( + scene, + captured.ObjectSceneGameObjectPath, + captured.ObjectSceneComponentTypeName, + captured.ObjectSceneComponentOrdinal + ); + } + + if (!string.IsNullOrEmpty(captured.ObjectSceneGameObjectPath)) + { + return ResolveGameObject(scene, captured.ObjectSceneGameObjectPath); + } + + if (!string.IsNullOrEmpty(captured.ObjectGlobalId) && GlobalObjectId.TryParse(captured.ObjectGlobalId, out GlobalObjectId globalId)) + { + return GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalId); + } + + return null; + } + + private static string BuildScalarArgument(CapturedPersistentCall captured) + { + switch (captured.ArgumentKind) + { + case CilboxUnityEventArgumentKind.Bool: + return captured.BoolArgument ? bool.TrueString : bool.FalseString; + case CilboxUnityEventArgumentKind.Int: + return captured.IntArgument.ToString(CultureInfo.InvariantCulture); + case CilboxUnityEventArgumentKind.Float: + return captured.FloatArgument.ToString("R", CultureInfo.InvariantCulture); + case CilboxUnityEventArgumentKind.String: + return captured.StringArgument; + default: + return null; + } + } + + private static MethodInfo ResolvePersistentCallMethod( + Type targetType, + string methodName, + PersistentListenerMode listenerMode, + string objectArgumentAssemblyTypeName + ) + { + if (targetType == null || string.IsNullOrEmpty(methodName)) + { + return null; + } + + BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + Type parameterType = listenerMode switch + { + PersistentListenerMode.Void => null, + PersistentListenerMode.Bool => typeof(bool), + PersistentListenerMode.Int => typeof(int), + PersistentListenerMode.Float => typeof(float), + PersistentListenerMode.String => typeof(string), + PersistentListenerMode.Object => ResolveType(objectArgumentAssemblyTypeName) ?? typeof(UnityEngine.Object), + _ => null + }; + + if (listenerMode == PersistentListenerMode.Void) + { + return targetType.GetMethod(methodName, flags, null, Type.EmptyTypes, null); + } + + if (parameterType == null) + { + return null; + } + + MethodInfo exact = targetType.GetMethod(methodName, flags, null, new[] { parameterType }, null); + if (exact != null) + { + return exact; + } + + MethodInfo[] methods = targetType.GetMethods(flags); + int length = methods.Length; + for (int i = 0; i < length; i++) + { + MethodInfo method = methods[i]; + if (method.Name != methodName) + { + continue; + } + + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length != 1) + { + continue; + } + + if (parameters[0].ParameterType.IsAssignableFrom(parameterType)) + { + return method; + } + } + + return null; + } + + private static Type ResolveType(string unityTypeName) + { + if (string.IsNullOrWhiteSpace(unityTypeName)) + { + return null; + } + + Type resolved = Type.GetType(unityTypeName, false); + if (resolved != null) + { + return resolved; + } + + string typeName = unityTypeName; + string assemblyName = null; + int commaIndex = unityTypeName.IndexOf(','); + if (commaIndex >= 0) + { + typeName = unityTypeName.Substring(0, commaIndex).Trim(); + assemblyName = unityTypeName[(commaIndex + 1)..].Trim(); + } + + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + int length = assemblies.Length; + for (int i = 0; i < length; i++) + { + Assembly assembly = assemblies[i]; + if (assemblyName != null && !string.Equals(assembly.GetName().Name, assemblyName, StringComparison.Ordinal)) + { + continue; + } + + resolved = assembly.GetType(typeName, false); + if (resolved != null) + { + return resolved; + } + } + + return null; + } + + private static int GetComponentOrdinal(Component component) + { + Component[] components = component.gameObject.GetComponents(component.GetType()); + int length = components.Length; + for (int i = 0; i < length; i++) + { + if (components[i] == component) + { + return i; + } + } + + return -1; + } + + private static string GetGameObjectPath(GameObject gameObject) + { + if (gameObject == null) + { + return string.Empty; + } + + List siblingIndices = new List(); + Transform current = gameObject.transform; + while (current.parent != null) + { + siblingIndices.Add(current.GetSiblingIndex()); + current = current.parent; + } + + GameObject[] roots = gameObject.scene.GetRootGameObjects(); + int rootIndex = -1; + int rootCount = roots.Length; + for (int i = 0; i < rootCount; i++) + { + if (roots[i] == current.gameObject) + { + rootIndex = i; + break; + } + } + + if (rootIndex < 0) + { + return string.Empty; + } + + siblingIndices.Reverse(); + return siblingIndices.Count == 0 ? $"r{rootIndex}" : $"r{rootIndex}/{string.Join("/", siblingIndices)}"; + } + + private static GameObject ResolveGameObject(Scene scene, string path) + { + if (!scene.IsValid() || !scene.isLoaded || string.IsNullOrEmpty(path)) + { + return null; + } + + GameObject[] roots = scene.GetRootGameObjects(); + string[] parts = path.Split('/'); + if (parts.Length == 0 || parts[0].Length <= 1 || parts[0][0] != 'r' || !int.TryParse(parts[0].Substring(1), out int rootIndex)) + { + return null; + } + + if (rootIndex < 0 || rootIndex >= roots.Length) + { + return null; + } + + Transform current = roots[rootIndex]?.transform; + if (current == null) + { + return null; + } + + for (int partIndex = 1; partIndex < parts.Length; partIndex++) + { + if (!int.TryParse(parts[partIndex], out int siblingIndex) || siblingIndex < 0 || siblingIndex >= current.childCount) + { + return null; + } + + current = current.GetChild(siblingIndex); + if (current == null) + { + return null; + } + } + + return current.gameObject; + } + + private static Component ResolveComponent(Scene scene, string gameObjectPath, string componentTypeName, int componentOrdinal) + { + GameObject gameObject = ResolveGameObject(scene, gameObjectPath); + if (gameObject == null || string.IsNullOrEmpty(componentTypeName) || componentOrdinal < 0) + { + return null; + } + + Type componentType = ResolveType(componentTypeName); + if (componentType == null) + { + return null; + } + + Component[] components = gameObject.GetComponents(componentType); + if (componentOrdinal >= components.Length) + { + return null; + } + + return components[componentOrdinal]; + } + + private static CilboxProxy FindMatchingProxy(Scene scene, string gameObjectPath, string className, int componentOrdinal) + { + GameObject gameObject = ResolveGameObject(scene, gameObjectPath); + if (gameObject == null || string.IsNullOrEmpty(className) || componentOrdinal < 0) + { + return null; + } + + CilboxProxy[] proxies = gameObject.GetComponents(); + int ordinal = 0; + int length = proxies.Length; + for (int i = 0; i < length; i++) + { + CilboxProxy proxy = proxies[i]; + if (proxy == null || !string.Equals(proxy.className, className, StringComparison.Ordinal)) + { + continue; + } + + if (ordinal == componentOrdinal) + { + return proxy; + } + + ordinal++; + } + + return null; + } + + private static string GetUnityTypeName(Type type) + { + return $"{type.FullName}, {type.Assembly.GetName().Name}"; + } + } +} +#endif diff --git a/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs.meta b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs.meta new file mode 100644 index 0000000000..25a824a997 --- /dev/null +++ b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 504c47b8cc8742dc8a6e8a2676f3d512 diff --git a/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs new file mode 100644 index 0000000000..60fa02dbdb --- /dev/null +++ b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs @@ -0,0 +1,156 @@ +using System; +using System.Globalization; +using Cilbox; +using UnityEngine; + +namespace Basis.Shims +{ + public enum CilboxUnityEventArgumentKind + { + None, + Bool, + Int, + Float, + String, + Object + } + + public sealed class CilboxUnityEventShim : MonoBehaviour + { + [SerializeField] private CilboxProxy target; + [SerializeField] private string className; + [SerializeField] private string methodName; + [SerializeField] private string fullSignature; + [SerializeField] private CilboxUnityEventArgumentKind argumentKind; + [SerializeField] private string scalarArgument; + [SerializeField] private UnityEngine.Object objectArgument; + + [NonSerialized] private bool hasCachedMethodIndex; + [NonSerialized] private uint cachedMethodIndex; + + public void Invoke() + { + if (target == null) + { + Debug.LogError($"[{nameof(CilboxUnityEventShim)}:{name}] Missing cilbox proxy target for {className}.{methodName}"); + return; + } + + target.RuntimeProxyLoad(); + + CilboxClass proxyClass = target.cls; + if (proxyClass == null) + { + Debug.LogError($"[{nameof(CilboxUnityEventShim)}:{name}] Proxy class was not initialized for {className}.{methodName}"); + return; + } + + CilboxMethod method = ResolveMethod(proxyClass); + if (method == null) + { + Debug.LogError( + $"[{nameof(CilboxUnityEventShim)}:{name}] Could not resolve method {className}.{methodName} ({fullSignature}) on {target.gameObject.name}" + ); + return; + } + + object[] parameters = BuildParameters(); + if (parameters.Length == 1 && parameters[0] is CilboxProxy argumentProxy) + { + argumentProxy.RuntimeProxyLoad(); + } + + method.Interpret(target, parameters); + } + + internal void Configure( + CilboxProxy targetProxy, + string configuredClassName, + string configuredMethodName, + string configuredFullSignature, + CilboxUnityEventArgumentKind configuredArgumentKind, + string configuredScalarArgument, + UnityEngine.Object configuredObjectArgument + ) + { + target = targetProxy; + className = configuredClassName; + methodName = configuredMethodName; + fullSignature = configuredFullSignature; + argumentKind = configuredArgumentKind; + scalarArgument = configuredScalarArgument; + objectArgument = configuredObjectArgument; + hasCachedMethodIndex = false; + cachedMethodIndex = 0; + } + + private CilboxMethod ResolveMethod(CilboxClass proxyClass) + { + if (hasCachedMethodIndex && cachedMethodIndex < proxyClass.methods.Length) + { + return proxyClass.methods[cachedMethodIndex]; + } + + if (!string.IsNullOrEmpty(fullSignature) && proxyClass.methodFullSignatureToIndex.TryGetValue(fullSignature, out uint signatureIndex)) + { + hasCachedMethodIndex = true; + cachedMethodIndex = signatureIndex; + return proxyClass.methods[signatureIndex]; + } + + if (!string.IsNullOrEmpty(fullSignature)) + { + Debug.LogWarning( + $"[{nameof(CilboxUnityEventShim)}:{name}] Full signature lookup failed for {className}.{methodName} ({fullSignature}), falling back to method name." + ); + } + + if (!string.IsNullOrEmpty(methodName) && proxyClass.methodNameToIndex.TryGetValue(methodName, out uint methodIndex)) + { + hasCachedMethodIndex = true; + cachedMethodIndex = methodIndex; + return proxyClass.methods[methodIndex]; + } + + return null; + } + + private object[] BuildParameters() + { + switch (argumentKind) + { + case CilboxUnityEventArgumentKind.None: + return Array.Empty(); + case CilboxUnityEventArgumentKind.Bool: + return new object[] { ParseBoolArgument() }; + case CilboxUnityEventArgumentKind.Int: + return new object[] { ParseIntArgument() }; + case CilboxUnityEventArgumentKind.Float: + return new object[] { ParseFloatArgument() }; + case CilboxUnityEventArgumentKind.String: + return new object[] { scalarArgument }; + case CilboxUnityEventArgumentKind.Object: + return new object[] { objectArgument }; + default: + return Array.Empty(); + } + } + + private bool ParseBoolArgument() + { + return string.Equals(scalarArgument, bool.TrueString, StringComparison.OrdinalIgnoreCase); + } + + private int ParseIntArgument() + { + return int.TryParse(scalarArgument, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) ? value : 0; + } + + private float ParseFloatArgument() + { + return float.TryParse(scalarArgument, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out float value) + ? value + : 0f; + } + } +} diff --git a/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs.meta b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs.meta new file mode 100644 index 0000000000..93bc641ad9 --- /dev/null +++ b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a06c13f860fa4681ac80c4144f9af57d