Added write support for binary files!

Holy cow, this took a lot of work. I think I may need to do a few more
things before I consider this a 1.0 release, but I'm glad to have
finally overcome this hurdle!
This commit is contained in:
CloneTrooper1019
2019-06-07 22:43:28 -05:00
parent cb063d1ada
commit 47112242e7
22 changed files with 1405 additions and 222 deletions

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace RobloxFiles.BinaryFormat
{
public class BinaryRobloxFileReader : BinaryReader
{
public readonly BinaryRobloxFile File;
private byte[] lastStringBuffer = new byte[0] { };
public BinaryRobloxFileReader(BinaryRobloxFile file, Stream stream) : base(stream)
{
File = file;
}
// Reads 'count * sizeof(T)' interleaved bytes and converts
// them into an array of T[count] where each value in the
// array has been decoded by the provided 'decode' function.
public T[] ReadInterleaved<T>(int count, Func<byte[], int, T> decode) where T : struct
{
int bufferSize = Marshal.SizeOf<T>();
byte[] interleaved = ReadBytes(count * bufferSize);
T[] values = new T[count];
for (int i = 0; i < count; i++)
{
long buffer = 0;
for (int column = 0; column < bufferSize; column++)
{
long block = interleaved[(column * count) + i];
int shift = (bufferSize - column - 1) * 8;
buffer |= (block << shift);
}
byte[] sequence = BitConverter.GetBytes(buffer);
values[i] = decode(sequence, 0);
}
return values;
}
// Decodes an int from an interleaved buffer.
private int DecodeInt(byte[] buffer, int startIndex)
{
int value = BitConverter.ToInt32(buffer, startIndex);
return (value >> 1) ^ (-(value & 1));
}
// Decodes a float from an interleaved buffer.
private float DecodeFloat(byte[] buffer, int startIndex)
{
uint u = BitConverter.ToUInt32(buffer, startIndex);
uint i = (u >> 1) | (u << 31);
byte[] b = BitConverter.GetBytes(i);
return BitConverter.ToSingle(b, 0);
}
// Reads an interleaved buffer of integers.
public int[] ReadInts(int count)
{
return ReadInterleaved(count, DecodeInt);
}
// Reads an interleaved buffer of floats.
public float[] ReadFloats(int count)
{
return ReadInterleaved(count, DecodeFloat);
}
// Reads an interleaved buffer of unsigned integers.
public uint[] ReadUInts(int count)
{
return ReadInterleaved(count, BitConverter.ToUInt32);
}
// Reads and accumulates an interleaved buffer of integers.
public List<int> ReadInstanceIds(int count)
{
int[] values = ReadInts(count);
for (int i = 1; i < count; ++i)
values[i] += values[i - 1];
return values.ToList();
}
public override string ReadString()
{
int length = ReadInt32();
byte[] buffer = ReadBytes(length);
lastStringBuffer = buffer;
return Encoding.UTF8.GetString(buffer);
}
public float ReadFloat()
{
return ReadSingle();
}
public byte[] GetLastStringBuffer()
{
return lastStringBuffer;
}
}
}

View File

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.IO;
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<string, INST> TypeMap;
public readonly BinaryRobloxFile File;
public List<Instance> Instances;
public BinaryRobloxFileWriter(BinaryRobloxFile file, Stream workBuffer = null) : base(workBuffer ?? new MemoryStream())
{
File = file;
ChunkStart = 0;
ChunkType = "";
Instances = new List<Instance>();
TypeMap = new Dictionary<string, INST>();
}
private static byte[] GetBytes<T>(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>(T value) where T : struct
{
int bufferSize = Marshal.SizeOf<T>();
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<T>(List<T> values, Func<T, T> encode = null) where T : struct
{
int count = values.Count;
int bufferSize = Marshal.SizeOf<T>();
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 ints.
public void WriteInts(List<int> values)
{
WriteInterleaved(values, EncodeInt);
}
// Writes an interleaved list of floats
public void WriteFloats(List<float> values)
{
WriteInterleaved(values, EncodeFloat);
}
// Writes an accumlated array of integers.
public void WriteInstanceIds(List<int> values)
{
int numIds = values.Count;
var instIds = new List<int>(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<Instance> instances)
{
foreach (Instance instance in instances)
{
int instId = (int)(File.NumInstances++);
instance.Referent = instId.ToString();
Instances.Add(instance);
string className = instance.ClassName;
INST inst = null;
if (!TypeMap.ContainsKey(className))
{
inst = new INST()
{
TypeName = className,
InstanceIds = new List<int>(),
IsService = instance.IsService
};
TypeMap.Add(className, inst);
}
else
{
inst = TypeMap[className];
}
inst.NumInstances++;
inst.InstanceIds.Add(instId);
RecordInstances(instance.GetChildren());
}
}
// 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;
}
}
}