diff --git a/Assets/Scripts/Data/QExtensions.cs b/Assets/Scripts/Data/QExtensions.cs index f7d35a0..fa68609 100644 --- a/Assets/Scripts/Data/QExtensions.cs +++ b/Assets/Scripts/Data/QExtensions.cs @@ -15,7 +15,7 @@ public static class QExtensions for (int i = 0; i < count; ++i) { result[i] = Marshal.PtrToStructure(current); - current = new IntPtr(current.ToInt64() + size); + current = IntPtr.Add(current, size); } return result; } diff --git a/Assets/Scripts/Modules/SystemModule.Interop.cs b/Assets/Scripts/Modules/SystemModule.Interop.cs index 8100ce4..a899ff1 100644 --- a/Assets/Scripts/Modules/SystemModule.Interop.cs +++ b/Assets/Scripts/Modules/SystemModule.Interop.cs @@ -50,18 +50,20 @@ public partial class SystemModule: CallbackHandler public IntPtr SysFileTime; public IntPtr SysMkDir; } - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void SysPrintCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string message); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private delegate void SysPrintCallback(IntPtr target, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(QuakeTextMarshaler))] string message); [MonoPInvokeCallback(typeof(SysPrintCallback))] private static void Callback_SysPrint(IntPtr target, string message) { GetSelf(target).Print(message); } - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void SysErrorCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string message); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private delegate void SysErrorCallback(IntPtr target, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(QuakeTextMarshaler))] string message); [MonoPInvokeCallback(typeof(SysErrorCallback))] private static void Callback_SysError(IntPtr target, string message) @@ -87,7 +89,7 @@ public partial class SystemModule: CallbackHandler return GetSelf(target).DoubleTime(); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private delegate int SysFileOpenReadCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path, out int handle); [MonoPInvokeCallback(typeof(SysFileOpenReadCallback))] @@ -96,7 +98,7 @@ public partial class SystemModule: CallbackHandler return GetSelf(target).FileOpenRead(path, out handle); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private delegate int SysFileOpenWriteCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path); [MonoPInvokeCallback(typeof(SysFileOpenWriteCallback))] @@ -141,7 +143,7 @@ public partial class SystemModule: CallbackHandler return GetSelf(target).FileWrite(handle, data, count); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private delegate int SysFileTimeCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path); [MonoPInvokeCallback(typeof(SysFileTimeCallback))] @@ -150,7 +152,7 @@ public partial class SystemModule: CallbackHandler return GetSelf(target).FileTime(path); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private delegate bool SysMkDirCallback(IntPtr target, [MarshalAs(UnmanagedType.LPStr)] string path); [MonoPInvokeCallback(typeof(SysMkDirCallback))] diff --git a/Assets/Scripts/QuakeTextMarshaler.cs b/Assets/Scripts/QuakeTextMarshaler.cs new file mode 100644 index 0000000..c01f2a2 --- /dev/null +++ b/Assets/Scripts/QuakeTextMarshaler.cs @@ -0,0 +1,87 @@ + using System; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// Quake uses a character set for its strings that is partially ASCII, but also partially nothing standard at all. + /// Inside the main PAK file is a character atlas texture and in order to print text, each char value inside of a + /// string is used to index a glyph in this atlas, which is then drawn to the screen. + /// + /// Most strings received from Quake will work fine with the default ANSI charset marshaling, however some mods will + /// define their own character sets and use byte values >127 to print special characters. This is where the ANSI + /// marshaler breaks down, as these bytes are interpreted to signify multi-byte characters and result in a String + /// conversion error. To remedy this, we define our own single-byte character set that reflects the default one from + /// Quake and decode strings received from the engine by ourselves. + /// + public class QuakeTextMarshaler: ICustomMarshaler + { + private static QuakeTextMarshaler instance = null; + + private static readonly char[] CharacterTable = + { + '·', ' ', ' ', ' ', ' ', '·', ' ', ' ', ' ', ' ', '\n', ' ', ' ', ' ', '·', '·', + '[', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '·', ' ', ' ', ' ', + ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '←', + '·', ' ', ' ', ' ', ' ', '·', ' ', ' ', ' ', ' ', '\n', ' ', ' ', ' ', '·', '·', + '[', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '·', ' ', ' ', ' ', + ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '←', + }; + + public IntPtr MarshalManagedToNative(object managedObj) + { + if (managedObj == null) + return IntPtr.Zero; + + if (!(managedObj is string str)) + throw new MarshalDirectiveException("QuakeTextMarshaler must be used on a string."); + + throw new NotImplementedException("QuakeTextMarshaler can only be used to decode text from Quake."); + } + + public object MarshalNativeToManaged(IntPtr pNativeData) + { + byte b; + + var sb = new StringBuilder(); + while ((b = Marshal.ReadByte(pNativeData)) != 0) + { + sb.Append(CharacterTable[b]); + pNativeData = IntPtr.Add(pNativeData, 1); + } + + return sb.ToString(); + } + + public void CleanUpManagedData(object managedObj) + { + } + + public void CleanUpNativeData(IntPtr pNativeData) + { + // Free HGlobal memory if we decide to implement MarshalManagedToNative + } + + public int GetNativeDataSize() + { + return -1; + } + + public static ICustomMarshaler GetInstance(string cookie) + { + if (instance == null) + return instance = new QuakeTextMarshaler(); + + return instance; + } + } + \ No newline at end of file diff --git a/Assets/Scripts/QuakeTextMarshaler.cs.meta b/Assets/Scripts/QuakeTextMarshaler.cs.meta new file mode 100644 index 0000000..38a8a91 --- /dev/null +++ b/Assets/Scripts/QuakeTextMarshaler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 23e2ec4793084710924ba442860bf2d6 +timeCreated: 1618677615 \ No newline at end of file