You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

264 lines
7.1 KiB

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; }
/// <summary>
/// Time since startup for this particular instance of Quake
/// </summary>
public double CurrentTime => Time.timeAsDouble - startTime;
private Action<string> logHandler;
void Start()
{
logHandler = PrintLog; // Send each log statement to Unity immediately during startup
systemModule = new SystemModule(this);
renderModule = new RenderModule(this);
LoadLibrary();
List<string> arguments = new List<string>
{
"",
"-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_InitFunc>("UniQuake_Init");
UniQuake_Update = LoadLibraryFunction<UniQuake_UpdateFunc>("UniQuake_Update");
UniQuake_Shutdown = LoadLibraryFunction<UniQuake_ShutdownFunc>("UniQuake_Shutdown");
UniQuake_SetFmodSystem = LoadLibraryFunction<UniQuake_SetFmodSystemFunc>("UniQuake_SetFmodSystem");
}
private TDelegate LoadLibraryFunction<TDelegate>(string functionName)
{
IntPtr procAddress = SystemLibrary.GetProcAddress(libraryHandle, functionName);
if (procAddress == IntPtr.Zero)
{
throw new DllNotFoundException($"Could not find library function: {functionName}");
}
return Marshal.GetDelegateForFunctionPointer<TDelegate>(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;
}
}