using System; using System.Collections.Generic; using System.IO; using System.Text; namespace XIVChatCommon { public static class XivString { private const byte START = 2; private const byte END = 3; public static List ToChunks(byte[] bytes) { var chunks = new List(); var stringBytes = new List(); var italic = false; uint? foreground = null; uint? glow = null; Action appendCurrent = (bool clear) => { var text = Encoding.UTF8.GetString(stringBytes.ToArray()); chunks.Add(new TextChunk { Foreground = foreground, Glow = glow, Italic = italic, Content = text, }); if (clear) { stringBytes.Clear(); } }; var reader = new BinaryReader(new MemoryStream(bytes)); while (reader.BaseStream.Position < reader.BaseStream.Length) { var b = reader.ReadByte(); if (b == START) { var kind = reader.ReadByte(); // kind var len = GetInteger(reader); // data length var data = new BinaryReader(new MemoryStream(reader.ReadBytes((int)len))); // data var end = reader.ReadByte(); // end if (end != END) { throw new ArgumentException("Input was not a valid XivString"); } switch (kind) { // icon processing case 0x12: var spriteIndex = GetInteger(data); chunks.Add(new IconChunk { Index = (byte)spriteIndex, }); break; // italics processing case 0x1a: var newStatus = GetInteger(data) == 1; var appendNow = (italic && !newStatus) || (!italic && newStatus); if (!appendNow) { break; } appendCurrent(true); italic = newStatus; break; // foreground case 0x48: break; // glow case 0x49: break; } continue; } stringBytes.Add(b); } return chunks; } public static string GetText(byte[] bytes) { var stringBytes = new List(); var reader = new BinaryReader(new MemoryStream(bytes)); while (reader.BaseStream.Position < reader.BaseStream.Length) { var b = reader.ReadByte(); if (b == START) { reader.ReadByte(); // kind var len = GetInteger(reader); // data length reader.ReadBytes((int)len); // data var end = reader.ReadByte(); // end if (end != END) { throw new ArgumentException("Input was not a valid XivString"); } continue; } stringBytes.Add(b); } return Encoding.UTF8.GetString(stringBytes.ToArray()); } // Thanks, Dalamud protected enum IntegerType { // used as an internal marker; sometimes single bytes are bare with no marker at all None = 0, Byte = 0xF0, ByteTimes256 = 0xF1, Int16 = 0xF2, ByteSHL16 = 0xF3, Int16Packed = 0xF4, // seen in map links, seemingly 2 8-bit values packed into 2 bytes with only one marker Int16SHL8 = 0xF5, Int24Special = 0xF6, // unsure how different form Int24 - used for hq items that add 1 million, also used for normal 24-bit values in map links Int8SHL24 = 0xF7, Int8SHL8Int8 = 0xF8, Int8SHL8Int8SHL8 = 0xF9, Int24 = 0xFA, Int16SHL16 = 0xFB, Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not?? Int16Int8SHL8 = 0xFD, Int32 = 0xFE } private static uint GetInteger(BinaryReader input) { var t = input.ReadByte(); var type = (IntegerType)t; return GetInteger(input, type); } private static uint GetInteger(BinaryReader input, IntegerType type) { const byte ByteLengthCutoff = 0xF0; var t = (byte)type; if (t < ByteLengthCutoff) { return (uint)(t - 1); } switch (type) { case IntegerType.Byte: return input.ReadByte(); case IntegerType.ByteTimes256: return input.ReadByte() * (uint)256; case IntegerType.ByteSHL16: return (uint)(input.ReadByte() << 16); case IntegerType.Int8SHL24: return (uint)(input.ReadByte() << 24); case IntegerType.Int8SHL8Int8: { var v = 0; v |= input.ReadByte() << 24; v |= input.ReadByte(); return (uint)v; } case IntegerType.Int8SHL8Int8SHL8: { var v = 0; v |= input.ReadByte() << 24; v |= input.ReadByte() << 8; return (uint)v; } case IntegerType.Int16: // fallthrough - same logic case IntegerType.Int16Packed: { var v = 0; v |= input.ReadByte() << 8; v |= input.ReadByte(); return (uint)v; } case IntegerType.Int16SHL8: { var v = 0; v |= input.ReadByte() << 16; v |= input.ReadByte() << 8; return (uint)v; } case IntegerType.Int16SHL16: { var v = 0; v |= input.ReadByte() << 24; v |= input.ReadByte() << 16; return (uint)v; } case IntegerType.Int24Special: // Fallthrough - same logic case IntegerType.Int24Packed: // fallthrough again case IntegerType.Int24: { var v = 0; v |= input.ReadByte() << 16; v |= input.ReadByte() << 8; v |= input.ReadByte(); return (uint)v; } case IntegerType.Int16Int8SHL8: { var v = 0; v |= input.ReadByte() << 24; v |= input.ReadByte() << 16; v |= input.ReadByte() << 8; return (uint)v; } case IntegerType.Int32: { var v = 0; v |= input.ReadByte() << 24; v |= input.ReadByte() << 16; v |= input.ReadByte() << 8; v |= input.ReadByte(); return (uint)v; } default: throw new NotSupportedException(); } } } }