// /*===============================================================================
// Copyright (C) 2020 PhantomsXR Ltd. All Rights Reserved.
//
// This file is part of the XR-MOD SDK.
//
// The XR-MOD SDK cannot be copied, distributed, or made available to
// third-parties for commercial purposes without written permission of PhantomsXR Ltd.
//
// Contact nswell@phantomsxr.com for licensing requests.
// ===============================================================================*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Phantom.XRMOD.XRMODPackageTools.Runtime;
using Phantom.XRMOD.UnityFusion.Runtime;
using UnityFusion.CLR.Method;
using UnityFusion.CLR.TypeSystem;
using UnityFusion.Runtime.Intepreter;
using UnityEngine;
using AppDomain = UnityFusion.Runtime.Enviorment.AppDomain;
using Phantom.XRMOD.ActionNotification.Runtime;
using Phantom.XRMOD.UnityFusion.Runtime.CodeHook;
using UnityFusion.CLRBinding.Adapter;
using UnityEngine.Assertions;
using Phantom.XRMOD.Core.Runtime;
using Object = UnityEngine.Object;
namespace Phantom.XRMOD.UnityFusion.Runtime
{
///
/// The core hook class for the XR-MOD runtime.
/// Manages the ILRuntime/CLR AppDomain, assembly loading, and hooks into Unity's lifecycle and event system.
///
// ReSharper disable once InconsistentNaming
public class CodesHook
{
/// Gets or sets whether debug mode is enabled.
public bool debug { get; set; }
private static bool _INITIALIZED;
private IDllLoaderProvider dllLoaderProvider;
// private static AppDomain GetAppDomain;
// private MemoryStream SharedData.Instance.dllStream;
// private MemoryStream SharedData.Instance.symbolStream;
// For multi-XR Experiences
private List updateMethods;
private IMethod onReleaseMethod;
private IMethod onEventMethod;
// Parameters
private readonly object[] onEventData = new object[1];
// private IMethod onMultiplayerMethod;
private ILTypeInstance ilTypeInstance;
private string projectName;
///
/// Gets the current active AppDomain.
///
public static AppDomain GetAppDomain => SharedData.Instance.appDomain;
///
/// Gets the cached runtime asset reference databases by project name.
///
public static Dictionary AssetReferences { get; private set; } = new();
///
/// Initializes the hook for a specific project and entry point.
///
/// The name of the project.
/// The domain/namespace name.
/// The main class entry point.
/// The JIT flag for the AppDomain.
public void InitializeHook(string _projectName, string _domainName, string _mainEntryPoint, int _jitFlag)
{
_INITIALIZED = SharedData.Instance.appDomain != null;
SharedData.Instance.appDomain ??= new AppDomain(_jitFlag);
projectName = _projectName;
LoadExpandDllAsync(_projectName, _domainName, _mainEntryPoint);
IocContainer.GetIoc.Register(new UserInstantiatedObjects());
}
private async void LoadExpandDllAsync(string _projectName, string _domainName, string _mainEntryPoint)
{
try
{
dllLoaderProvider = Application.isEditor || debug
? new DebugDllLoaderProvider()
: new ReleaseDllLoaderProvider();
await dllLoaderProvider.LoadAssembly(SharedData.Instance.appDomain, _projectName);
if (!_INITIALIZED)
{
InitializeUnityFusion();
}
// Fix null exception after second load.
// Since the release of AR Experience, the notification list will be cleaned up.
// So we need to warm it.
ActionNotificationCenter.DefaultCenter.AddObserver(InstantiatingNotificationAction,
nameof(ActionParameterDataType.Instantiate));
try
{
var tmp_ProjectName = _domainName.Split(".")[0];
var tmp_Db = await LoadRuntimeReferenceDatabase(tmp_ProjectName);
if (tmp_Db)
{
AssetReferences.TryAdd(tmp_ProjectName, tmp_Db);
}
}
catch (Exception tmp_Exception)
{
Debug.LogError($"Load runtime reference database failed: {tmp_Exception.Message}");
}
StartHookingCode($"{_domainName}.{_mainEntryPoint}");
}
catch (Exception tmp_Exception)
{
Debug.LogError($"Load expand dll failed: {tmp_Exception.Message}\n{tmp_Exception.StackTrace}");
}
}
private void InitializeUnityFusion()
{
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
GetAppDomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
Singleton.GetInstance.Register(GetAppDomain);
Singleton.GetInstance.Register(GetAppDomain);
Singleton.GetInstance.Register(GetAppDomain);
Singleton.GetInstance.Register(GetAppDomain);
Singleton.GetInstance.Register(GetAppDomain);
GetAppDomain.InitializeBindings();
ActionNotificationCenter.DefaultCenter.PostNotification(nameof(ActionParameterDataType.RegisterBuiltInCLR),
new UnityFusionArgs()
{
AppDomain = GetAppDomain
});
// Send register clr command to every modules(Extra CLR).
ActionNotificationCenter.DefaultCenter.PostNotification(
nameof(ActionParameterDataType.RegisterExtraCLR), new UnityFusionArgs
{
AppDomain = GetAppDomain
});
_INITIALIZED = true;
}
private void StartHookingCode(string _runtimeEntryPoint)
{
var tmp_MethodDirectory = GetAppDomain.LoadedTypes[_runtimeEntryPoint];
ilTypeInstance = ((ILType) tmp_MethodDirectory).Instantiate();
var tmp_Constructor = ilTypeInstance.Type.GetConstructor(new List());
if (tmp_Constructor != null)
{
GetAppDomain.Invoke(tmp_Constructor, ilTypeInstance, null);
}
try
{
GetAppDomain.Invoke(_runtimeEntryPoint, MethodKey.ONLOAD, ilTypeInstance, null);
if (tmp_MethodDirectory == null) return;
onEventMethod = tmp_MethodDirectory.GetMethod(MethodKey.ONEVENT, 1);
onReleaseMethod = tmp_MethodDirectory.GetMethod(MethodKey.RELEASEMEMORY, 1);
updateMethods ??= new List();
updateMethods.Add(tmp_MethodDirectory.GetMethod(MethodKey.ONUPDATE, 0));
}
catch (Exception tmp_Exception)
{
throw new Exception($"Start hooking code failed: {tmp_Exception.Message}", tmp_Exception);
}
}
#region Unity Life cycle methods
////////////////////////
//Call back function////
////////////////////////
///
/// Called during the Unity Update cycle.
/// Invokes the 'OnUpdate' method in the hot-reload assembly for all active instances.
///
public void OnUpdate()
{
if (!_INITIALIZED || updateMethods == null || SharedData.Instance.dllStream == null) return;
foreach (var tmp_UpdateMethod in updateMethods)
{
GetAppDomain.Invoke(tmp_UpdateMethod, ilTypeInstance);
}
}
///
/// Only work XR-MOD not work in XR-Widgets
///
///
public void OnEvent(BaseNotificationData _data)
{
if (!_INITIALIZED || onEventMethod == null || SharedData.Instance.dllStream == null) return;
onEventData[0] = _data;
GetAppDomain.Invoke(onEventMethod, ilTypeInstance, onEventData);
}
///
/// Only work XR-MOD not work in XR-Widgets
///
///
public void ReleaseMemory(string _projectName)
{
if (!_INITIALIZED || SharedData.Instance.dllStream == null || onReleaseMethod == null) return;
GetAppDomain.Invoke(onReleaseMethod, ilTypeInstance, _projectName ?? projectName);
}
///
/// Disposes of the hook, releases memory, and cleans up the AppDomain and asset references.
///
/// The project to clean up (optional).
public async void Dispose(string _projectName = null)
{
try
{
ReleaseMemory(_projectName);
//Release MonoBehaviour and XRMODBehaviour
List tmp_AllMonoAdaptors = new List();
tmp_AllMonoAdaptors.AddRange(
Object.FindObjectsByType(FindObjectsInactive.Include,
FindObjectsSortMode.None));
foreach (var tmp_MonoBehaviour in tmp_AllMonoAdaptors)
{
if (tmp_MonoBehaviour == null) continue;
tmp_MonoBehaviour.enabled = false;
Object.DestroyImmediate(tmp_MonoBehaviour, true);
}
IocContainer.GetIoc.UnRegister();
ActionNotificationCenter.DefaultCenter.RemoveObserver(nameof(ActionParameterDataType.OnEvent));
updateMethods = null;
onEventMethod = null;
ilTypeInstance = null;
GetAppDomain?.Dispose();
await Task.Delay(500);
SharedData.Instance.dllStream?.Dispose();
SharedData.Instance.symbolStream?.Dispose();
SharedData.Instance.dllStream = SharedData.Instance.symbolStream = null;
AssetReferences.Clear();
#if UNITY_EDITOR
SharedData.Instance.appDomain = null;
#endif
}
catch (Exception tmp_Exception)
{
throw new Exception($"CodesHook dispose error:{tmp_Exception.Message}");
}
}
#endregion
private object InstantiatingNotificationAction(BaseNotificationData _arg)
{
if (_arg is not InstantiateArgs tmp_Data) return null;
if (GetAppDomain == null)
{
Assert.IsNotNull(tmp_Data.Prefab);
return tmp_Data.Parent
? Object.Instantiate(tmp_Data.Prefab, tmp_Data.Parent)
: Object.Instantiate(tmp_Data.Prefab);
}
Utility.CheckoutIlTypeInstance(tmp_Data.Prefab, out var tmp_Go, out var tmp_Type);
var tmp_ResultOfThisMethod = tmp_Data.Parent
? Object.Instantiate(tmp_Data.Prefab, tmp_Data.Parent)
: Object.Instantiate(tmp_Data.Prefab);
var tmp_Res = Utility.DoBinding(tmp_Go, tmp_ResultOfThisMethod, GetAppDomain, tmp_Type);
if (tmp_Type == null && tmp_Res is GameObject tmp_GameObject &&
tmp_GameObject.GetType() != tmp_Data.Prefab.GetType())
{
return tmp_GameObject.GetComponent(tmp_Data.Prefab.GetType());
}
return tmp_Res;
}
private async Task LoadRuntimeReferenceDatabase(string _projectName)
{
var tmp_RuntimeAssetDb =
await BasePackageLoaderUtility.LoadAssetFromPackage(_projectName,
nameof(RuntimeAssetReferenceDatabase));
await tmp_RuntimeAssetDb.Initialize(_projectName);
return tmp_RuntimeAssetDb;
}
}
}