using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; using UnityEngine; public class UniQuake: MonoBehaviour { #if UNITY_EDITOR private const string DllPath = "Plugins/windows/x86_64/uniquake.dll"; #elif UNITY_STANDALONE_WIN #if UNITY_64 private const string DllPath = "Plugins/x86_64/uniquake.dll"; #else private const string DllPath = "Plugins/x86/uniquake.dll"; #endif #endif private const int MemSize = 0x8000000; // 128 MB of heap space private IntPtr libraryHandle; private QuakeParms quakeParms; private SystemModule systemModule; private RenderModule renderModule; private bool initialized = false; private double startTime; public MissionPack BaseGame { get; set; } public string ModDirectory { get; set; } /// /// Time since startup for this particular instance of Quake /// public double CurrentTime => Time.timeAsDouble - startTime; private Action logHandler; void Start() { logHandler = PrintLog; // Send each log statement to Unity immediately during startup systemModule = new SystemModule(this); renderModule = new RenderModule(this); LoadLibrary(); List arguments = new List { "", "-window", "-width", "1440", "-height", "1080", }; switch (BaseGame) { case MissionPack.Hipnotic: arguments.Add("-hipnotic"); break; case MissionPack.Rogue: arguments.Add("-rogue"); break; } if (!string.IsNullOrEmpty(ModDirectory)) { arguments.AddRange(new[] { "-game", ModDirectory }); } if (Debug.isDebugBuild) { arguments.AddRange(new[] { "+developer", "1" }); } quakeParms = new QuakeParms { baseDir = Application.persistentDataPath, userDir = null, // TODO set this if user prefs need to be stored somewhere specific, otherwise this will be the same as baseDir numCpus = SystemInfo.processorCount, errState = 0, }; quakeParms.SetArguments(arguments.ToArray()); quakeParms.AllocateMemory(MemSize); startTime = Time.timeAsDouble; try { UniQuake_SetFmodSystem(AudioManager.Instance.FmodSystem.handle); UniQuake_Init(quakeParms.ToNativePtr(), systemModule.CallbacksPtr, renderModule.CallbacksPtr); initialized = true; } catch (QuakeException ex) { if (ex.ExitCode == 0) Debug.Log(ex.Message); else Debug.LogException(ex); Shutdown(); } catch (Exception ex) { Debug.LogException(ex); Shutdown(); } } void Update() { if (!initialized) return; logHandler = CollectLog; // Collect and dump logs to Unity once per frame try { UniQuake_Update(Time.deltaTime); } catch (QuakeException ex) { if (ex.ExitCode == 0) Debug.Log(ex.Message); else Debug.LogException(ex); Shutdown(); } catch (Exception ex) { Debug.LogException(ex); Shutdown(); } FlushLog(); } private void Shutdown() { initialized = false; UniQuake_Shutdown(); Destroy(this); } private void OnDestroy() { renderModule.Destroy(); systemModule.Destroy(); if (quakeParms != null) { quakeParms.Destroy(); quakeParms = null; } FreeLibrary(); } public void AddLog(string log) { logHandler?.Invoke(log); } private void PrintLog(string log) { Debug.Log(log); } private StringBuilder logBuffer = new StringBuilder(); private void CollectLog(string log) { logBuffer.Append(log); } private void FlushLog() { if (logBuffer.Length > 0) { Debug.Log(logBuffer.ToString()); logBuffer.Clear(); } } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void UniQuake_InitFunc(IntPtr parms, IntPtr sysCalls, IntPtr glCalls); private UniQuake_InitFunc UniQuake_Init; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void UniQuake_UpdateFunc(float deltaTime); private UniQuake_UpdateFunc UniQuake_Update; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void UniQuake_ShutdownFunc(); private UniQuake_ShutdownFunc UniQuake_Shutdown; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void UniQuake_SetFmodSystemFunc(IntPtr fmodSystem); private UniQuake_SetFmodSystemFunc UniQuake_SetFmodSystem; private void LoadLibrary() { string dllFile = Path.Combine(Application.dataPath, DllPath); // Experimental code to allow running multiple instances of Quake next to each other // string dllName = Path.GetFileNameWithoutExtension(dllFile); // string dllExt = Path.GetExtension(dllFile); // string dllCopy = Path.Combine(Application.persistentDataPath, $"{dllName}{GetInstanceID()}{dllExt}"); // File.Copy(dllFile, dllCopy, true); // dllFile = dllCopy; libraryHandle = SystemLibrary.LoadLibrary(dllFile); if (libraryHandle == IntPtr.Zero) { throw new DllNotFoundException($"Failed to load UniQuake library from path: {dllFile}, last error = {Marshal.GetLastWin32Error()}"); } UniQuake_Init = LoadLibraryFunction("UniQuake_Init"); UniQuake_Update = LoadLibraryFunction("UniQuake_Update"); UniQuake_Shutdown = LoadLibraryFunction("UniQuake_Shutdown"); UniQuake_SetFmodSystem = LoadLibraryFunction("UniQuake_SetFmodSystem"); } private TDelegate LoadLibraryFunction(string functionName) { IntPtr procAddress = SystemLibrary.GetProcAddress(libraryHandle, functionName); if (procAddress == IntPtr.Zero) { throw new DllNotFoundException($"Could not find library function: {functionName}"); } return Marshal.GetDelegateForFunctionPointer(procAddress); } private void FreeLibrary() { if (libraryHandle != IntPtr.Zero) { SystemLibrary.FreeLibrary(libraryHandle); libraryHandle = IntPtr.Zero; } } } public enum MissionPack { Quake, // Vanilla Quake Hipnotic, // Scourge of Armagon Rogue, // Dissolution of Eternity } public class QuakeException: Exception { public int ExitCode { get; } public QuakeException(string message, int exitCode = 0) : base(message) { ExitCode = exitCode; } }