diff --git a/.github/scripts/unitypackagegen.sh b/.github/scripts/unitypackagegen.sh index 8d437c0602..85d4b9322d 100755 --- a/.github/scripts/unitypackagegen.sh +++ b/.github/scripts/unitypackagegen.sh @@ -7,6 +7,7 @@ PACKAGES="Packages/org.basisvr.generator.equals-3.2.0.tgz: Packages/org.basisvr.simplebase-4.0.2.tgz: Packages/org.basisvr.bouncycastle-2.5.0.tgz" SUBFOLDERS="Packages/com.basis.sdk: + Packages/com.basis.openlipsync: Packages/UnityJigglePhysics-upm: Packages/org.basisvr.k4os.compression.lz4: Packages/com.basis.bundlemanagement: @@ -24,15 +25,16 @@ if [[ "$1" == "full" ]]; then PACKAGES+=":Packages/com.valvesoftware.unity.openvr-1.2.1.tgz" - # Need this for framework (But only framework) + # Framework + client-only additions on top of the SDK base. SUBFOLDERS+=":Packages/com.avionblock.opussharp: + Packages/com.basis.addon.snapcontrols: Packages/com.basis.common: Packages/com.basis.eventdriver: Packages/com.basis.examples: Packages/com.basis.framework: Packages/com.basis.framework.editor: Packages/com.basis.gizmos: - Packages/com.basis.openlipsync: + Packages/com.basis.integration.audiolink: Packages/com.basis.openvr: Packages/com.basis.openxr: Packages/com.basis.profilerintergration: @@ -41,21 +43,17 @@ if [[ "$1" == "full" ]]; then Packages/com.basis.textmeshpro: Packages/com.basis.vehicles: Packages/com.basis.visualtrackers: - Packages/com.basis.zeromessenger: Packages/com.cnlohr.cilbox: Packages/com.cqf.urpvolumetricfog: - Packages/com.hecomi.ulipsync: Packages/com.llealloo.audiolink: Packages/com.steam.steamaudio: Packages/com.steam.steamvr: Packages/com.unity.3rdpersondemo: - Packages/com.unity.render-pipelines.core: Packages/com.unity.render-pipelines.universal: - Packages/com.unity.xr.openxr: Packages/com.xiph.rnnoise: Packages/dev.hai-vr.basis.comms: - Packages/HVRBasisNDMF: Packages/nuget.meamod.dns: + Packages/org.basisvr.k4os.compression.lz4: Assets/AddressableAssetsData: Assets/Basis: Assets/MetaXR: @@ -72,14 +70,24 @@ if [[ "$1" == "full" ]]; then elif [[ "$1" == "sdk" ]]; then echo "Producing SDK package" # All things are already included. + else - echo "Only full and sdk targets are specified." + echo "Only full, sdk, and creator targets are specified." die exit fi set -e +# Fail loudly if a listed Packages/ path doesn't exist +for ddv in $(echo "$SUBFOLDERS" | tr : '\n'); do + ddv_trimmed=$(echo "$ddv" | tr -d '[:space:]') + if [[ -n "$ddv_trimmed" && "$ddv_trimmed" == Packages/* && ! -d "Basis/$ddv_trimmed" ]]; then + echo "ERROR: SUBFOLDERS lists '$ddv_trimmed' but Basis/$ddv_trimmed does not exist." >&2 + exit 1 + fi +done + cd Basis rm -rf generate_unitypackage diff --git a/.github/vpm-packages.txt b/.github/vpm-packages.txt index 9f6700ac08..711f90fc2a 100644 --- a/.github/vpm-packages.txt +++ b/.github/vpm-packages.txt @@ -37,6 +37,7 @@ com.llealloo.audiolink com.xiph.rnnoise com.cnlohr.cilbox dev.hai-vr.basis.comms +dev.hai-vr.basis.ndmf # VR / spatial-audio (Apache 2.0 / BSD-3). com.steam.steamaudio diff --git a/Basis/.gitignore b/Basis/.gitignore index 0c7a594e00..47c0e70337 100644 --- a/Basis/.gitignore +++ b/Basis/.gitignore @@ -106,6 +106,8 @@ mono_crash.mem.*.*.blob [Aa]ssets/_[Uu]ser[Cc]ontent.meta [Aa]ssets/[Gg]it[Ii]gnore/ [Aa]ssets/[Gg]it[Ii]gnore.meta +[Aa]ssets/_[Bb]asis[Tt]emp/ +[Aa]ssets/_[Bb]asis[Tt]emp.meta # FFmpeg /[Aa]ssets/[Ss]treamingAssets/[Ff][Ff]mpeg/ diff --git a/Basis/Packages/com.basis.eventdriver/BasisEventDriver.asmdef b/Basis/Packages/com.basis.eventdriver/BasisEventDriver.asmdef index 3cde6efd7c..27731eee32 100644 --- a/Basis/Packages/com.basis.eventdriver/BasisEventDriver.asmdef +++ b/Basis/Packages/com.basis.eventdriver/BasisEventDriver.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "", "references": [ "BasisConsoleDisplay", + "BasisSDK", "Basis Framework", "Unity.InputSystem", "com.gator-dragon-games.jigglephysics", diff --git a/Basis/Packages/com.basis.integration.audiolink/Runtime/Basis.Integration.AudioLink.asmdef b/Basis/Packages/com.basis.integration.audiolink/Runtime/Basis.Integration.AudioLink.asmdef index ee327a61a9..a40da93a80 100644 --- a/Basis/Packages/com.basis.integration.audiolink/Runtime/Basis.Integration.AudioLink.asmdef +++ b/Basis/Packages/com.basis.integration.audiolink/Runtime/Basis.Integration.AudioLink.asmdef @@ -2,6 +2,7 @@ "name": "Basis.Integration.AudioLink", "rootNamespace": "Basis.Integration.AudioLink", "references": [ + "BasisSDK", "Basis Framework", "AudioLink" ], diff --git a/Basis/Packages/com.basis.sdk/BasisSDK.asmdef b/Basis/Packages/com.basis.sdk/BasisSDK.asmdef index e210c2e329..4f2538143f 100644 --- a/Basis/Packages/com.basis.sdk/BasisSDK.asmdef +++ b/Basis/Packages/com.basis.sdk/BasisSDK.asmdef @@ -6,7 +6,8 @@ "BasisDebug", "Unity.TextMeshPro", "Unity.Addressables", - "Unity.ResourceManager" + "Unity.ResourceManager", + "Basis.OpenLipSync.Runtime" ], "includePlatforms": [], "excludePlatforms": [], @@ -15,6 +16,12 @@ "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.basis.framework", + "expression": "", + "define": "BASIS_FRAMEWORK_EXISTS" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/Basis/Packages/com.basis.sdk/Scripts/Avatar.meta b/Basis/Packages/com.basis.sdk/Scripts/Avatar.meta new file mode 100644 index 0000000000..922795e68e --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Avatar.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9eaf2bdb0c49482f9d21a64db55ea32f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.framework/Avatar/Animator.meta b/Basis/Packages/com.basis.sdk/Scripts/Avatar/Animator.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Avatar/Animator.meta rename to Basis/Packages/com.basis.sdk/Scripts/Avatar/Animator.meta diff --git a/Basis/Packages/com.basis.framework/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs b/Basis/Packages/com.basis.sdk/Scripts/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs similarity index 100% rename from Basis/Packages/com.basis.framework/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs rename to Basis/Packages/com.basis.sdk/Scripts/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs diff --git a/Basis/Packages/com.basis.framework/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs.meta rename to Basis/Packages/com.basis.sdk/Scripts/Avatar/Animator/BasisPreviousAndCurrentAnimatorData.cs.meta diff --git a/Basis/Packages/com.basis.framework/Avatar/BasisAvatarRecorder.cs b/Basis/Packages/com.basis.sdk/Scripts/Avatar/BasisAvatarRecorder.cs similarity index 100% rename from Basis/Packages/com.basis.framework/Avatar/BasisAvatarRecorder.cs rename to Basis/Packages/com.basis.sdk/Scripts/Avatar/BasisAvatarRecorder.cs diff --git a/Basis/Packages/com.basis.framework/Avatar/BasisAvatarRecorder.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Avatar/BasisAvatarRecorder.cs.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Avatar/BasisAvatarRecorder.cs.meta rename to Basis/Packages/com.basis.sdk/Scripts/Avatar/BasisAvatarRecorder.cs.meta diff --git a/Basis/Packages/com.basis.sdk/Scripts/Drivers.meta b/Basis/Packages/com.basis.sdk/Scripts/Drivers.meta new file mode 100644 index 0000000000..b3f76e61d9 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Drivers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6aee75d491d047a1a39b929f13e7ddf1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common.meta b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common.meta new file mode 100644 index 0000000000..9d31fd89d8 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e7008f1d087b4c0ea7061e245f8ff23b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.framework/Drivers/Common/BasisAudioAndVisemeDriver.cs b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisAudioAndVisemeDriver.cs similarity index 97% rename from Basis/Packages/com.basis.framework/Drivers/Common/BasisAudioAndVisemeDriver.cs rename to Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisAudioAndVisemeDriver.cs index 4634d4023a..f108ec950b 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Common/BasisAudioAndVisemeDriver.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisAudioAndVisemeDriver.cs @@ -268,10 +268,12 @@ public void TryShutdown() public volatile bool InVisemeRange = true; // Slot in BasisRemoteAudioDriver.Drivers; -1 when not registered. - [System.NonSerialized] internal int RegisteredIndex = -1; + // Public for cross-asmdef access from Framework's BasisRemoteAudioDriver only — do not mutate externally. + [System.NonSerialized] public int RegisteredIndex = -1; // Slot in BasisRemoteAudioDriver.ActiveDrivers; -1 when out of range. - [System.NonSerialized] internal int ActiveIndex = -1; + // Public for cross-asmdef access from Framework's BasisRemoteAudioDriver only — do not mutate externally. + [System.NonSerialized] public int ActiveIndex = -1; /// /// Callback that updates whether viseme processing is active based on face visibility. diff --git a/Basis/Packages/com.basis.framework/Drivers/Common/BasisAudioAndVisemeDriver.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisAudioAndVisemeDriver.cs.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Drivers/Common/BasisAudioAndVisemeDriver.cs.meta rename to Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisAudioAndVisemeDriver.cs.meta diff --git a/Basis/Packages/com.basis.framework/Drivers/Common/BasisOpenLipSyncContext.cs b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisOpenLipSyncContext.cs similarity index 100% rename from Basis/Packages/com.basis.framework/Drivers/Common/BasisOpenLipSyncContext.cs rename to Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisOpenLipSyncContext.cs diff --git a/Basis/Packages/com.basis.framework/Drivers/Common/BasisOpenLipSyncContext.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisOpenLipSyncContext.cs.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Drivers/Common/BasisOpenLipSyncContext.cs.meta rename to Basis/Packages/com.basis.sdk/Scripts/Drivers/Common/BasisOpenLipSyncContext.cs.meta diff --git a/Basis/Packages/com.basis.sdk/Scripts/Drivers/Local.meta b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Local.meta new file mode 100644 index 0000000000..06b84cefce --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Local.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 959a90b3fcf14c23acd300ff68e5fdd6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalFacialBlinkDriver.cs b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Local/BasisLocalFacialBlinkDriver.cs similarity index 100% rename from Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalFacialBlinkDriver.cs rename to Basis/Packages/com.basis.sdk/Scripts/Drivers/Local/BasisLocalFacialBlinkDriver.cs diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalFacialBlinkDriver.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Drivers/Local/BasisLocalFacialBlinkDriver.cs.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalFacialBlinkDriver.cs.meta rename to Basis/Packages/com.basis.sdk/Scripts/Drivers/Local/BasisLocalFacialBlinkDriver.cs.meta diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAutomaticSetupAvatarEditor.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAutomaticSetupAvatarEditor.cs index 0cf675047f..647fcf2d8e 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAutomaticSetupAvatarEditor.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAutomaticSetupAvatarEditor.cs @@ -13,32 +13,29 @@ public static class BasisAutomaticSetupAvatarEditor public static void TryToAutomatic(BasisAvatarSDKInspector Inspector) { BasisAvatar avatar = Inspector.Avatar; - if (avatar != null) + // Inspector can redraw with a destroyed/cleared target during play-mode transitions + // and asset reloads. Bail silently — it's not an error worth logging on every redraw. + if (avatar == null) return; + + if (TryFindOrCheckAvatar(avatar)) { - if (TryFindOrCheckAvatar(avatar)) + if (CheckAnimator(avatar)) { - if (CheckAnimator(avatar)) + if (TryFindNeckAndHead(avatar, out Transform Neck, out Transform Head)) { - if (TryFindNeckAndHead(avatar, out Transform Neck, out Transform Head)) - { - TrySetAvatarEyePosition(avatar); - TrySetAvatarMouthPosition(avatar, Head); - } + TrySetAvatarEyePosition(avatar); + TrySetAvatarMouthPosition(avatar, Head); } } - else - { - Debug.LogError("Animator component not found on GameObject " + avatar.gameObject.name); - } - UpdateAvatarRenders(avatar); - - EditorUtility.SetDirty(avatar); - AssetDatabase.Refresh(); } else { - Debug.LogError("Avatar instance is null."); + Debug.LogError("Animator component not found on GameObject " + avatar.gameObject.name); } + UpdateAvatarRenders(avatar); + + EditorUtility.SetDirty(avatar); + AssetDatabase.Refresh(); } private static bool CheckAnimator(BasisAvatar avatar) { diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAvatarEditorPreviewBootstrap.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAvatarEditorPreviewBootstrap.cs new file mode 100644 index 0000000000..e2cdd0b9ea --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAvatarEditorPreviewBootstrap.cs @@ -0,0 +1,15 @@ +#if !BASIS_FRAMEWORK_EXISTS +using UnityEditor; + +namespace Basis.Scripts.BasisSdk.Players.Editor +{ + internal static class BasisAvatarEditorPreviewBootstrap + { + [InitializeOnLoadMethod] + private static void Register() + { + BasisAvatarSDKInspector.PreviewFoldoutFactory = BasisEditorPreviewParametersFoldout.Create; + } + } +} +#endif diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAvatarEditorPreviewBootstrap.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAvatarEditorPreviewBootstrap.cs.meta new file mode 100644 index 0000000000..9b00eb5d4e --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisAvatarEditorPreviewBootstrap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d4a957b49cc4485e81ced9dba1d0ecaa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewLocalPlayer.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewLocalPlayer.cs similarity index 59% rename from Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewLocalPlayer.cs rename to Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewLocalPlayer.cs index 663cb871ad..673ed2512b 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewLocalPlayer.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewLocalPlayer.cs @@ -6,9 +6,8 @@ namespace Basis.Scripts.BasisSdk.Players.Editor { - // Avatar preview for projects without com.basis.framework. Compiled out when framework - // is installed (gated via versionDefine BASIS_FRAMEWORK_EXISTS) — framework's real - // BasisLocalPlayer registers itself with BasisLocalPlayerData at play-mode init. + // SDK-side avatar preview. Backs off when framework's BasisLocalPlayer has + // already registered with BasisLocalPlayerData (play mode). internal sealed class BasisEditorPreviewLocalPlayer : IBasisLocalPlayer { private const string FallbackControllerPath = "Packages/com.basis.sdk/Animator/BasisLocomotion.controller"; @@ -16,8 +15,10 @@ internal sealed class BasisEditorPreviewLocalPlayer : IBasisLocalPlayer private static readonly BasisEditorPreviewLocalPlayer _instance = new BasisEditorPreviewLocalPlayer(); - public static BasisAvatar ActiveAvatar { get; private set; } - public static BasisAnimatorVariableApply Applier { get; private set; } + public static BasisAvatar ActiveAvatar; + public static BasisAnimatorVariableApply Applier; + public static BasisAvatarSimPlayer ActiveSimPlayer; + public static BasisAvatarSimMic ActiveSimMic; public static event System.Action StateChanged; private static GameObject _previewRoot; @@ -25,10 +26,32 @@ internal sealed class BasisEditorPreviewLocalPlayer : IBasisLocalPlayer [InitializeOnLoadMethod] private static void Register() { + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + + if (BasisLocalPlayerData.Instance != null) return; BasisLocalPlayerData.Instance = _instance; BasisLocalPlayerData.RaiseLocalPlayerInitialized(); } + private static void OnPlayModeStateChanged(PlayModeStateChange state) + { + // The preview is created during play mode (via the inspector's Test-in-Editor flow, + // which enters play mode and resolves the pending avatar from SessionState). Unity's + // play-mode revert destroys the preview root + clone automatically — we just need + // to drop our static refs so they don't dangle into the next session. + if (state == PlayModeStateChange.ExitingPlayMode) + { + if (ActiveSimMic != null) ActiveSimMic.StopCapture(); + _previewRoot = null; + ActiveAvatar = null; + Applier = null; + ActiveSimPlayer = null; + ActiveSimMic = null; + StateChanged?.Invoke(); + } + } + public Task CreateAvatarFromMode(BasisLoadMode LoadMode, BasisLoadableBundle BasisLoadableBundle) { if (LoadMode != BasisLoadMode.ByGameobjectReference) @@ -46,7 +69,7 @@ public Task CreateAvatarFromMode(BasisLoadMode LoadMode, BasisLoadableBundle Bas DisposeActive(); - _previewRoot = new GameObject(PreviewRootName) { hideFlags = HideFlags.DontSave }; + _previewRoot = new GameObject(PreviewRootName); inSceneItem.transform.SetParent(_previewRoot.transform, worldPositionStays: false); inSceneItem.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity); @@ -72,16 +95,28 @@ public Task CreateAvatarFromMode(BasisLoadMode LoadMode, BasisLoadableBundle Bas ActiveAvatar = avatar; avatar.NotifyAvatarReady(true); + + // SimPlayer + SimMic provide play-mode ticking for blink and visemes. + // The MonoBehaviours only tick in play mode; in edit mode they're inert. + ActiveSimPlayer = _previewRoot.AddComponent(); + ActiveSimPlayer.Bootstrap(avatar); + + ActiveSimMic = _previewRoot.AddComponent(); + ActiveSimMic.Driver = ActiveSimPlayer.Viseme; + StateChanged?.Invoke(); return Task.CompletedTask; } public static void DisposeActive() { + if (ActiveSimMic != null) ActiveSimMic.StopCapture(); if (_previewRoot != null) Object.DestroyImmediate(_previewRoot); _previewRoot = null; ActiveAvatar = null; Applier = null; + ActiveSimPlayer = null; + ActiveSimMic = null; StateChanged?.Invoke(); } diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewLocalPlayer.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewLocalPlayer.cs.meta new file mode 100644 index 0000000000..913611bc25 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewLocalPlayer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bdc48551ca8324e4a98c25e5cf6c4af9 \ No newline at end of file diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewParametersFoldout.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewParametersFoldout.cs similarity index 57% rename from Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewParametersFoldout.cs rename to Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewParametersFoldout.cs index 4bbd122611..9c1ebb6ec5 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewParametersFoldout.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewParametersFoldout.cs @@ -6,9 +6,10 @@ namespace Basis.Scripts.BasisSdk.Players.Editor { // Drives the live preview avatar's animator parameters from an inspector foldout. - // Mounted by BasisAvatarSDKInspector when the SDK preview is the active driver - // (framework absent). Generic over Animator.parameters — works for the locomotion - // hashes (VelocityX/VelocityZ/IsCrouched/...) and any custom FX-layer params alike. + // Mounted into BasisAvatarSDKInspector via PreviewFoldoutFactory delegate registered + // by BasisAvatarEditorPreviewBootstrap at editor load. Generic over Animator.parameters + // — works for the locomotion hashes (VelocityX/VelocityZ/IsCrouched/...) and any + // custom FX-layer params alike. internal static class BasisEditorPreviewParametersFoldout { public static VisualElement Create() @@ -29,6 +30,8 @@ public static VisualElement Create() return root; } + private static int _selectedMicIndex; + private static void Draw() { var avatar = BasisEditorPreviewLocalPlayer.ActiveAvatar; @@ -40,10 +43,14 @@ private static void Draw() if (GUILayout.Button("Stop Preview")) { - BasisEditorPreviewLocalPlayer.DisposeActive(); + // Exiting play mode is the cleanup. Unity reverts the play-mode scene, + // which destroys the preview root + clone; OnPlayModeStateChanged clears statics. + EditorApplication.ExitPlaymode(); return; } + DrawMicSection(); + Animator anim = avatar.Animator; foreach (AnimatorControllerParameter p in anim.parameters) { @@ -80,6 +87,48 @@ private static void Draw() } } } + + private static void DrawMicSection() + { + var mic = BasisEditorPreviewLocalPlayer.ActiveSimMic; + if (mic == null) return; + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("Microphone", EditorStyles.boldLabel); + + string[] devices = Microphone.devices; + if (devices.Length == 0) + { + EditorGUILayout.HelpBox("No microphones detected.", MessageType.Info); + return; + } + + // Keep the dropdown selection in sync with whatever the mic is currently using + if (mic.IsCapturing && !string.IsNullOrEmpty(mic.DeviceName)) + { + int idx = System.Array.IndexOf(devices, mic.DeviceName); + if (idx >= 0) _selectedMicIndex = idx; + } + _selectedMicIndex = Mathf.Clamp(_selectedMicIndex, 0, devices.Length - 1); + + using (new EditorGUI.DisabledScope(mic.IsCapturing)) + { + _selectedMicIndex = EditorGUILayout.Popup("Device", _selectedMicIndex, devices); + } + + if (mic.IsCapturing) + { + if (GUILayout.Button("Stop Mic")) mic.StopCapture(); + } + else + { + if (GUILayout.Button("Start Mic")) + { + if (!mic.StartCapture(devices[_selectedMicIndex])) + Debug.LogWarning("[Basis SDK Preview] Failed to start mic capture."); + } + } + } } } #endif diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewParametersFoldout.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewParametersFoldout.cs.meta new file mode 100644 index 0000000000..002db13c19 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/BasisEditorPreviewParametersFoldout.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 381583f845a005747a6cedc41625c7cb \ No newline at end of file diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players.meta b/Basis/Packages/com.basis.sdk/Scripts/Editor/Players.meta deleted file mode 100644 index 93b5dcdfed..0000000000 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7b82c20af2150374aa69fc5ffccab2a3 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewLocalPlayer.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewLocalPlayer.cs.meta deleted file mode 100644 index d487765f85..0000000000 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewLocalPlayer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 52797d3e87169014cb7c262de9a87bcc \ No newline at end of file diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewParametersFoldout.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewParametersFoldout.cs.meta deleted file mode 100644 index 1ca03aba44..0000000000 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/Players/BasisEditorPreviewParametersFoldout.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 87df41ce57a93e3478768be9d3eedcc2 \ No newline at end of file diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.AvatarBehaviours.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.AvatarBehaviours.cs index 24e0e8010e..13a56b8e1c 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.AvatarBehaviours.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.AvatarBehaviours.cs @@ -54,6 +54,10 @@ private static bool IsVisibleInAvatarMenu(Type type) private void RefreshNetworkBehaviours() { + // Inspector can redraw during play-mode transitions when the target avatar has + // been destroyed (Unity reroutes selection slightly later). Bail rather than throw. + if (Avatar == null) return; + _attachedContainer.Clear(); _availableContainer.Clear(); diff --git a/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.cs b/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.cs index f49f04088d..75408c9345 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Editor/SDKInspector/BasisAvatarSDKInspector.cs @@ -15,6 +15,8 @@ public partial class BasisAvatarSDKInspector : Editor { private const string PendingTestInEditorAvatarIdSessionKey = "BasisAvatarSDKInspector.PendingTestInEditorAvatarId"; + private const string TempPrefabDirectory = "Assets/" + TempPrefabFolderName; + private const string TempPrefabFolderName = "_BasisTemp"; public delegate void BeforeTestInEditorHandler(GameObject clone); public static BeforeTestInEditorHandler OnBeforeTestInEditor; @@ -23,6 +25,7 @@ public partial class BasisAvatarSDKInspector : Editor public static event Action InspectorGuiCreated; public static event Action ButtonClicked; public static event Action ValueChanged; + public static Func PreviewFoldoutFactory; public VisualTreeAsset visualTree; public BasisAvatar Avatar; public VisualElement uiElementsRoot; @@ -40,49 +43,66 @@ private static void InitializeTestInEditorHooks() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + + // Clean up temp prefabs left over from a previous editor session. + if (string.IsNullOrEmpty(GetPendingTestInEditorAvatarId())) + { + CleanupTempPrefabDirectory(); + } } private static void OnPlayModeStateChanged(PlayModeStateChange state) { - if (state != PlayModeStateChange.EnteredPlayMode || !HasPendingTestInEditorAvatarId()) + if (state == PlayModeStateChange.EnteredPlayMode) { - return; + TryExecutePendingTestInEditor(); + } + else if (state == PlayModeStateChange.EnteredEditMode) + { + CleanupTempPrefabDirectory(); } - - EditorApplication.delayCall -= TryExecutePendingTestInEditor; - EditorApplication.delayCall += TryExecutePendingTestInEditor; } - private static void TryExecutePendingTestInEditor() + private static string CreateTempPrefabFromSceneAvatar(BasisAvatar avatar) { - string pendingAvatarId = GetPendingTestInEditorAvatarId(); - if (string.IsNullOrEmpty(pendingAvatarId)) + if (avatar == null) return null; + if (!AssetDatabase.IsValidFolder(TempPrefabDirectory)) { - return; + AssetDatabase.CreateFolder("Assets", TempPrefabFolderName); } - if (!GlobalObjectId.TryParse(pendingAvatarId, out GlobalObjectId avatarId)) + string path = AssetDatabase.GenerateUniqueAssetPath( + $"{TempPrefabDirectory}/{avatar.gameObject.name}.prefab"); + // SaveAsPrefabAsset (no Connect) leaves the original scene GameObject untouched. + var prefab = PrefabUtility.SaveAsPrefabAsset(avatar.gameObject, path); + return prefab != null ? path : null; + } + + private static void CleanupTempPrefabDirectory() + { + if (AssetDatabase.IsValidFolder(TempPrefabDirectory)) { - ClearPendingTestInEditorAvatarId(); - return; + AssetDatabase.DeleteAsset(TempPrefabDirectory); } + } - BasisAvatar avatar = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(avatarId) as BasisAvatar; + private static void TryExecutePendingTestInEditor() + { + string assetPath = GetPendingTestInEditorAvatarId(); + if (string.IsNullOrEmpty(assetPath)) return; ClearPendingTestInEditorAvatarId(); + + var go = AssetDatabase.LoadAssetAtPath(assetPath); + BasisAvatar avatar = go != null ? go.GetComponent() : null; if (avatar == null) { - BasisDebug.LogError("Unable to resolve the pending avatar for Test In Editor.", BasisDebug.LogTag.Editor); + BasisDebug.LogError($"Unable to resolve the pending avatar at '{assetPath}' for Test In Editor.", BasisDebug.LogTag.Editor); return; } RequestAvatarLoad(avatar); } - private static bool HasPendingTestInEditorAvatarId() - { - return SessionState.GetBool(PendingTestInEditorAvatarIdSessionKey + ".Exists", false); - } - private static string GetPendingTestInEditorAvatarId() { return SessionState.GetString(PendingTestInEditorAvatarIdSessionKey, string.Empty); @@ -138,9 +158,8 @@ public override VisualElement CreateInspectorGUI() SetupItems(); AvatarSDKVisemes.Initialize(this); SetupNetworkBehaviours(); -#if !BASIS_FRAMEWORK_EXISTS - rootElement.Add(Basis.Scripts.BasisSdk.Players.Editor.BasisEditorPreviewParametersFoldout.Create()); -#endif + var previewFoldout = PreviewFoldoutFactory?.Invoke(); + if (previewFoldout != null) rootElement.Add(previewFoldout); InspectorGuiCreated?.Invoke(this); } else @@ -624,28 +643,59 @@ public void GenerateMeshLODs(int lodLimit = -1) #endif public void AvatarTestInEditorClickFunction() { -#if BASIS_FRAMEWORK_EXISTS if (!Application.isPlaying) { + // Avatar in PrefabStage gets destroyed when EnterPlaymode auto-closes + // the stage. Bail rather than try to map back to the asset. + var stage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); + if (stage != null && Avatar != null && Avatar.gameObject.scene == stage.scene) + { + EditorUtility.DisplayDialog( + "Test In Editor", + "Close the prefab edit mode and select the avatar from the Project window or a scene before testing.", + BasisEditorLocalization.Get("sdk.common.dialog.yes")); + return; + } + + if (Avatar == null) return; + bool result = EditorUtility.DisplayDialog( BasisEditorLocalization.Get("sdk.common.dialog.confirm"), BasisEditorLocalization.Get("sdk.avatar.testInEditor.confirm.body"), BasisEditorLocalization.Get("sdk.common.dialog.yes"), BasisEditorLocalization.Get("sdk.common.dialog.no")); - if (result) + if (!result) return; + + // Defer the temp-prefab write until after the confirm dialog so a + // cancelled flow doesn't leave orphan files on disk. + string assetPath = ResolveAvatarAssetPath(Avatar) ?? CreateTempPrefabFromSceneAvatar(Avatar); + if (string.IsNullOrEmpty(assetPath)) { - SetPendingTestInEditorAvatarId(GlobalObjectId.GetGlobalObjectIdSlow(Avatar).ToString()); - EditorApplication.EnterPlaymode(); + EditorUtility.DisplayDialog( + "Test In Editor", + "Couldn't resolve the avatar to an asset or write a temporary prefab. Save it as a prefab manually and try again.", + BasisEditorLocalization.Get("sdk.common.dialog.yes")); + return; } + + SetPendingTestInEditorAvatarId(assetPath); + EditorApplication.EnterPlaymode(); } else { RequestAvatarLoad(); } -#else - // SDK preview runs in either edit or play mode — no play-mode prompt needed. - RequestAvatarLoad(); -#endif + } + + // Resolves the inspector target to a project-relative asset path. Returns + // null when the avatar lives only in a scene with no prefab backing. + private static string ResolveAvatarAssetPath(BasisAvatar avatar) + { + if (avatar == null) return null; + string path = AssetDatabase.GetAssetPath(avatar); + if (!string.IsNullOrEmpty(path)) return path; + var source = PrefabUtility.GetCorrespondingObjectFromSource(avatar); + return source != null ? AssetDatabase.GetAssetPath(source) : null; } public void RequestAvatarLoad() { diff --git a/Basis/Packages/com.basis.sdk/Scripts/Players/BasisLocalPlayerData.cs b/Basis/Packages/com.basis.sdk/Scripts/Players/BasisLocalPlayerData.cs index b04365f0af..ffd88274a5 100644 --- a/Basis/Packages/com.basis.sdk/Scripts/Players/BasisLocalPlayerData.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Players/BasisLocalPlayerData.cs @@ -9,8 +9,9 @@ public interface IBasisLocalPlayer } // SDK-side local player data. Framework's BasisLocalPlayer writes Instance - // when present; otherwise the SDK editor preview writes a stand-in (gated - // by BASIS_FRAMEWORK_EXISTS so only one writer ever runs). + // when present; otherwise the SDK's editor preview writes a stand-in + // (the preview only registers when Instance is still null, so only one writer + // ever wins). public static class BasisLocalPlayerData { public static IBasisLocalPlayer Instance; diff --git a/Basis/Packages/com.basis.framework/Players/Common/BasisPlayer.cs b/Basis/Packages/com.basis.sdk/Scripts/Players/BasisPlayer.cs similarity index 99% rename from Basis/Packages/com.basis.framework/Players/Common/BasisPlayer.cs rename to Basis/Packages/com.basis.sdk/Scripts/Players/BasisPlayer.cs index 3f1eaa6b76..71f3d18bb2 100644 --- a/Basis/Packages/com.basis.framework/Players/Common/BasisPlayer.cs +++ b/Basis/Packages/com.basis.sdk/Scripts/Players/BasisPlayer.cs @@ -1,4 +1,3 @@ -using Basis.Scripts.Drivers; using System; using System.Text.RegularExpressions; using UnityEngine; diff --git a/Basis/Packages/com.basis.framework/Players/Common/BasisPlayer.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Players/BasisPlayer.cs.meta similarity index 100% rename from Basis/Packages/com.basis.framework/Players/Common/BasisPlayer.cs.meta rename to Basis/Packages/com.basis.sdk/Scripts/Players/BasisPlayer.cs.meta diff --git a/Basis/Packages/com.basis.sdk/Scripts/Sim.meta b/Basis/Packages/com.basis.sdk/Scripts/Sim.meta new file mode 100644 index 0000000000..115d34efba --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Sim.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b64301a783814a909a23d72d5228360f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimMic.cs b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimMic.cs new file mode 100644 index 0000000000..2dad6a56f5 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimMic.cs @@ -0,0 +1,60 @@ +#if !BASIS_FRAMEWORK_EXISTS +using Basis.Scripts.Drivers; +using UnityEngine; + +namespace Basis.Scripts.BasisSdk.Players +{ + // Mic capture for the editor preview. Routes raw PCM samples from the OS + // mic to a BasisAudioAndVisemeDriver via OnAudioFilterRead. AudioSource + // volume stays at 0 so the mic doesn't echo to speakers. + [RequireComponent(typeof(AudioSource))] + public sealed class BasisAvatarSimMic : MonoBehaviour + { + public BasisAudioAndVisemeDriver Driver; + public string DeviceName; + public bool IsCapturing { get; private set; } + + private AudioSource _source; + private AudioClip _clip; + private const int ClipLengthSeconds = 1; + + public bool StartCapture(string device) + { + if (IsCapturing) StopCapture(); + if (Microphone.devices.Length == 0) return false; + + DeviceName = string.IsNullOrEmpty(device) ? Microphone.devices[0] : device; + + Microphone.GetDeviceCaps(DeviceName, out int minFreq, out int maxFreq); + int sampleRate = maxFreq > 0 ? maxFreq : AudioSettings.outputSampleRate; + + _clip = Microphone.Start(DeviceName, true, ClipLengthSeconds, sampleRate); + if (_clip == null) return false; + + _source = GetComponent(); + _source.clip = _clip; + _source.loop = true; + _source.volume = 0f; + _source.Play(); + IsCapturing = true; + return true; + } + + public void StopCapture() + { + if (!IsCapturing) return; + if (!string.IsNullOrEmpty(DeviceName)) Microphone.End(DeviceName); + if (_source != null) _source.Stop(); + _clip = null; + IsCapturing = false; + } + + private void OnAudioFilterRead(float[] data, int channels) + { + Driver?.ProcessAudioSamples(data, channels, data.Length); + } + + private void OnDisable() => StopCapture(); + } +} +#endif diff --git a/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimMic.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimMic.cs.meta new file mode 100644 index 0000000000..c1ff2223b7 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimMic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c1d0451c4f604815a946015776cb8b5c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimPlayer.cs b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimPlayer.cs new file mode 100644 index 0000000000..a4c47c63f2 --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimPlayer.cs @@ -0,0 +1,64 @@ +#if !BASIS_FRAMEWORK_EXISTS +using Basis.Scripts.BasisSdk; +using Basis.Scripts.BasisSdk.Helpers; +using Basis.Scripts.Drivers; +using UnityEngine; + +namespace Basis.Scripts.BasisSdk.Players +{ + // Runtime tick host for the SDK's editor preview. Attached to the + // preview root after the avatar is loaded so blink + visemes have somewhere + // to live in play mode. + public sealed class BasisAvatarSimPlayer : BasisPlayer + { + public BasisLocalFacialBlinkDriver Blink = new BasisLocalFacialBlinkDriver(); + public BasisAudioAndVisemeDriver Viseme = new BasisAudioAndVisemeDriver(); + + public void Bootstrap(BasisAvatar avatar) + { + IsLocal = true; + BasisAvatar = avatar; + FaceIsVisible = true; + + if (avatar.FaceVisemeMesh != null) + { + FaceRenderer = BasisHelpers.GetOrAddComponent(avatar.FaceVisemeMesh.gameObject); + } + + // Zero animator params for an idle-pose preview. The locomotion controller defaults + // CurrentSpeed to 1, which puts the avatar mid-walk before the user touches anything. + // The foldout's sliders still let the user drive these afterwards. + if (avatar.Animator != null && avatar.Animator.runtimeAnimatorController != null) + { + foreach (var p in avatar.Animator.parameters) + { + if (avatar.Animator.IsParameterControlledByCurve(p.nameHash)) continue; + switch (p.type) + { + case AnimatorControllerParameterType.Float: avatar.Animator.SetFloat(p.nameHash, 0f); break; + case AnimatorControllerParameterType.Bool: avatar.Animator.SetBool(p.nameHash, false); break; + case AnimatorControllerParameterType.Int: avatar.Animator.SetInteger(p.nameHash, 0); break; + } + } + } + + Blink.Initialize(this, avatar); + Viseme.TryInitialize(this); + } + + private void Update() + { + Blink.Simulate(Time.timeAsDouble); + Viseme.Simulate(Time.deltaTime); + BasisOpenLipSyncContext.ProcessAllPending(); + Viseme.Apply(); + } + + private void OnDestroy() + { + Blink.OnDestroy(); + Viseme.OnDestroy(); + } + } +} +#endif diff --git a/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimPlayer.cs.meta b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimPlayer.cs.meta new file mode 100644 index 0000000000..40dfd07adb --- /dev/null +++ b/Basis/Packages/com.basis.sdk/Scripts/Sim/BasisAvatarSimPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ac2331c71974f0c8cf33631007ec6b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Basis/Packages/com.basis.sdk/package.json b/Basis/Packages/com.basis.sdk/package.json index ea9afb0850..ac2e5e48e8 100644 --- a/Basis/Packages/com.basis.sdk/package.json +++ b/Basis/Packages/com.basis.sdk/package.json @@ -16,6 +16,7 @@ "com.unity.ugui": "2.0.0" }, "vpmDependencies": { + "com.basis.openlipsync": "0.2.0", "org.basisvr.bouncycastle": "2.5.0", "org.basisvr.newtonsoft.json": "13.0.3" } diff --git a/Basis/Packages/dev.hai-vr.hvr.license-review/Scripts/Runtime/HVR.LicenseReview.asmdef b/Basis/Packages/dev.hai-vr.hvr.license-review/Scripts/Runtime/HVR.LicenseReview.asmdef index 069b50ebf7..8bb1cbcf1a 100644 --- a/Basis/Packages/dev.hai-vr.hvr.license-review/Scripts/Runtime/HVR.LicenseReview.asmdef +++ b/Basis/Packages/dev.hai-vr.hvr.license-review/Scripts/Runtime/HVR.LicenseReview.asmdef @@ -2,6 +2,7 @@ "name": "HVR.LicenseReview", "rootNamespace": "", "references": [ + "BasisSDK", "Basis Framework", "Unity.Addressables", "Unity.ResourceManager"