using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using RobloxFiles.BinaryFormat.Chunks; namespace RobloxFiles.BinaryFormat { public class BinaryRobloxFileWriter : BinaryWriter { public IBinaryFileChunk Chunk { get; private set; } public bool WritingChunk { get; private set; } public string ChunkType { get; private set; } public long ChunkStart { get; private set; } public Dictionary ClassMap; public readonly BinaryRobloxFile File; // Instances in parent->child order public List Instances; // Instances in child->parent order public List PostInstances; public BinaryRobloxFileWriter(BinaryRobloxFile file, Stream workBuffer = null) : base(workBuffer ?? new MemoryStream()) { File = file; ChunkStart = 0; ChunkType = ""; Instances = new List(); PostInstances = new List(); ClassMap = new Dictionary(); } public static int SizeOf() where T : struct { int result = 1; if (typeof(T) != typeof(bool)) result = Marshal.SizeOf(); return result; } private static byte[] GetBytes(T value, int bufferSize, IntPtr converter) { byte[] bytes = new byte[bufferSize]; Marshal.StructureToPtr(value, converter, true); Marshal.Copy(converter, bytes, 0, bufferSize); return bytes; } public static byte[] GetBytes(T value) where T : struct { int bufferSize = SizeOf(); IntPtr converter = Marshal.AllocHGlobal(bufferSize); var result = GetBytes(value, bufferSize, converter); Marshal.FreeHGlobal(converter); return result; } // Writes 'count * sizeof(T)' interleaved bytes from a List of T where // each value in the array will be encoded using the provided 'encode' function. public void WriteInterleaved(List values, Func encode = null) where T : struct { int count = values.Count; int bufferSize = SizeOf(); byte[][] blocks = new byte[count][]; IntPtr converter = Marshal.AllocHGlobal(bufferSize); for (int i = 0; i < count; i++) { T value = values[i]; if (encode != null) value = encode(value); blocks[i] = GetBytes(value, bufferSize, converter); } for (int layer = bufferSize - 1; layer >= 0; layer--) { for (int i = 0; i < count; i++) { byte value = blocks[i][layer]; Write(value); } } Marshal.FreeHGlobal(converter); } // Encodes an int for an interleaved buffer. private int EncodeInt(int value) { return (value << 1) ^ (value >> 31); } // Encodes a float for an interleaved buffer. private float EncodeFloat(float value) { byte[] buffer = BitConverter.GetBytes(value); uint bits = BitConverter.ToUInt32(buffer, 0); bits = (bits << 1) | (bits >> 31); buffer = BitConverter.GetBytes(bits); return BitConverter.ToSingle(buffer, 0); } // Writes an interleaved list of integers. public void WriteInts(List values) { WriteInterleaved(values, EncodeInt); } // Writes an interleaved list of floats public void WriteFloats(List values) { WriteInterleaved(values, EncodeFloat); } // Accumulatively writes an interleaved array of integers. public void WriteInstanceIds(List values) { int numIds = values.Count; var instIds = new List(values); for (int i = 1; i < numIds; ++i) instIds[i] -= values[i - 1];; WriteInts(instIds); } // Writes a string to the buffer with the option to exclude a length prefix. public void WriteString(string value, bool raw = false) { byte[] buffer = Encoding.UTF8.GetBytes(value); if (!raw) { int length = buffer.Length; Write(length); } Write(buffer); } internal void RecordInstances(IEnumerable instances) { foreach (Instance instance in instances) { if (!instance.Archivable) continue; int instId = (int)(File.NumInstances++); instance.Referent = instId.ToString(); Instances.Add(instance); string className = instance.ClassName; INST inst = null; if (!ClassMap.ContainsKey(className)) { inst = new INST() { ClassName = className, InstanceIds = new List(), IsService = instance.IsService }; ClassMap.Add(className, inst); } else { inst = ClassMap[className]; } inst.NumInstances++; inst.InstanceIds.Add(instId); RecordInstances(instance.GetChildren()); PostInstances.Add(instance); } } internal void ApplyClassMap() { File.Instances = Instances.ToArray(); var classNames = ClassMap .Select(type => type.Key) .ToList(); classNames.Sort(StringComparer.Ordinal); var classes = classNames .Select(className => ClassMap[className]) .ToArray(); for (int i = 0; i < classes.Length; i++, File.NumClasses++) { string className = classNames[i]; INST inst = ClassMap[className]; inst.ClassIndex = i; } File.Classes = classes; } // Marks that we are writing a chunk. public bool StartWritingChunk(string chunkType) { if (chunkType.Length != 4) throw new Exception("BinaryFileWriter.StartWritingChunk - ChunkType length should be 4!"); if (!WritingChunk) { WritingChunk = true; ChunkType = chunkType; ChunkStart = BaseStream.Position; return true; } return false; } // Marks that we are writing a chunk. public bool StartWritingChunk(IBinaryFileChunk chunk) { if (!WritingChunk) { string chunkType = chunk.GetType().Name; StartWritingChunk(chunkType); Chunk = chunk; return true; } return false; } // Compresses the data that was written into a BinaryRobloxFileChunk and writes it. public BinaryRobloxFileChunk FinishWritingChunk(bool compress = true) { if (!WritingChunk) throw new Exception($"BinaryRobloxFileWriter: Cannot finish writing a chunk without starting one!"); // Create the compressed chunk. var chunk = new BinaryRobloxFileChunk(this, compress); // Clear out the uncompressed data. BaseStream.Position = ChunkStart; BaseStream.SetLength(ChunkStart); // Write the compressed chunk. chunk.Handler = Chunk; chunk.WriteChunk(this); // Clean up for the next chunk. WritingChunk = false; ChunkStart = 0; ChunkType = ""; Chunk = null; return chunk; } public void SaveChunk(IBinaryFileChunk handler) { var chunk = handler.SaveAsChunk(this); File.Chunks.Add(chunk); } public BinaryRobloxFileChunk WriteEndChunk() { StartWritingChunk("END\0"); WriteString("", true); return FinishWritingChunk(false); } } }