using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEngine; public class UniQuake: MonoBehaviour { private const string DllName = "uniquake.dll"; private const int MemSize = 0x4000000; // 64 MB of heap space private const int MaxArgs = 50; // Corresponds with MAX_NUM_ARGVS in engine private QuakeParms quakeParms; private UnityCallbacks callbacks = new UnityCallbacks(); private static double startTime; // TODO: this should be an instance field, but it requires a reference back to this object in the callbacks void Start() { startTime = Time.timeAsDouble; string[] arguments = { "", "-window", "-width", "1440", "-height", "1080", }; IntPtr[] argv = new IntPtr[MaxArgs]; for (int i = 0; i < arguments.Length && i < MaxArgs; ++i) { argv[i] = Marshal.StringToHGlobalAnsi(arguments[i]); } quakeParms = new QuakeParms { baseDir = Application.persistentDataPath, cacheDir = null, argc = arguments.Length, argv = argv, memBase = Marshal.AllocHGlobal(MemSize), memSize = MemSize, }; UniQuake_Init(callbacks.CallbacksPtr, quakeParms); } void Update() { UniQuake_Update(Time.deltaTime); } private void OnDestroy() { callbacks.Destroy(); if (quakeParms != null) { if (quakeParms.memBase != IntPtr.Zero) { Marshal.FreeHGlobal(quakeParms.memBase); quakeParms.memBase = IntPtr.Zero; } if (quakeParms.argv != null) { for (int i = 0; i < quakeParms.argv.Length; ++i) { if (quakeParms.argv[i] != IntPtr.Zero) { Marshal.FreeHGlobal(quakeParms.argv[i]); quakeParms.argv[i] = IntPtr.Zero; } } } } } [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void UniQuake_Echo(DebugLogCallback logCallback, [MarshalAs(UnmanagedType.LPStr)] string message); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void UniQuake_Init(IntPtr callbacks, QuakeParms parms); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void UniQuake_Update(float deltaTime); private delegate void DebugLogCallback([MarshalAs(UnmanagedType.LPStr)] string message); [AOT.MonoPInvokeCallback(typeof(DebugLogCallback))] private static void Callback_DebugLog(string message) { Debug.Log(message); } private delegate void DebugLogErrorCallback([MarshalAs(UnmanagedType.LPStr)] string message); [AOT.MonoPInvokeCallback(typeof(DebugLogErrorCallback))] private static void Callback_DebugLogError(string message) { Debug.LogError(message); } private delegate void ApplicationQuitCallback(int exitCode); [AOT.MonoPInvokeCallback(typeof(ApplicationQuitCallback))] private static void Callback_ApplicationQuit(int exitCode) { Debug.Log($"Quitting application with exit code: {exitCode}"); // TODO: kill execution of the DLL entirely (Sys_Quit expects immediate exit, which Application.Quit doesn't do) Application.Quit(exitCode); } private delegate double RealtimeSinceStartupCallback(); [AOT.MonoPInvokeCallback(typeof(RealtimeSinceStartupCallback))] private static double Callback_RealtimeSinceStartup() { return Time.timeAsDouble - startTime; } private class UnityCallbacks { private List handles = new List(); private UnityCallbacksContainer container; private GCHandle containerHandle; public UnityCallbacks() { container = new UnityCallbacksContainer { DebugLogPtr = CreateCallback(Callback_DebugLog), DebugLogErrorPtr = CreateCallback(Callback_DebugLogError), ApplicationQuitPtr = CreateCallback(Callback_ApplicationQuit), RealtimeSinceStartupPtr = CreateCallback(Callback_RealtimeSinceStartup), }; containerHandle = GCHandle.Alloc(container, GCHandleType.Pinned); } public IntPtr CallbacksPtr => containerHandle.AddrOfPinnedObject(); public void Destroy() { if (containerHandle.IsAllocated) containerHandle.Free(); foreach (var handle in handles) { if (handle.IsAllocated) handle.Free(); } } private IntPtr CreateCallback(TDelegate callback) { handles.Add(GCHandle.Alloc(callback)); return Marshal.GetFunctionPointerForDelegate(callback); } } [StructLayout(LayoutKind.Sequential, Pack = 0)] private class UnityCallbacksContainer { public IntPtr DebugLogPtr; public IntPtr DebugLogErrorPtr; public IntPtr ApplicationQuitPtr; public IntPtr RealtimeSinceStartupPtr; } [StructLayout(LayoutKind.Sequential, Pack = 0)] private class QuakeParms { [MarshalAs(UnmanagedType.LPStr)] public string baseDir; [MarshalAs(UnmanagedType.LPStr)] public string cacheDir; public int argc; [MarshalAs(UnmanagedType.LPArray, SizeConst = MaxArgs)] public IntPtr[] argv; public IntPtr memBase; public int memSize; } }