Browse Source

Added a custom marshaler to decode screen texts received from Quake, which uses single-byte characters and a non-standard character set. Fixes string conversion errors and a fatal exception whenever the "ö" character is printed in the Zerstörer mod.

console
Nico de Poel 5 years ago
parent
commit
ab880f9cfb
  1. 2
      Assets/Scripts/Data/QExtensions.cs
  2. 22
      Assets/Scripts/Modules/SystemModule.Interop.cs
  3. 87
      Assets/Scripts/QuakeTextMarshaler.cs
  4. 3
      Assets/Scripts/QuakeTextMarshaler.cs.meta

2
Assets/Scripts/Data/QExtensions.cs

@ -15,7 +15,7 @@ public static class QExtensions
for (int i = 0; i < count; ++i)
{
result[i] = Marshal.PtrToStructure<TStruct>(current);
current = new IntPtr(current.ToInt64() + size);
current = IntPtr.Add(current, size);
}
return result;
}

22
Assets/Scripts/Modules/SystemModule.Interop.cs

@ -50,18 +50,20 @@ public partial class SystemModule: CallbackHandler<SystemModule>
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<SystemModule>
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<SystemModule>
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<SystemModule>
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<SystemModule>
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))]

87
Assets/Scripts/QuakeTextMarshaler.cs

@ -0,0 +1,87 @@
 using System;
using System.Runtime.InteropServices;
using System.Text;
/// <summary>
/// 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.
/// </summary>
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;
}
}

3
Assets/Scripts/QuakeTextMarshaler.cs.meta

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 23e2ec4793084710924ba442860bf2d6
timeCreated: 1618677615
Loading…
Cancel
Save