Fusion 2.1 and Meta Core XR SDK

For the time being, there is a compatibility issue between Fusion 2.1 and the Meta Core XR SDK package (com.meta.xr.sdk.core). You can follow the instructions on this page to have a temporary workaround, until the Meta SDK is updated for Fusion 2.1 compatibility.

Don’t hesitate to contact us for any further help on this matter.

Compatibility issue

The Meta Core XR SDK includes references to Fusion SDK, to be able to provide building blocks setting up a networked scene.

The Fusion 2.1 version introduces breaking changes to its API, and the Meta team has not yet had the opportunity to reflect those changes in their SDK.

Here are the API changes impacting the Meta integration:

  • The interface method INetworkRunnerCallbacks.OnReliableDataReceived had a signature change
  • The NetworkTransform.DisableSharedModeInterpolation property does not exist anymore (this feature is integrated in config flags now)
  • Fusion now allows using the full underlying Photon Realtime API. Due to that, some functions now depends on the Photon.Realtime module, so modules using it have to add this reference in their asmdef.

We are in discussion with the Meta team to be sure that an incoming version of the Meta XR Core SDK will be compatible with Fusion 2.1.

In the meantime, to be able to use the Meta Core XR SDK alongside Fusion 2.1, some changes are required on the package com.meta.xr.sdk.core.

How to apply changes

The com.meta.xr.sdk.core is by default installed in the Library\PackageCache folder.

Editing the package in this location is not totally possible (notably, editing asmdef files in the inspector), so a solution, to make the required modifications simpler to apply, is to embed the package.

For more information about embedding packages, you can read those links:

Script to embed a package

To easily embed the package, you can add this script to your project: PackageEmbedding.cs (see source code and the end of this document)

Then, right-click on the "Meta XR Core SDK" package in your project view, and finally select "Move as local embedded package". The package will move from the Library\PackageCache folder, to the Packages folder, and modifications will be easier to apply.

To place it back, close the editor, then delete the Packages\com.meta.xr.sdk.core folder.

Note: alternatively, you can go to the package manager view, choose the "Meta XR Core SDK" package, and then select "Remove", however Unity will fail cleaning up the folder completely while running, as the OVRPlugin.dll is already loaded. So closing the editor first is a cleaner approach.

Changes to apply

In Meta SDK 78.0.0 or 81.0.0, 4 files need to be edited before being able to run Fusion 2.1 successfully alongside the Meta XR Core SDK.

The recommend approach is to:

  • Embed the Meta XR Core SDK package
  • Install Fusion 2.1 (this will display errors)
  • Add the Photon.realtime reference in the 2 asmdef to edit
  • Edit the 2 files to change
  • Restart Unity. Don't be surprised if the editor reports errors when launching the first time. Ignore the message, as Unity needs to complete the launch to verify that the project compiles correctly and that there are no more errors.
  • If needed, install the appropriate version of Photon Voice

Meta.XR.MultiplayerBlocks.Fusion.asmdef

File to edit location

Packages/com.meta.xr.sdk.core/Scripts/BuildingBlocks/MultiplayerBlocks/PhotonFusion/Meta.XR.MultiplayerBlocks.Fusion.asmdef

Initial content of the section to edit

    "references": [
        "Oculus.Interaction",
        "Oculus.Platform",
        "Oculus.VR",
        "Oculus.AvatarSDK2",
        "PhotonVoice.Fusion",
        "PhotonRealtime",
        "PhotonVoice",
        "PhotonVoice.API",
        "Meta.XR.BuildingBlocks",
        "Meta.XR.MultiplayerBlocks.Shared",
        "Fusion.Unity",
        "Fusion.Addons.Physics"
    ],

Modified content

Alternative in editor edition:

  • if the package is embedded, instead of manually editing the file, you can edit the asmdef file directly in the Unity inspector Window in "Assembly definition references", to add the Photon.Realtime asmdef reference.
  • Note that you will need to have Fusion installed, to be able to list the Photon.Realtime asmdef successfully in the inspector

    "references": [
        "Oculus.Interaction",
        "Oculus.Platform",
        "Oculus.VR",
        "Oculus.AvatarSDK2",
        "PhotonVoice.Fusion",
        "PhotonRealtime",
        "PhotonVoice",
        "PhotonVoice.API",
        "Meta.XR.BuildingBlocks",
        "Meta.XR.MultiplayerBlocks.Shared",
        "Fusion.Unity",
        "Fusion.Addons.Physics",
        "Photon.Realtime"
    ],

