From fad60483a1bbf553da9dfc54dfd7c0962aacd8f4 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 10 Apr 2026 18:00:43 -0500 Subject: [PATCH 1/7] Implements the ability to pass cilbox script into Unity Events and directly call methods. I need a tester. Did a short test with normal test script. --- .../Shims/BasisCilboxBuildHook.cs | 322 +++++++- .../Shims/CilboxUnityEventRebinder.cs | 744 ++++++++++++++++++ .../Shims/CilboxUnityEventRebinder.cs.meta | 2 + .../Shims/CilboxUnityEventShim.cs | 156 ++++ .../Shims/CilboxUnityEventShim.cs.meta | 2 + 5 files changed, 1197 insertions(+), 29 deletions(-) create mode 100644 Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs create mode 100644 Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs.meta create mode 100644 Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs create mode 100644 Basis/Packages/com.basis.shim/Shims/CilboxUnityEventShim.cs.meta diff --git a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs index 81557c088f..66dc135e58 100644 --- a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs +++ b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs @@ -1,8 +1,8 @@ #if UNITY_EDITOR using System; using System.Collections.Generic; -using System.Threading.Tasks; using Basis.Scripts.BasisSdk; +using Basis.Shims; using Cilbox; using UnityEditor; using UnityEditor.SceneManagement; @@ -11,12 +11,125 @@ public class BasisCilboxBuildHook { + [Serializable] + private sealed class PlayModeSceneCapture + { + public string ScenePath; + public string SceneName; + public int SceneIndex; + public string CapturedCallsJson; + } + + [Serializable] + private sealed class PlayModeSceneCaptureCollection + { + public PlayModeSceneCapture[] Scenes; + } + + private const string PlayModeCaptureSessionKey = "BasisCilboxBuildHook.PlayModeSceneCaptures"; + [InitializeOnLoadMethod] private static void Initialize() { - //Debug.Log("BasisCilboxBuildHook initialized."); BasisAssetBundlePipeline.OnBeforeBuildPrefab -= HandleBeforeBuildPrefab; BasisAssetBundlePipeline.OnBeforeBuildPrefab += HandleBeforeBuildPrefab; + + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + } + + 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, + 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."); + } + + int sceneCount = SceneManager.sceneCount; + for (int sceneIndex = 0; sceneIndex < sceneCount; sceneIndex++) + { + Scene scene = SceneManager.GetSceneAt(sceneIndex); + if (!SceneNeedsCilboxProcessing(scene)) + { + continue; + } + + try + { + Dictionary cilboxAssemblySnapshot = CaptureCilboxAssemblySnapshot(); + List capturedPersistentCalls = LoadCapturedCallsForScene(collection, scene, sceneIndex); + 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) @@ -26,14 +139,15 @@ 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 temporarilyDisabledRoots = new List(); Scene temporaryScene = default; - Cilbox.Cilbox temporarySceneCilbox = null; GameObject temporaryCilboxHost = null; bool detachedFromParent = false; try @@ -50,32 +164,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 { @@ -110,6 +206,104 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun } } + 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; + } + + return JsonUtility.FromJson(json); + } + + private static List LoadCapturedCallsForScene(PlayModeSceneCaptureCollection collection, Scene scene, int sceneIndex) + { + if (collection?.Scenes == null) + { + return new List(); + } + + 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; + } + + return CilboxUnityEventRebinder.DeserializeCapturedCalls(sceneCapture.CapturedCallsJson); + } + + return new List(); + } + private static void CleanupStaleCilboxHelpers() { GameObject[] allObjects = Resources.FindObjectsOfTypeAll(); @@ -127,11 +321,22 @@ private static void CleanupStaleCilboxHelpers() continue; } - if (go.name == "CilboxDirtier" || go.name.StartsWith("CilboxAsm ")) + if (go.name == "CilboxDirtier" || go.name.StartsWith("CilboxAsm ", StringComparison.Ordinal)) { UnityEngine.Object.DestroyImmediate(go); } } + + 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.IsValid() && shim.gameObject.scene.isLoaded) + { + UnityEngine.Object.DestroyImmediate(shim); + } + } } private static bool HasCilboxableComponents(GameObject root) @@ -157,9 +362,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(); @@ -305,6 +541,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++) { @@ -347,6 +612,5 @@ private static void RestoreExternalCilboxAssemblyData(Dictionary 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) + { + string path = gameObject.name; + Transform current = gameObject.transform.parent; + while (current != null) + { + path = $"{current.name}/{path}"; + current = current.parent; + } + + return path; + } + + private static GameObject ResolveGameObject(Scene scene, string path) + { + if (!scene.IsValid() || !scene.isLoaded || string.IsNullOrEmpty(path)) + { + return null; + } + + string[] parts = path.Split('/'); + GameObject current = null; + GameObject[] roots = scene.GetRootGameObjects(); + int rootCount = roots.Length; + for (int i = 0; i < rootCount; i++) + { + GameObject root = roots[i]; + if (root != null && root.name == parts[0]) + { + current = root; + break; + } + } + + if (current == null) + { + return null; + } + + for (int partIndex = 1; partIndex < parts.Length; partIndex++) + { + Transform child = current.transform.Find(parts[partIndex]); + if (child == null) + { + return null; + } + + current = child.gameObject; + } + + return current; + } + + 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 From 5cc84035eed1b90f81233b8fb1ed97db7a18cef6 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 10 Apr 2026 18:25:49 -0500 Subject: [PATCH 2/7] Fix prefab root chaning cilbox reference. --- .../Shims/BasisCilboxBuildHook.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs index 66dc135e58..efb2ce6b3c 100644 --- a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs +++ b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs @@ -11,6 +11,12 @@ public class BasisCilboxBuildHook { + private sealed class CilboxProxyReferenceSnapshot + { + public CilboxProxy Proxy; + public Cilbox.Cilbox Box; + } + [Serializable] private sealed class PlayModeSceneCapture { @@ -146,6 +152,7 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun 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; GameObject temporaryCilboxHost = null; @@ -189,6 +196,8 @@ private static void HandleBeforeBuildPrefab(GameObject prefabRoot, BasisAssetBun prefabRoot.transform.SetSiblingIndex(siblingIndex); } + RestoreProxyReferences(proxyReferenceSnapshot); + if (temporaryCilboxHost != null) { UnityEngine.Object.DestroyImmediate(temporaryCilboxHost); @@ -612,5 +621,56 @@ 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 From a15ed1b06a4fc6af3b67aaffa71fb78e00e007ef Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 10 Apr 2026 18:30:24 -0500 Subject: [PATCH 3/7] Change from gameobject name, to sibling path. --- .../Shims/CilboxUnityEventRebinder.cs | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs index cefc9ead5f..a275b132ef 100644 --- a/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs +++ b/Basis/Packages/com.basis.shim/Shims/CilboxUnityEventRebinder.cs @@ -631,15 +631,38 @@ private static int GetComponentOrdinal(Component component) private static string GetGameObjectPath(GameObject gameObject) { - string path = gameObject.name; - Transform current = gameObject.transform.parent; - while (current != null) + if (gameObject == null) { - path = $"{current.name}/{path}"; + return string.Empty; + } + + List siblingIndices = new List(); + Transform current = gameObject.transform; + while (current.parent != null) + { + siblingIndices.Add(current.GetSiblingIndex()); current = current.parent; } - return path; + 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) @@ -649,20 +672,19 @@ private static GameObject ResolveGameObject(Scene scene, string path) return null; } - string[] parts = path.Split('/'); - GameObject current = null; GameObject[] roots = scene.GetRootGameObjects(); - int rootCount = roots.Length; - for (int i = 0; i < rootCount; i++) + 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)) { - GameObject root = roots[i]; - if (root != null && root.name == parts[0]) - { - current = root; - break; - } + return null; + } + + if (rootIndex < 0 || rootIndex >= roots.Length) + { + return null; } + Transform current = roots[rootIndex]?.transform; if (current == null) { return null; @@ -670,16 +692,19 @@ private static GameObject ResolveGameObject(Scene scene, string path) for (int partIndex = 1; partIndex < parts.Length; partIndex++) { - Transform child = current.transform.Find(parts[partIndex]); - if (child == null) + if (!int.TryParse(parts[partIndex], out int siblingIndex) || siblingIndex < 0 || siblingIndex >= current.childCount) { return null; } - current = child.gameObject; + current = current.GetChild(siblingIndex); + if (current == null) + { + return null; + } } - return current; + return current.gameObject; } private static Component ResolveComponent(Scene scene, string gameObjectPath, string componentTypeName, int componentOrdinal) From 1bcd8e7a65a2f5ac692da961dcd43be9f96f88eb Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 10 Apr 2026 18:56:27 -0500 Subject: [PATCH 4/7] Hardening on the capturedcalls from scene. --- .../Shims/BasisCilboxBuildHook.cs | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs index efb2ce6b3c..534137a37f 100644 --- a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs +++ b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs @@ -23,6 +23,7 @@ private sealed class PlayModeSceneCapture public string ScenePath; public string SceneName; public int SceneIndex; + public bool HasPersistedCapture; public string CapturedCallsJson; } @@ -32,6 +33,12 @@ private sealed class PlayModeSceneCaptureCollection public PlayModeSceneCapture[] Scenes; } + private sealed class PlayModeSceneCaptureLoadResult + { + public bool Found; + public List CapturedCalls; + } + private const string PlayModeCaptureSessionKey = "BasisCilboxBuildHook.PlayModeSceneCaptures"; [InitializeOnLoadMethod] @@ -87,6 +94,7 @@ private static void CaptureLoadedScenesForPlayMode() ScenePath = scene.path, SceneName = scene.name, SceneIndex = sceneIndex, + HasPersistedCapture = true, CapturedCallsJson = CilboxUnityEventRebinder.SerializeCapturedCalls(capturedPersistentCalls) } ); @@ -125,7 +133,16 @@ private static void ProcessLoadedScenesForPlayMode() try { Dictionary cilboxAssemblySnapshot = CaptureCilboxAssemblySnapshot(); - List capturedPersistentCalls = LoadCapturedCallsForScene(collection, scene, sceneIndex); + 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." ); @@ -280,14 +297,35 @@ private static PlayModeSceneCaptureCollection LoadPlayModeCaptures() return null; } - return JsonUtility.FromJson(json); + 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 List LoadCapturedCallsForScene(PlayModeSceneCaptureCollection collection, Scene scene, int sceneIndex) + private static PlayModeSceneCaptureLoadResult LoadCapturedCallsForScene(PlayModeSceneCaptureCollection collection, Scene scene, int sceneIndex) { + PlayModeSceneCaptureLoadResult result = new PlayModeSceneCaptureLoadResult + { + Found = false, + CapturedCalls = new List() + }; + if (collection?.Scenes == null) { - return new List(); + return result; } int captureCount = collection.Scenes.Length; @@ -307,10 +345,17 @@ private static List LoadCapturedCallsForScene(PlayModeSceneCaptureCollec continue; } - return CilboxUnityEventRebinder.DeserializeCapturedCalls(sceneCapture.CapturedCallsJson); + if (!sceneCapture.HasPersistedCapture) + { + return result; + } + + result.Found = true; + result.CapturedCalls = CilboxUnityEventRebinder.DeserializeCapturedCalls(sceneCapture.CapturedCallsJson); + return result; } - return new List(); + return result; } private static void CleanupStaleCilboxHelpers() From 685ed104ef0e9490c4bcf661a53878ac27de9210 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Fri, 10 Apr 2026 19:18:49 -0500 Subject: [PATCH 5/7] return on no work. --- Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs index 534137a37f..7301d2846c 100644 --- a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs +++ b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs @@ -119,6 +119,7 @@ private static void ProcessLoadedScenesForPlayMode() 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; @@ -375,7 +376,7 @@ private static void CleanupStaleCilboxHelpers() continue; } - if (go.name == "CilboxDirtier" || go.name.StartsWith("CilboxAsm ", StringComparison.Ordinal)) + if (string.Equals(go.name, "CilboxDirtier", StringComparison.Ordinal) || go.name.StartsWith("CilboxAsm ", StringComparison.Ordinal)) { UnityEngine.Object.DestroyImmediate(go); } From 5aa66fdad9ce421e9dec660e7213d5626d74ff17 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 16 Apr 2026 20:30:25 -0500 Subject: [PATCH 6/7] Add to Content Police. --- .../com.basis.sdk/Settings/AvatarContentPoliceSelector.asset | 1 + .../com.basis.sdk/Settings/PropContentPoliceSelector.asset | 1 + .../com.basis.sdk/Settings/SceneContentPoliceSelector.asset | 1 + 3 files changed, 3 insertions(+) diff --git a/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset b/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset index 5cec2383db..8c4a4d5946 100644 --- a/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset +++ b/Basis/Packages/com.basis.sdk/Settings/AvatarContentPoliceSelector.asset @@ -193,3 +193,4 @@ MonoBehaviour: - Cilbox.CilboxAvatarBasis - Basis.Shims.BasisAvatarShim - Basis.Shims.BasisNetworkShim + - Basis.Shims.CilboxUnityEventShim diff --git a/Basis/Packages/com.basis.sdk/Settings/PropContentPoliceSelector.asset b/Basis/Packages/com.basis.sdk/Settings/PropContentPoliceSelector.asset index c5726eb926..ffbd7f13c9 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 - 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 347a8156f2..f8011f845a 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 From 8d478d29e40fa977a8823b721430059efeeed0b7 Mon Sep 17 00:00:00 2001 From: Toys0125 Date: Thu, 16 Apr 2026 20:31:45 -0500 Subject: [PATCH 7/7] Add proper scene building. Uses the uniqueID for the scenePath. --- .../BasisAssetBundlePipeline.cs | 13 ++- .../BasisTemporaryStorageHandler.cs | 10 +-- .../Shims/BasisCilboxBuildHook.cs | 87 +++++++++++++++++-- 3 files changed, 96 insertions(+), 14 deletions(-) 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 03ed5eb828..06e5b371fc 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.shim/Shims/BasisCilboxBuildHook.cs b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs index 9ac69e9d29..7ca1021e6c 100644 --- a/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs +++ b/Basis/Packages/com.basis.shim/Shims/BasisCilboxBuildHook.cs @@ -1,6 +1,7 @@ #if UNITY_EDITOR using System; using System.Collections.Generic; +using System.IO; using Basis.Scripts.BasisSdk; using Basis.Shims; using Cilbox; @@ -46,6 +47,8 @@ private static void Initialize() { BasisAssetBundlePipeline.OnBeforeBuildPrefab -= HandleBeforeBuildPrefab; BasisAssetBundlePipeline.OnBeforeBuildPrefab += HandleBeforeBuildPrefab; + BasisAssetBundlePipeline.OnPrepareBuildSceneAssetPath -= PrepareSceneAssetPathForBuild; + BasisAssetBundlePipeline.OnPrepareBuildSceneAssetPath += PrepareSceneAssetPathForBuild; BasisAvatarSDKInspector.OnBeforeTestInEditor -= HandleBeforeTestInEditor; BasisAvatarSDKInspector.OnBeforeTestInEditor += HandleBeforeTestInEditor; @@ -240,6 +243,67 @@ 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, @@ -368,17 +432,26 @@ private static PlayModeSceneCaptureLoadResult LoadCapturedCallsForScene(PlayMode 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) - { - continue; - } - - if (!go.scene.IsValid() || !go.scene.isLoaded) + if (go == null || go.scene != scene) { continue; } @@ -394,7 +467,7 @@ private static void CleanupStaleCilboxHelpers() for (int i = 0; i < shimCount; i++) { CilboxUnityEventShim shim = staleShims[i]; - if (shim != null && shim.gameObject.scene.IsValid() && shim.gameObject.scene.isLoaded) + if (shim != null && shim.gameObject.scene == scene) { UnityEngine.Object.DestroyImmediate(shim); }