using System; using System.IO; using System.Runtime.InteropServices; using AOT; using UnityEngine; public class SystemModule: CallbackHandler { private const int MaxFileHandles = 50; private readonly UniQuake uq; private readonly FileStream[] fileHandles = new FileStream[MaxFileHandles]; public SystemModule(UniQuake uniQuake) { uq = uniQuake; var callbacks = new Callbacks { target = SelfPtr, SysPrint = CreateCallback(Callback_SysPrint), SysError = CreateCallback(Callback_SysError), SysQuit = CreateCallback(Callback_SysQuit), SysFloatTime = CreateCallback(Callback_SysFloatTime), SysFileOpenRead = CreateCallback(Callback_SysFileOpenRead), SysFileOpenWrite = CreateCallback(Callback_SysFileOpenWrite), SysFileClose = CreateCallback(Callback_SysFileClose), SysFileSeek = CreateCallback(Callback_SysFileSeek), SysFileRead = CreateCallback(Callback_SysFileRead), SysFileWrite = CreateCallback(Callback_SysFileWrite), SysFileTime = CreateCallback(Callback_SysFileTime), SysMkDir = CreateCallback(Callback_SysMkDir), }; RegisterCallbacks(callbacks); } public override void Destroy() { for (int i = 0; i < MaxFileHandles; ++i) { if (fileHandles[i] != null) fileHandles[i].Dispose(); } base.Destroy(); } /// /// This matches struct unity_syscalls_s from uniquake.h in native code. /// [StructLayout(LayoutKind.Sequential, Pack = 0)] private class Callbacks { public IntPtr target; public IntPtr SysPrint; public IntPtr SysError; public IntPtr SysQuit; public IntPtr SysFloatTime; public IntPtr SysFileOpenRead; public IntPtr SysFileOpenWrite; public IntPtr SysFileClose; public IntPtr SysFileSeek; public IntPtr SysFileRead; public IntPtr SysFileWrite; public IntPtr SysFileTime; public IntPtr SysMkDir; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void SysPrintCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string message); [MonoPInvokeCallback(typeof(SysPrintCallback))] private static void Callback_SysPrint(IntPtr target, string message) { GetSelf(target).Print(message); } private void Print(string message) { // Debug.Log(message); // TODO: collect logs per frame and print after each Update loop } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void SysErrorCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string message); [MonoPInvokeCallback(typeof(SysErrorCallback))] private static void Callback_SysError(IntPtr target, string message) { GetSelf(target).Error(message); } private void Error(string message) { // Use an exception to emulate Quake's behavior of immediately terminating the game's code execution on error throw new QuakeException(message, 1); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void SysQuitCallback(IntPtr target); [MonoPInvokeCallback(typeof(SysQuitCallback))] private static void Callback_SysQuit(IntPtr target) { GetSelf(target).Quit(); } private void Quit() { // This exception will be caught by the UniQuake update loop where the game will be shut down throw new QuakeException("Quitting application normally"); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate double SysFloatTimeCallback(IntPtr target); [MonoPInvokeCallback(typeof(SysFloatTimeCallback))] private static double Callback_SysFloatTime(IntPtr target) { return GetSelf(target).FloatTime(); } private double FloatTime() { return uq.CurrentTime; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int SysFileOpenReadCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path, out int handle); [MonoPInvokeCallback(typeof(SysFileOpenReadCallback))] private static int Callback_SysFileOpenRead(IntPtr target, string path, out int handle) { return GetSelf(target).FileOpenRead(path, out handle); } private int FileOpenRead(string path, out int handle) { int i = FindFileHandle(); try { Debug.Log($"Opening file for reading: {path}"); FileStream fileStream = File.OpenRead(path); fileHandles[i] = fileStream; handle = i; return (int)fileStream.Length; } catch { handle = -1; return -1; } } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int SysFileOpenWriteCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path); [MonoPInvokeCallback(typeof(SysFileOpenWriteCallback))] private static int Callback_SysFileOpenWrite(IntPtr target, string path) { return GetSelf(target).FileOpenWrite(path); } private int FileOpenWrite(string path) { int i = FindFileHandle(); try { Debug.Log($"Opening file for writing: {path}"); FileStream fileStream = File.OpenWrite(path); fileHandles[i] = fileStream; return i; } catch (Exception ex) { Error($"Error opening {path}: {ex.Message}"); return -1; } } private int FindFileHandle() { for (int i = 1; i < MaxFileHandles; ++i) { if (fileHandles[i] == null) return i; } Error("Out of handles!"); return -1; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void SysFileCloseCallback(IntPtr target, int handle); [MonoPInvokeCallback(typeof(SysFileCloseCallback))] private static void Callback_SysFileClose(IntPtr target, int handle) { GetSelf(target).FileClose(handle); } private void FileClose(int handle) { if (handle < 0 || handle >= MaxFileHandles) { Debug.LogWarning($"Invalid file handle! {handle}"); return; } fileHandles[handle].Dispose(); fileHandles[handle] = null; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void SysFileSeekCallback(IntPtr target, int handle, int position); [MonoPInvokeCallback(typeof(SysFileSeekCallback))] private static void Callback_SysFileSeek(IntPtr target, int handle, int position) { GetSelf(target).FileSeek(handle, position); } private void FileSeek(int handle, int position) { if (handle < 0 || handle >= MaxFileHandles) { Debug.LogWarning($"Invalid file handle! {handle}"); return; } fileHandles[handle].Seek(position, SeekOrigin.Begin); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int SysFileReadCallback(IntPtr target, int handle, IntPtr dest, int count); [MonoPInvokeCallback(typeof(SysFileReadCallback))] private static int Callback_SysFileRead(IntPtr target, int handle, IntPtr dest, int count) { return GetSelf(target).FileRead(handle, dest, count); } private int FileRead(int handle, IntPtr dest, int count) { if (handle < 0 || handle >= MaxFileHandles) { Debug.LogWarning($"Invalid file handle! {handle}"); return 0; } byte[] buf = new byte[count]; // TODO: reuse buffer int numRead = fileHandles[handle].Read(buf, 0, count); Marshal.Copy(buf, 0, dest, numRead); return numRead; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int SysFileWriteCallback(IntPtr target, int handle, IntPtr data, int count); [MonoPInvokeCallback(typeof(SysFileWriteCallback))] private static int Callback_SysFileWrite(IntPtr target, int handle, IntPtr data, int count) { return GetSelf(target).FileWrite(handle, data, count); } private int FileWrite(int handle, IntPtr data, int count) { if (handle < 0 || handle >= MaxFileHandles) { Debug.LogWarning($"Invalid file handle! {handle}"); return 0; } byte[] buf = new byte[count]; // TODO: reuse buffer Marshal.Copy(data, buf, 0, count); fileHandles[handle].Write(buf, 0, count); return count; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int SysFileTimeCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path); [MonoPInvokeCallback(typeof(SysFileTimeCallback))] private static int Callback_SysFileTime(IntPtr target, string path) { return GetSelf(target).FileTime(path); } private int FileTime(string path) { // It would logically make more sense to return an actual file write time here, // but a signed 32-bit int is quite limited and we don't want any Year 2038 problems here. return File.Exists(path) ? 1 : -1; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void SysMkDirCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path); [MonoPInvokeCallback(typeof(SysMkDirCallback))] private static void Callback_SysMkDir(IntPtr target, string path) { GetSelf(target).MkDir(path); } private void MkDir(string path) { try { Directory.CreateDirectory(path); } catch { Debug.LogWarning($"Could not create directory: {path}"); } } }