Meta.XR.MultiplayerBlocks.Fusion.Editor.asmdef

File to edit location

Packages/com.meta.xr.sdk.core/Editor/BuildingBlocks/BlockData/MultiplayerBlocks/PhotonFusion/Meta.XR.MultiplayerBlocks.Fusion.Editor.asmdef

Initial content of the section to edit

    "references": [
        "Meta.XR.BuildingBlocks",
        "Meta.XR.BuildingBlocks.Editor",
        "Oculus.Interaction",
        "Oculus.Interaction.OVR.Editor",
        "Oculus.VR",
        "Oculus.VR.Editor",
        "PhotonVoice",
        "PhotonRealtime",
        "PhotonVoice.Fusion",
        "Meta.XR.MultiplayerBlocks.Fusion",
        "Fusion.Unity.Editor",
        "Fusion.Unity",
        "Fusion.Addons.Physics",
        "Meta.XR.MultiplayerBlocks.Shared.Editor",
        "Meta.XR.MultiplayerBlocks.Shared"
    ],

Modified content

Alternative in editor edition:

  • if the package is embedded, instead of manually editing the file, you can edit the asmdef file directly in the Unity inspector Window in "Assembly definition references", to add the Photon.Realtime asmdef reference.
  • Note that you will need to have Fusion installed, to be able to list the Photon.Realtime asmdef successfully in the inspector

    "references": [
        "Meta.XR.BuildingBlocks",
        "Meta.XR.BuildingBlocks.Editor",
        "Oculus.Interaction",
        "Oculus.Interaction.OVR.Editor",
        "Oculus.VR",
        "Oculus.VR.Editor",
        "PhotonVoice",
        "PhotonRealtime",
        "PhotonVoice.Fusion",
        "Meta.XR.MultiplayerBlocks.Fusion",
        "Fusion.Unity.Editor",
        "Fusion.Unity",
        "Fusion.Addons.Physics",
        "Meta.XR.MultiplayerBlocks.Shared.Editor",
        "Meta.XR.MultiplayerBlocks.Shared",
        "Photon.Realtime"
    ],

FusionBBEvents.cs

File to edit location

Packages/com.meta.xr.sdk.core/Scripts/BuildingBlocks/MultiplayerBlocks/PhotonFusion/NetworkRunner/Scripts/FusionBBEvents.cs

Initial code of the section to edit

void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key,
      ArraySegment<byte> data)
  {
    OnReliableDataReceived?.Invoke(runner, player, key, data);
  }

Modified code

#if FUSION_2_1
  void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key,
      ReadOnlySpan<byte> data)
  {
    OnReliableDataReceived?.Invoke(runner, player, key, data.ToArray());
  }
#else
  void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key,
      ArraySegment<byte> data)
  {
    OnReliableDataReceived?.Invoke(runner, player, key, data);
  }
#endif

NetworkedGrabbableObjectFusionInstallationRoutine.cs

File to edit location

Packages/com.meta.xr.sdk.core/Editor/BuildingBlocks/BlockData/MultiplayerBlocks/PhotonFusion/NetworkedGrabbableObject/Scripts/NetworkedGrabbableObjectFusionInstallationRoutine.cs

Initial code of the section to edit

  networkTransform.DisableSharedModeInterpolation = true; // otherwise object cannot be grabbed by ISDK

Modified code

#if FUSION_2_1
  // otherwise object cannot be grabbed by ISDK
  networkTransform.ConfigFlags = NetworkTransform.NetworkTransformFlags.DisableSharedModeInterpolation; 
#else
  // otherwise object cannot be grabbed by ISDK
  networkTransform.DisableSharedModeInterpolation = true;
#endif

Photon Voice

If Photon Voice is needed, the Photon Voice Realtime 5 version should be used instead of the version available on the asset store.

It can be found in the "Complementary download" section of the Fusion 2.1 download page: https://doc.photonengine.com/fusion/current/getting-started/preview-2-1/download-2-1#matching-downloads

