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 static string GetString(byte[] bytes) { var sb = new StringBuilder(bytes.Length); for (int i = 0; i < bytes.Length; ++i) { sb.Append(CharacterTable[bytes[i]]); } 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; } }