using System; using System.IO; using System.Runtime.InteropServices; using UnityEngine; public partial class SystemModule { private const int MaxFileHandles = 50; private readonly UniQuake uq; private readonly FileStream[] fileHandles = new FileStream[MaxFileHandles]; public SystemModule(UniQuake uniQuake) { uq = uniQuake; BuildCallbacks(); } public override void Destroy() { for (int i = 0; i < MaxFileHandles; ++i) { if (fileHandles[i] != null) fileHandles[i].Dispose(); } base.Destroy(); } private void Print(string message) { if (string.IsNullOrWhiteSpace(message)) return; uq.AddLog(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); } 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"); } private double DoubleTime() { return uq.CurrentTime; } 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; } } private int FileOpenWrite(string path) { // TODO: we could use this method to redirect user config & save data to a platform-specific user storage 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; } private void FileClose(int handle) { if (handle < 0 || handle >= MaxFileHandles) { Debug.LogWarning($"Invalid file handle! {handle}"); return; } fileHandles[handle].Dispose(); fileHandles[handle] = null; } 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); } 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; } 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; } 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; } private bool MkDir(string path) { try { var info = Directory.CreateDirectory(path); return info.Exists; } catch (Exception ex) { Debug.LogWarning($"Could not create directory {path}, reason: {ex.Message}"); return false; } } }