PackageEmbedding.cs source

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace Photon.PackageTooling
{
    /*
     * Allows to move an existing package as an embedded package
     * See https://docs.unity3d.com/Manual/upm-embed.html, https://docs.unity3d.com/Manual/upm-api.html#Embed and https://docs.unity3d.com/ScriptReference/PackageManager.Client.Embed.html
     * 
     * Inspired by https://github.com/liortal53/upm-embed/blob/master/Assets/EmbedPackage/Scripts/EmbedPackage.cs
     */
    public static class PackageEmbedding
    {
        static EmbedRequest EmbeddingRequest;
        static RemoveRequest RemoveEmbeddingRequest;

        public static bool IsAnyRequestPending()
        {
            return EditorApplication.isCompiling || EmbeddingRequest != null || RemoveEmbeddingRequest != null;
        }
        public static void EmbedPackage(string packageName)
        {
            if (IsAnyRequestPending())
            {
                Debug.LogError("Editor busy, or an embedding-related request is already in progress: please ait and retry");
                return;
            }
            Debug.Log($"Installed package '{packageName}' is being moved to the Packages folder of the project as an embedded package ...");

            EmbeddingRequest = UnityEditor.PackageManager.Client.Embed(packageName);
            EditorApplication.update += EmbedProgress;
        }

        private static void EmbedProgress()
        {
            if (EmbeddingRequest.IsCompleted)
            {
                switch (EmbeddingRequest.Status)
                {
                    case StatusCode.Success:
                        Debug.Log($"Package successfully embedded (id: {EmbeddingRequest.Result.packageId}. " +
                            $" Uninstalling it from the package manager will remove this custom version and return to the original one." +
                            $" Alternatively, to un-embed it, simply delete the embedded version version folder in your project Packages folder ({Path.GetFullPath(EmbeddingRequest.Result.assetPath)}):" +
                            $" the repository package will re reinstalled in its unedited version");
                        AssetDatabase.Refresh();
                        Debug.LogError("Database refreshed");
                        break;
                    case StatusCode.Failure:
                        Debug.Log($"Unable to embed package: {EmbeddingRequest.Error.message}");
                        break;
                }
                
                EditorApplication.update -= EmbedProgress;
                EmbeddingRequest = null;

            }
        }

        public static bool TryGetFirstSelectedAssetPath(out string selectedItemPath, out string selectedItemFullPath)
        {
            // Source: https://docs.unity3d.com/ScriptReference/Selection.GetFiltered.html and https://discussions.unity.com/t/selection-activeobject-on-items-on-left-side-of-project-window/664382/6
            selectedItemPath = null;
            selectedItemFullPath = null;
            UnityEngine.Object[] selectedAssets = Selection.GetFiltered(typeof(object), SelectionMode.Assets);
            foreach (var selectedAsset in selectedAssets)
            {
                selectedItemPath = AssetDatabase.GetAssetPath(selectedAsset);
                selectedItemFullPath = Path.GetFullPath(selectedItemPath);
                return true;
            }
            return false;

        }

        public static void UnEmbedPackage(string packageName, string absolutePath)
        {
            if (IsAnyRequestPending())
            {
                Debug.LogError("Editor busy, or an embedding-related request is already in progress: please ait and retry");
                return;
            }
            Debug.Log($"Embedded package '{packageName}' will be deleted and replaced by the original unembedded package version");

            Debug.LogError("Will erase " + absolutePath);
            if(TrySafeDeleteFolder(absolutePath) == false)
            {
                Debug.LogError("Failed to erase embedded version");
            }
            else
            {
                // Deletion succeeded
                Debug.LogError("Deletion ok");
                AssetDatabase.Refresh();
                Debug.LogError("Database refreshed");
            }
        }

        public static bool TrySafeDeleteFolder(string path)
        {
            var folderInfo = new DirectoryInfo(path);
            if (TrySafeDeleteFolder(folderInfo, dryRun: true) == false)
            {
                Debug.LogError($"Deletion of {path} aborted: no files were edited");
                return false;
            } else
            {
                Debug.LogError("Dry run of deletion ok");
            }
            return TrySafeDeleteFolder(folderInfo);
        }

        public static bool TrySafeDeleteFolder(DirectoryInfo folderInfo, bool dryRun = false)
        { 
            if (folderInfo.Exists == false)
            {
                return false;
            }
            FileInfo[] fileInfos = folderInfo.GetFiles();
            foreach (var fileInfo in fileInfos)
            {
                if (fileInfo.IsReadOnly)
                {
                    Debug.LogError($"{fileInfo} is read-only");
                    return false;
                }
                if (dryRun == false)
                {
                    try
                    {
                        fileInfo.Delete();
                    }
                    catch (Exception e)
                    {
                        Debug.LogError($"Unable to delete file {fileInfo}: {e.GetType()} {e.Message}");
                        return false;
                    }
                }
                else
                {
                    // Dry run
                    try
                    {
                        // Check if the file is used. See https://stackoverflow.com/questions/876473/is-there-a-way-to-check-if-a-file-is-in-use
                        using (FileStream stream = fileInfo.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None))
                        {
                            stream.Close();
                        }
                    }
                    catch (Exception e)
                    {
                        Debug.LogError($"[Preliminary check fail] File {fileInfo} deletion won't be possible: [{e.GetType()}] {e.Message} (dry-run only: no files were edited)");
                        return false;
                    }
                }

            }
            DirectoryInfo[] subFolderInfos = folderInfo.GetDirectories();
            foreach(var subFolderInfo in subFolderInfos)
            {
                if (TrySafeDeleteFolder(subFolderInfo, dryRun: dryRun) == false)
                {
                    return false;
                }
            }
            if(dryRun == false)
            {
                try
                {
                    folderInfo.Delete();
                }
                catch (Exception e)
                {
                    Debug.LogError($"Unable to delete folder {folderInfo}: {e.GetType()} {e.Message}");
                }
            }

            return true;
        }

        public static void EmbedPackageFolder(string folderLocalPath, string folderGlobalPath, bool backupOriginalPath = false)
        {
            if (folderLocalPath.StartsWith("Assets") == false) return;
            string packageFile = Path.Join(folderLocalPath, "package.json");
            if (File.Exists(packageFile) == false) return;

            string packageName = Path.GetFileName(folderLocalPath);
            string originalPathFilePath = Path.Join(folderLocalPath, "OriginalFolderPath.log");
            string projectFolder = Path.GetDirectoryName(Application.dataPath); 
            string newLocationPath = Path.Join(projectFolder, "Packages", packageName);

            Debug.LogError($"Will move {folderGlobalPath} to {newLocationPath}");

            var folderInfo = new DirectoryInfo(folderLocalPath);
            if (TrySafeDeleteFolder(folderInfo, dryRun: true) == false)
            {
                Debug.LogError($"Moving of {folderLocalPath} aborted: no files were edited");
                return;
            }

            if (Directory.Exists(newLocationPath))
            {
                Debug.LogError($"Moving of {folderLocalPath} aborted: folder {newLocationPath} already exists");
                return;
            }

            if (backupOriginalPath && File.Exists(originalPathFilePath) == false)
            {
                // Store original path in case of revert needs
                var originalPathFile = File.CreateText(originalPathFilePath);
                originalPathFile.WriteLine(folderLocalPath);
                originalPathFile.Close();
            }

            try
            {
                Directory.Move(folderGlobalPath, newLocationPath);
                AssetDatabase.Refresh();
                Debug.LogError("Database refreshed");
            }
            catch (Exception e)
            {
                Debug.LogError("Unable to convert folder as an embedded package (close your editor an any app that could have access to it)) " + e.Message);
                throw e;
            }

        }

        #region Embed menu item
        [MenuItem("Assets/Move as local embedded package", false, 10_000)]
        public static void PackageEmbeddingAction()
        {
            if (TryGetFirstSelectedAssetPath(out string selectedItemPath, out string selectedItemFullPath))
            {
                string packageName = Path.GetFileName(selectedItemPath);

                PackagePresenceCheck.LookForPackage(packageName, (packageInfo) => {
                    if (packageInfo == null)
                    {
                        Debug.LogError($"Package {packageName} not found");
                    }
                    else if (packageInfo.isDirectDependency == false)
                    {
                        Debug.LogError($"Error: Client.Embed cannot embed indirect dependency. Install {packageInfo.name} directly, not automatically as a dependency, to be able to embed it");
                    }
                    else
                    {
                        switch (packageInfo.source)
                        {
                            case PackageSource.BuiltIn:
                                Debug.LogError($"Error: built-in package embedding not supported ({packageInfo.name})");
                                break;
                            case PackageSource.Embedded:
                                Debug.LogError($"Package {packageInfo.name} is already embedded. Uninstalling it from the package manager will remove this custom version and return to the original one");
                                break;
                        }
                    }
                    EmbedPackage(packageName);
                });
            }
        }

        [MenuItem("Assets/Move as local embedded package", true)]
        public static bool PackageEmbeddingValidation()
        {
            if (TryGetFirstSelectedAssetPath(out string selectedItemPath, out string selectedItemFullPath))
            {
                if (selectedItemPath.StartsWith("Packages"))
                {
                    string packageName = Path.GetFileName(selectedItemPath);
                    // It is a package: already embedded ? 
                    if (selectedItemFullPath.Contains("PackageCache") == false)
                    {
                        // Already an embedded package
                        //Debug.Log($"Already embedded package: {packageName}");
                        return false;
                    }
                    // Non-embedded package (not a garantee we can embed it: built-in and dependency packages are not embeddable
                    //Debug.Log($"Non embedded pacakge {packageName} at path {selectedItemPath}"); 
                    return true;
                }
                return false;

            }
            return false;
        }
        #endregion        
    }

    /*
     * Package Lookup methods
     */
    public class PackagePresenceCheck
    {
        string[] packageNames = null;
        UnityEditor.PackageManager.Requests.ListRequest request;
        public delegate void ResultDelegate(Dictionary<string, UnityEditor.PackageManager.PackageInfo> packageInfoByPackageName);
        ResultDelegate resultCallback;
        public delegate void SingleResultDelegate(UnityEditor.PackageManager.PackageInfo packageInfo);
        SingleResultDelegate singleResultCallback;

        Dictionary<string, UnityEditor.PackageManager.PackageInfo> results = new Dictionary<string, UnityEditor.PackageManager.PackageInfo>();

        public static void LookForPackage(string packageToSearch, SingleResultDelegate packageLookupCallback = null, string defineToAddIfDetected = null)
        {
            var packageCheck = new PackagePresenceCheck(packageToSearch, (packageInfo) => {
                var group = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
                var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);

                if (packageInfo != null)
                {
                    if (string.IsNullOrEmpty(defineToAddIfDetected) == false && defines.Contains(defineToAddIfDetected) == false) {
                        defines = $"{defines};{defineToAddIfDetected}";
                        PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
                    }
                    packageLookupCallback(packageInfo);
                }
                else
                {
                    if (string.IsNullOrEmpty(defineToAddIfDetected) == false && defines.Contains(defineToAddIfDetected) == true)
                    {
                        Debug.LogError($"Project define symbols include {defineToAddIfDetected}, while package {packageToSearch} is not installed anymore: it should be removed in the Player settings");
                    }
                    packageLookupCallback(null);
                }
            });
        }

        public PackagePresenceCheck(string[] packageNames, ResultDelegate resultCallback, bool useOfflineMode = true)
        {
            this.packageNames = packageNames;
            this.resultCallback = resultCallback;
            request = UnityEditor.PackageManager.Client.List(offlineMode: useOfflineMode, includeIndirectDependencies: true);
            EditorApplication.update += Progress;
        }

        public PackagePresenceCheck(string packageName, SingleResultDelegate resultCallback, bool useOfflineMode = true)
        {
            this.packageNames = new string[] { packageName };
            this.singleResultCallback = resultCallback;
            request = UnityEditor.PackageManager.Client.List(offlineMode: useOfflineMode, includeIndirectDependencies: true);
            EditorApplication.update += Progress;
        }

        void Progress()
        {
            if (request.IsCompleted)
            {
                bool singleResultCallbackReturned = false;
                results = new Dictionary<string, UnityEditor.PackageManager.PackageInfo>();
                if (request.Status == StatusCode.Success)
                {
                    foreach (var info in request.Result)
                    {
                        foreach(var checkedPackageName in packageNames)
                        {
                            if (info.name == checkedPackageName)
                            {
                                results[checkedPackageName] = info;
                                if (singleResultCallback != null)
                                {
                                    singleResultCallbackReturned = true;
                                    singleResultCallback(info);
                                }
                                break;
                            }
                        }
                    }
                }
                if (singleResultCallback != null && singleResultCallbackReturned == false)
                {
                    singleResultCallback(null);
                }
                if (resultCallback != null)
                    resultCallback(results);
                EditorApplication.update -= Progress;
            }
        }
    }
}

#endif
Back to top