diff --git a/.gitignore b/.gitignore index b7205ba..6c1f512 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ obj/* packages/* .vs/* *.suo -*.ide \ No newline at end of file +*.ide +*.user \ No newline at end of file diff --git a/BinaryFormat/Chunk.cs b/BinaryFormat/BinaryChunk.cs similarity index 96% rename from BinaryFormat/Chunk.cs rename to BinaryFormat/BinaryChunk.cs index 8471631..3d5f853 100644 --- a/BinaryFormat/Chunk.cs +++ b/BinaryFormat/BinaryChunk.cs @@ -20,7 +20,7 @@ namespace Roblox.BinaryFormat public override string ToString() { - return ChunkType + " Chunk [" + Size + ']'; + return ChunkType + " Chunk [" + Size + " bytes]"; } public RobloxBinaryReader GetReader(string chunkType) diff --git a/BinaryFormat/BinaryFile.cs b/BinaryFormat/BinaryFile.cs index c4c614d..3d0e912 100644 --- a/BinaryFormat/BinaryFile.cs +++ b/BinaryFormat/BinaryFile.cs @@ -1,81 +1,85 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Text; using Roblox.BinaryFormat.Chunks; namespace Roblox.BinaryFormat { - public class RobloxBinaryFile : RobloxFile + public class RobloxBinaryFile : IRobloxFile { - public const string FileSignature = " BinaryChunks = new List(); + // Header Specific + public const string MagicHeader = " INSTs = new Dictionary(); - public readonly List PROPs = new List(); + // IRobloxFile + public List BinaryTrunk = new List(); + public IReadOnlyList Trunk => BinaryTrunk.AsReadOnly(); - public readonly RobloxInstance[] Instances; - - public readonly ushort Version; - public readonly uint NumTypes; - public readonly uint NumInstances; - public readonly long Reserved; - - public RobloxBinaryFile(byte[] contents) + // Runtime Specific + public List Chunks = new List(); + public override string ToString() => GetType().Name; + + public Instance[] Instances; + public META Metadata; + public INST[] Types; + + public void Initialize(byte[] contents) { using (MemoryStream file = new MemoryStream(contents)) using (RobloxBinaryReader reader = new RobloxBinaryReader(file)) { + // Verify the signature of the file. byte[] binSignature = reader.ReadBytes(14); string signature = Encoding.UTF7.GetString(binSignature); - if (signature != FileSignature) - throw new InvalidDataException("Signature does not match RobloxBinaryFile.FileSignature!"); + if (signature != MagicHeader) + throw new InvalidDataException("Provided file's signature does not match RobloxBinaryFile.MagicHeader!"); + // Read header data. Version = reader.ReadUInt16(); NumTypes = reader.ReadUInt32(); NumInstances = reader.ReadUInt32(); - Reserved = reader.ReadInt64(); + Reserved = reader.ReadBytes(8); // Begin reading the file chunks. bool reading = true; - Instances = new RobloxInstance[NumInstances]; - BinaryChunks = new List(); + Types = new INST[NumTypes]; + Instances = new Instance[NumInstances]; + while (reading) { try { RobloxBinaryChunk chunk = new RobloxBinaryChunk(reader); - BinaryChunks.Add(chunk); + Chunks.Add(chunk); switch (chunk.ChunkType) { case "INST": - INST inst = new INST(chunk); - INSTs.Add(inst.TypeIndex, inst); + INST type = new INST(chunk); + type.Allocate(this); break; case "PROP": - PROP prop = new PROP(chunk); - PROPs.Add(prop); + PROP.ReadProperties(this, chunk); break; case "PRNT": PRNT prnt = new PRNT(chunk); - ParentIds = prnt; + prnt.Assemble(this); break; case "META": - META meta = new META(chunk); - Metadata = meta; + Metadata = new META(chunk); break; case "END\0": reading = false; break; default: - BinaryChunks.Remove(chunk); + Chunks.Remove(chunk); break; } } @@ -84,40 +88,6 @@ namespace Roblox.BinaryFormat throw new Exception("Unexpected end of file!"); } } - - foreach (INST chunk in INSTs.Values) - { - foreach (int id in chunk.InstanceIds) - { - RobloxInstance inst = new RobloxInstance(); - inst.ClassName = chunk.TypeName; - Instances[id] = inst; - } - } - - foreach (PROP prop in PROPs) - { - INST chunk = INSTs[prop.Index]; - prop.ReadPropertyValues(chunk, Instances); - } - - for (int i = 0; i < ParentIds.NumRelations; i++) - { - int childId = ParentIds.ChildrenIds[i]; - int parentId = ParentIds.ParentIds[i]; - - RobloxInstance child = Instances[childId]; - - if (parentId >= 0) - { - var parent = Instances[parentId]; - child.Parent = parent; - } - else - { - Trunk.Add(child); - } - } } } } diff --git a/BinaryFormat/BinaryReader.cs b/BinaryFormat/BinaryReader.cs new file mode 100644 index 0000000..e2d43f5 --- /dev/null +++ b/BinaryFormat/BinaryReader.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Roblox.BinaryFormat +{ + public class RobloxBinaryReader : BinaryReader + { + public RobloxBinaryReader(Stream stream) : base(stream) { } + private byte[] lastStringBuffer = new byte[0] { }; + + public T[] ReadInterlaced(int count, Func decode) where T : struct + { + int bytesPerBlock = Marshal.SizeOf(); + byte[] interlaced = ReadBytes(count * bytesPerBlock); + + T[] values = new T[count]; + + for (int i = 0; i < count; i++) + { + long block = 0; + + for (int pack = 0; pack < bytesPerBlock; pack++) + { + long bits = interlaced[(pack * count) + i]; + int shift = (bytesPerBlock - pack - 1) * 8; + block |= (bits << shift); + } + + byte[] buffer = BitConverter.GetBytes(block); + values[i] = decode(buffer, 0); + } + + return values; + } + + private int ReadInterlacedInt(byte[] buffer, int startIndex) + { + int value = BitConverter.ToInt32(buffer, startIndex); + return (value >> 1) ^ (-(value & 1)); + } + + private float ReadInterlacedFloat(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); + } + + public int[] ReadInts(int count) + { + return ReadInterlaced(count, ReadInterlacedInt); + } + + public float[] ReadFloats(int count) + { + return ReadInterlaced(count, ReadInterlacedFloat); + } + + public int[] ReadInstanceIds(int count) + { + int[] values = ReadInts(count); + + for (int i = 1; i < count; ++i) + values[i] += values[i - 1]; + + return values; + } + + 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; + } + } +} \ No newline at end of file diff --git a/BinaryFormat/ChunkTypes/INST.cs b/BinaryFormat/ChunkTypes/INST.cs index 1fd0dee..bd43087 100644 --- a/BinaryFormat/ChunkTypes/INST.cs +++ b/BinaryFormat/ChunkTypes/INST.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.IO; - -namespace Roblox.BinaryFormat.Chunks +namespace Roblox.BinaryFormat.Chunks { public class INST { @@ -11,8 +8,6 @@ namespace Roblox.BinaryFormat.Chunks public readonly int NumInstances; public readonly int[] InstanceIds; - public Dictionary Properties; - public override string ToString() { return TypeName; @@ -29,8 +24,19 @@ namespace Roblox.BinaryFormat.Chunks NumInstances = reader.ReadInt32(); InstanceIds = reader.ReadInstanceIds(NumInstances); } + } - Properties = new Dictionary(); + public void Allocate(RobloxBinaryFile file) + { + foreach (int instId in InstanceIds) + { + Instance inst = new Instance(); + inst.ClassName = TypeName; + + file.Instances[instId] = inst; + } + + file.Types[TypeIndex] = this; } } } diff --git a/BinaryFormat/ChunkTypes/META.cs b/BinaryFormat/ChunkTypes/META.cs index 0d8a0d2..890f64d 100644 --- a/BinaryFormat/ChunkTypes/META.cs +++ b/BinaryFormat/ChunkTypes/META.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; namespace Roblox.BinaryFormat.Chunks { diff --git a/BinaryFormat/ChunkTypes/PRNT.cs b/BinaryFormat/ChunkTypes/PRNT.cs index 3da3969..40784d2 100644 --- a/BinaryFormat/ChunkTypes/PRNT.cs +++ b/BinaryFormat/ChunkTypes/PRNT.cs @@ -19,5 +19,26 @@ ParentIds = reader.ReadInstanceIds(NumRelations); } } + + public void Assemble(RobloxBinaryFile file) + { + for (int i = 0; i < NumRelations; i++) + { + int childId = ChildrenIds[i]; + int parentId = ParentIds[i]; + + Instance child = file.Instances[childId]; + + if (parentId >= 0) + { + Instance parent = file.Instances[parentId]; + child.Parent = parent; + } + else + { + file.BinaryTrunk.Add(child); + } + } + } } } diff --git a/BinaryFormat/ChunkTypes/PROP.cs b/BinaryFormat/ChunkTypes/PROP.cs index 36a6088..36afbd6 100644 --- a/BinaryFormat/ChunkTypes/PROP.cs +++ b/BinaryFormat/ChunkTypes/PROP.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using Roblox.Enums; @@ -10,60 +9,46 @@ namespace Roblox.BinaryFormat.Chunks { public class PROP { - public int Index { get; private set; } - public string Name { get; private set; } - - public readonly PropertyType Type; - public RobloxProperty[] Properties => props; - - private RobloxBinaryReader reader; - private RobloxProperty[] props; - - public override string ToString() + public static void ReadProperties(RobloxBinaryFile file, RobloxBinaryChunk chunk) { - Type PropertyType = typeof(PropertyType); - return '[' + Enum.GetName(PropertyType, Type) + "] " + Name; - } + RobloxBinaryReader reader = chunk.GetReader("PROP"); - public PROP(RobloxBinaryChunk chunk) - { - reader = chunk.GetReader("PROP"); - - Index = reader.ReadInt32(); - Name = reader.ReadString(); + // Read the property's header info. + int typeIndex = reader.ReadInt32(); + string name = reader.ReadString(); + PropertyType propType; try { - byte propType = reader.ReadByte(); - Type = (PropertyType)propType; + byte typeId = reader.ReadByte(); + propType = (PropertyType)typeId; } catch { - Type = PropertyType.Unknown; + propType = PropertyType.Unknown; } - } - public void ReadPropertyValues(INST instChunk, RobloxInstance[] instMap) - { - int[] ids = instChunk.InstanceIds; - int instCount = ids.Length; + // Create access arrays for the objects we will be adding properties to. + INST type = file.Types[typeIndex]; + Property[] props = new Property[type.NumInstances]; - props = new RobloxProperty[instCount]; + int[] ids = type.InstanceIds; + int instCount = type.NumInstances; for (int i = 0; i < instCount; i++) { - RobloxProperty prop = new RobloxProperty(); - prop.Name = Name; - prop.Type = Type; + int instId = ids[i]; - Properties[i] = prop; - instMap[ids[i]].Properties.Add(prop); + Property prop = new Property(); + prop.Name = name; + prop.Type = propType; + props[i] = prop; + + Instance inst = file.Instances[instId]; + inst.AddProperty(ref prop); } - // Setup some short-hand functions for frequently used actions. - var readInstanceInts = new Func(() => reader.ReadInts(instCount)); - var readInstanceFloats = new Func(() => reader.ReadFloats(instCount)); - + // Setup some short-hand functions for actions frequently used during the read procedure. var loadProperties = new Action>(read => { for (int i = 0; i < instCount; i++) @@ -73,46 +58,67 @@ namespace Roblox.BinaryFormat.Chunks } }); - // Process the property data based on the property type. - switch (Type) + var readInts = new Func(() => reader.ReadInts(instCount)); + var readFloats = new Func(() => reader.ReadFloats(instCount)); + + // Read the property data based on the property type. + switch (propType) { case PropertyType.String: - loadProperties(i => reader.ReadString()); + loadProperties(i => + { + string result = reader.ReadString(); + + // Leave an access point for the original byte sequence, in case this is a BinaryString. + // This will allow the developer to read the sequence without any mangling from C# strings. + byte[] buffer = reader.GetLastStringBuffer(); + props[i].SetRawBuffer(buffer); + + return result; + }); + break; case PropertyType.Bool: loadProperties(i => reader.ReadBoolean()); break; case PropertyType.Int: - int[] ints = readInstanceInts(); + int[] ints = readInts(); loadProperties(i => ints[i]); break; case PropertyType.Float: - float[] floats = readInstanceFloats(); + float[] floats = readFloats(); loadProperties(i => floats[i]); break; case PropertyType.Double: loadProperties(i => reader.ReadDouble()); break; case PropertyType.UDim: - float[] scales = readInstanceFloats(); - int[] offsets = readInstanceInts(); + float[] UDim_Scales = readFloats(); + int[] UDim_Offsets = readInts(); loadProperties(i => { - float scale = scales[i]; - int offset = offsets[i]; + float scale = UDim_Scales[i]; + int offset = UDim_Offsets[i]; return new UDim(scale, offset); }); break; case PropertyType.UDim2: - float[] scalesX = readInstanceFloats(), scalesY = readInstanceFloats(); - int[] offsetsX = readInstanceInts(), offsetsY = readInstanceInts(); + float[] UDim2_Scales_X = readFloats(), + UDim2_Scales_Y = readFloats(); + + int[] UDim2_Offsets_X = readInts(), + UDim2_Offsets_Y = readInts(); loadProperties(i => { - float scaleX = scalesX[i], scaleY = scalesY[i]; - int offsetX = offsetsX[i], offsetY = offsetsY[i]; + float scaleX = UDim2_Scales_X[i], + scaleY = UDim2_Scales_Y[i]; + + int offsetX = UDim2_Offsets_X[i], + offsetY = UDim2_Offsets_Y[i]; + return new UDim2(scaleX, offsetX, scaleY, offsetY); }); @@ -147,19 +153,19 @@ namespace Roblox.BinaryFormat.Chunks break; case PropertyType.BrickColor: - int[] brickColors = readInstanceInts(); + int[] brickColors = readInts(); loadProperties(i => { int number = brickColors[i]; - return BrickColor.New(number); + return BrickColor.FromNumber(number); }); break; case PropertyType.Color3: - float[] color3_R = readInstanceFloats(), - color3_G = readInstanceFloats(), - color3_B = readInstanceFloats(); + float[] color3_R = readFloats(), + color3_G = readFloats(), + color3_B = readFloats(); loadProperties(i => { @@ -172,8 +178,8 @@ namespace Roblox.BinaryFormat.Chunks break; case PropertyType.Vector2: - float[] vector2_X = readInstanceFloats(), - vector2_Y = readInstanceFloats(); + float[] vector2_X = readFloats(), + vector2_Y = readFloats(); loadProperties(i => { @@ -185,9 +191,9 @@ namespace Roblox.BinaryFormat.Chunks break; case PropertyType.Vector3: - float[] vector3_X = readInstanceFloats(), - vector3_Y = readInstanceFloats(), - vector3_Z = readInstanceFloats(); + float[] vector3_X = readFloats(), + vector3_Y = readFloats(), + vector3_Z = readFloats(); loadProperties(i => { @@ -206,14 +212,17 @@ namespace Roblox.BinaryFormat.Chunks loadProperties(i => { - byte orientId = reader.ReadByte(); + int normalXY = reader.ReadByte(); - if (orientId > 0) + if (normalXY > 0) { - NormalId normX = (NormalId)((orientId - 1) / 6); + // Make sure this value is in a safe range. + normalXY = (normalXY - 1) % 36; + + NormalId normX = (NormalId)((normalXY - 1) / 6); Vector3 R0 = Vector3.FromNormalId(normX); - NormalId normY = (NormalId)((orientId - 1) % 6); + NormalId normY = (NormalId)((normalXY - 1) % 6); Vector3 R1 = Vector3.FromNormalId(normY); // Compute R2 using the cross product of R0 and R1. @@ -227,12 +236,10 @@ namespace Roblox.BinaryFormat.Chunks R2.X, R2.Y, R2.Z, }; } - else if (Type == PropertyType.Quaternion) + else if (propType == PropertyType.Quaternion) { - float qx = reader.ReadSingle(), - qy = reader.ReadSingle(), - qz = reader.ReadSingle(), - qw = reader.ReadSingle(); + float qx = reader.ReadFloat(), qy = reader.ReadFloat(), + qz = reader.ReadFloat(), qw = reader.ReadFloat(); Quaternion quat = new Quaternion(qx, qy, qz, qw); var rotation = quat.ToCFrame(); @@ -245,7 +252,7 @@ namespace Roblox.BinaryFormat.Chunks for (int m = 0; m < 9; m++) { - float value = reader.ReadSingle(); + float value = reader.ReadFloat(); matrix[m] = value; } @@ -253,9 +260,9 @@ namespace Roblox.BinaryFormat.Chunks } }); - float[] cframe_X = readInstanceFloats(), - cframe_Y = readInstanceFloats(), - cframe_Z = readInstanceFloats(); + float[] cframe_X = readFloats(), + cframe_Y = readFloats(), + cframe_Z = readFloats(); loadProperties(i => { @@ -273,8 +280,12 @@ namespace Roblox.BinaryFormat.Chunks break; case PropertyType.Enum: - uint[] enums = reader.ReadInterwovenValues(instCount, BitConverter.ToUInt32); + // TODO: I want to map these values to actual Roblox enums, but I'll have to add an + // interpreter for the JSON API Dump to do it properly. + + uint[] enums = reader.ReadInterlaced(instCount, BitConverter.ToUInt32); loadProperties(i => enums[i]); + break; case PropertyType.Ref: int[] instIds = reader.ReadInstanceIds(instCount); @@ -282,7 +293,7 @@ namespace Roblox.BinaryFormat.Chunks loadProperties(i => { int instId = instIds[i]; - return instId >= 0 ? instMap[instId] : null; + return instId >= 0 ? file.Instances[instId] : null; }); break; @@ -300,16 +311,16 @@ namespace Roblox.BinaryFormat.Chunks case PropertyType.NumberSequence: loadProperties(i => { - int keys = reader.ReadInt32(); - var keypoints = new NumberSequenceKeypoint[keys]; + int numKeys = reader.ReadInt32(); + var keypoints = new NumberSequenceKeypoint[numKeys]; - for (int key = 0; key < keys; key++) + for (int key = 0; key < numKeys; key++) { - float time = reader.ReadSingle(), - value = reader.ReadSingle(), - envelope = reader.ReadSingle(); + float Time = reader.ReadFloat(), + Value = reader.ReadFloat(), + Envelope = reader.ReadFloat(); - keypoints[key] = new NumberSequenceKeypoint(time, value, envelope); + keypoints[key] = new NumberSequenceKeypoint(Time, Value, Envelope); } return new NumberSequence(keypoints); @@ -319,18 +330,23 @@ namespace Roblox.BinaryFormat.Chunks case PropertyType.ColorSequence: loadProperties(i => { - int keys = reader.ReadInt32(); - var keypoints = new ColorSequenceKeypoint[keys]; + int numKeys = reader.ReadInt32(); + var keypoints = new ColorSequenceKeypoint[numKeys]; - for (int key = 0; key < keys; key++) + for (int key = 0; key < numKeys; key++) { - float time = reader.ReadSingle(), - R = reader.ReadSingle(), - G = reader.ReadSingle(), - B = reader.ReadSingle(), - envelope = reader.ReadSingle(); // unused, but still written + float Time = reader.ReadFloat(), + R = reader.ReadFloat(), + G = reader.ReadFloat(), + B = reader.ReadFloat(); - keypoints[key] = new ColorSequenceKeypoint(time, new Color3(R, G, B)); + Color3 Color = new Color3(R, G, B); + keypoints[key] = new ColorSequenceKeypoint(Time, Color); + + // ColorSequenceKeypoint has an unused `Envelope` float which has to be read. + // Roblox Studio writes it because it does an std::memcpy call to the C++ type. + // If we skip it, the stream will become misaligned. + reader.ReadBytes(4); } return new ColorSequence(keypoints); @@ -340,25 +356,21 @@ namespace Roblox.BinaryFormat.Chunks case PropertyType.NumberRange: loadProperties(i => { - float min = reader.ReadSingle(); - float max = reader.ReadSingle(); + float min = reader.ReadFloat(); + float max = reader.ReadFloat(); return new NumberRange(min, max); }); break; case PropertyType.Rect: - float[] Rect_X0 = readInstanceFloats(), - Rect_Y0 = readInstanceFloats(), - Rect_X1 = readInstanceFloats(), - Rect_Y1 = readInstanceFloats(); + float[] Rect_X0 = readFloats(), Rect_Y0 = readFloats(), + Rect_X1 = readFloats(), Rect_Y1 = readFloats(); loadProperties(i => { - float x0 = Rect_X0[i], - y0 = Rect_Y0[i], - x1 = Rect_X1[i], - y1 = Rect_Y1[i]; + float x0 = Rect_X0[i], y0 = Rect_Y0[i], + x1 = Rect_X1[i], y1 = Rect_Y1[i]; return new Rect(x0, y0, x1, y1); }); @@ -371,19 +383,19 @@ namespace Roblox.BinaryFormat.Chunks if (custom) { - float density = reader.ReadSingle(), - friction = reader.ReadSingle(), - elasticity = reader.ReadSingle(), - frictionWeight = reader.ReadSingle(), - elasticityWeight = reader.ReadSingle(); + float Density = reader.ReadFloat(), + Friction = reader.ReadFloat(), + Elasticity = reader.ReadFloat(), + FrictionWeight = reader.ReadFloat(), + ElasticityWeight = reader.ReadFloat(); return new PhysicalProperties ( - density, - friction, - elasticity, - frictionWeight, - elasticityWeight + Density, + Friction, + Elasticity, + FrictionWeight, + ElasticityWeight ); } @@ -407,7 +419,7 @@ namespace Roblox.BinaryFormat.Chunks break; case PropertyType.Int64: - long[] int64s = reader.ReadInterwovenValues(instCount, (buffer, start) => + long[] int64s = reader.ReadInterlaced(instCount, (buffer, start) => { long result = BitConverter.ToInt64(buffer, start); return (long)((ulong)result >> 1) ^ (-(result & 1)); @@ -415,8 +427,9 @@ namespace Roblox.BinaryFormat.Chunks loadProperties(i => int64s[i]); break; - } + + reader.Dispose(); } } } \ No newline at end of file diff --git a/BinaryFormat/Reader.cs b/BinaryFormat/Reader.cs deleted file mode 100644 index 9917fc6..0000000 --- a/BinaryFormat/Reader.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -using Roblox.DataTypes; - -namespace Roblox.BinaryFormat -{ - public class RobloxBinaryReader : BinaryReader - { - public RobloxBinaryReader(Stream stream) : base(stream) { } - - public T[] ReadInterwovenValues(int count, Func decode) where T : struct - { - int bufferSize = Marshal.SizeOf(); - - byte[] interwoven = ReadBytes(count * bufferSize); - T[] values = new T[count]; - - for (int i = 0; i < count; i++) - { - long unwind = 0; - - for (int weave = 0; weave < bufferSize; weave++) - { - long splice = interwoven[(weave * count) + i]; - int strand = (bufferSize - weave - 1) * 8; - unwind |= (splice << strand); - } - - byte[] buffer = BitConverter.GetBytes(unwind); - values[i] = decode(buffer, 0); - } - - return values; - } - - public int[] ReadInts(int count) - { - return ReadInterwovenValues(count, (buffer, start) => - { - int value = BitConverter.ToInt32(buffer, start); - return (value >> 1) ^ (-(value & 1)); - }); - } - - public float[] ReadFloats(int count) - { - return ReadInterwovenValues(count, (buffer, start) => - { - uint u = BitConverter.ToUInt32(buffer, start); - uint i = (u >> 1) | (u << 31); - - byte[] b = BitConverter.GetBytes(i); - return BitConverter.ToSingle(b, 0); - }); - } - - public int[] ReadInstanceIds(int count) - { - int[] values = ReadInts(count); - - for (int i = 1; i < count; ++i) - values[i] += values[i - 1]; - - return values; - } - - public override string ReadString() - { - int length = ReadInt32(); - byte[] buffer = ReadBytes(length); - return Encoding.UTF8.GetString(buffer); - } - } -} diff --git a/Core/Instance.cs b/Core/Instance.cs index bf33e3b..4bed088 100644 --- a/Core/Instance.cs +++ b/Core/Instance.cs @@ -1,39 +1,59 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; namespace Roblox { - public class RobloxInstance + /// + /// Describes an object in Roblox's Parent->Child hierarchy. + /// Instances can have sets of properties loaded from *.rbxl/*.rbxm files. + /// + public class Instance { - private List _children = new List(); - private RobloxInstance _parent; + public string ClassName = ""; + public List Properties = new List(); + + private List Children = new List(); + private Instance rawParent; - public string ClassName; - public List Properties = new List(); + public string Name => ReadProperty("Name", ClassName); + public override string ToString() => Name; - public bool IsAncestorOf(RobloxInstance other) + /// + /// Returns true if this Instance is an ancestor to the provided Instance. + /// + /// The instance whose descendance will be tested against this Instance. + public bool IsAncestorOf(Instance descendant) { - while (other != null) + while (descendant != null) { - if (other == this) + if (descendant == this) return true; - other = other.Parent; + descendant = descendant.Parent; } return false; } - public bool IsDescendantOf(RobloxInstance other) + /// + /// Returns true if this Instance is a descendant of the provided Instance. + /// + /// The instance whose ancestry will be tested against this Instance. + public bool IsDescendantOf(Instance ancestor) { - return other.IsAncestorOf(this); + return ancestor.IsAncestorOf(this); } - public RobloxInstance Parent + /// + /// The parent of this Instance, or null if the instance is the root of a tree. + /// Setting the value of this property will throw an exception if: + /// - The value is set to itself. + /// - The value is a descendant of the Instance. + /// + public Instance Parent { - get { return _parent; } + get { return rawParent; } set { if (IsAncestorOf(value)) @@ -42,50 +62,105 @@ namespace Roblox if (Parent == this) throw new Exception("Attempt to set parent to self"); - if (_parent != null) - _parent._children.Remove(this); + if (rawParent != null) + rawParent.Children.Remove(this); - value._children.Add(this); - _parent = value; + value.Children.Add(this); + rawParent = value; } } - public ReadOnlyCollection Children + public IEnumerable GetChildren() { - get { return _children.AsReadOnly(); } + var current = Children.ToArray(); + return current.AsEnumerable(); } + /// + /// Returns the first Instance whose Name is the provided string name. If the instance is not found, this returns null. + /// + /// The name of the instance to find. + /// The instance that was found with this name, or null. + public Instance FindFirstChild(string name) + { + Instance result = null; + + var query = Children.Where(child => child.Name == name); + if (query.Count() > 0) + result = query.First(); + + return result; + } + + /// + /// Looks for a property with the specified property name, and returns it as an object. + /// The resulting value may be null if the property is not serialized. + /// You can use the templated ReadProperty overload to fetch it as a specific type with a default value provided. + /// + /// The name of the property to be fetched from this Instance. + /// An object reference to the value of the specified property, if it exists. + /// public object ReadProperty(string propertyName) { - RobloxProperty property = Properties - .Where((prop) => prop.Name == propertyName) - .First(); + Property property = null; - return property.Value; + if (query.Count() > 0) + property = query.First(); + + return (property != null ? property.Value : null); } - public bool TryReadProperty(string propertyName, out T value) + /// + /// Looks for a property with the specified property name, and returns it as the specified type. + /// If it cannot be converted, the provided nullFallback value will be returned instead. + /// + /// The value type to convert to when finding the specified property name. + /// The name of the property to be fetched from this Instance. + /// A fallback value to be returned if casting to T fails, or the property is not found. + /// + public T ReadProperty(string propertyName, T nullFallback) { try { object result = ReadProperty(propertyName); - value = (T)result; + return (T)result; + } + catch (Exception e) + { + return nullFallback; + } + } + + /// + /// Looks for a property with the specified property name. If found, it will try to set the value of the referenced outValue to its value. + /// Returns true if the property was found and its value was casted to the referenced outValue. If it returns false, the outValue has not been set. + /// + /// The value type to convert to when finding the specified property name. + /// The name of the property to be fetched from this Instance. + /// The value to write to if the property can be casted to T correctly. + public bool TryReadProperty(string propertyName, ref T outValue) + { + try + { + object result = ReadProperty(propertyName); + outValue = (T)result; return true; } catch { - value = default(T); return false; } } - public override string ToString() + /// + /// Adds a property by reference to this Instance's property list. + /// This is used during the file loading procedure. + /// + /// A reference to the property that will be added. + public void AddProperty(ref Property prop) { - var name = ""; - TryReadProperty("Name", out name); - - return '[' + ClassName + ']' + name; + Properties.Add(prop); } } -} +} \ No newline at end of file diff --git a/Core/Property.cs b/Core/Property.cs index 477f9ea..1ae1abb 100644 --- a/Core/Property.cs +++ b/Core/Property.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Roblox { @@ -38,25 +34,63 @@ namespace Roblox Int64 } - public class RobloxProperty + public class Property { public string Name; public PropertyType Type; public object Value; + private byte[] RawBuffer = null; + public bool HasRawBuffer + { + get + { + if (RawBuffer == null && Value != null) + { + // Infer what the buffer should be if this is a primitive. + switch (Type) + { + case PropertyType.Int: + RawBuffer = BitConverter.GetBytes((int)Value); + break; + case PropertyType.Int64: + RawBuffer = BitConverter.GetBytes((long)Value); + break; + case PropertyType.Bool: + RawBuffer = BitConverter.GetBytes((bool)Value); + break; + case PropertyType.Float: + RawBuffer = BitConverter.GetBytes((float)Value); + break; + case PropertyType.Double: + RawBuffer = BitConverter.GetBytes((double)Value); + break; + } + } + + return (RawBuffer != null); + } + } + public override string ToString() { - Type PropertyType = typeof(PropertyType); + string typeName = Enum.GetName(typeof(PropertyType), Type); + string valueLabel = (Value != null ? Value.ToString() : "null"); - string typeName = Enum.GetName(PropertyType, Type); - string valueLabel; - - if (Value != null) - valueLabel = Value.ToString(); - else - valueLabel = "?"; + if (Type == PropertyType.String) + valueLabel = '"' + valueLabel + '"'; return string.Join(" ", typeName, Name, '=', valueLabel); } + + internal void SetRawBuffer(byte[] buffer) + { + RawBuffer = buffer; + } + + public byte[] GetRawBuffer() + { + return RawBuffer; + } } } diff --git a/Core/RobloxFile.cs b/Core/RobloxFile.cs index 60e0169..3134338 100644 --- a/Core/RobloxFile.cs +++ b/Core/RobloxFile.cs @@ -1,13 +1,83 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.IO; using System.Text; -using System.Threading.Tasks; + +using Roblox.BinaryFormat; +using Roblox.XmlFormat; namespace Roblox { - public class RobloxFile + /// + /// Interface which represents a RobloxFile implementation. + /// + public interface IRobloxFile { - public List Trunk { get; private set; } + IReadOnlyList Trunk { get; } + void Initialize(byte[] buffer); + } + + /// + /// Represents a loaded *.rbxl/*.rbxm Roblox file. + /// All of the surface-level Instances are stored in the RobloxFile's Trunk property. + /// + public class RobloxFile : IRobloxFile + { + public bool Initialized { get; private set; } + public IRobloxFile InnerFile { get; private set; } + + public IReadOnlyList Trunk => InnerFile.Trunk; + + public void Initialize(byte[] buffer) + { + if (!Initialized) + { + if (buffer.Length > 14) + { + string header = Encoding.UTF7.GetString(buffer, 0, 14); + IRobloxFile file = null; + + if (header == RobloxBinaryFile.MagicHeader) + file = new RobloxBinaryFile(); + else if (header.StartsWith(" ByPalette; private static Dictionary ByNumber; - private static Dictionary ByName; private static Random RNG = new Random(); @@ -41,20 +40,38 @@ namespace Roblox.DataTypes static BrickColor() { - ByName = BrickColors.ColorMap.ToDictionary(brickColor => brickColor.Name); + Dictionary bcSum = new Dictionary(); + + foreach (BrickColor color in BrickColors.ColorMap) + { + if (bcSum.ContainsKey(color.Name)) + { + bcSum[color.Name]++; + } + else + { + bcSum.Add(color.Name, 1); + } + } + ByNumber = BrickColors.ColorMap.ToDictionary(brickColor => brickColor.Number); ByPalette = BrickColors.PaletteMap.Select(number => ByNumber[number]).ToList(); } - public static BrickColor New(string name) + public static BrickColor FromName(string name) { - if (!ByName.ContainsKey(name)) - name = DefaultName; + BrickColor result = null; + var query = BrickColors.ColorMap.Where((bc) => bc.Name == name); - return ByName[name]; + if (query.Count() > 0) + result = query.First(); + else + result = FromName(DefaultName); + + return result; } - public static BrickColor New(int number) + public static BrickColor FromNumber(int number) { if (!ByNumber.ContainsKey(number)) number = DefaultNumber; @@ -62,14 +79,14 @@ namespace Roblox.DataTypes return ByNumber[number]; } - public static BrickColor New(Color3 color) + public static BrickColor FromColor3(Color3 color) { - return New(color.R, color.G, color.B); + return FromRGB(color.R, color.G, color.B); } - public static BrickColor New(float r = 0, float g = 0, float b = 0) + public static BrickColor FromRGB(float r = 0, float g = 0, float b = 0) { - BrickColor bestMatch = New(-1); + BrickColor bestMatch = FromNumber(-1); float closest = float.MaxValue; foreach (BrickColor brickColor in BrickColors.ColorMap) @@ -106,13 +123,13 @@ namespace Roblox.DataTypes return ByPalette[index]; } - public static BrickColor White() => New("White"); - public static BrickColor Gray() => New("Medium stone grey"); - public static BrickColor DarkGray() => New("Dark stone grey"); - public static BrickColor Black() => New("Black"); - public static BrickColor Red() => New("Bright red"); - public static BrickColor Yellow() => New("Bright yellow"); - public static BrickColor Green() => New("Dark green"); - public static BrickColor Blue() => New("Bright blue"); + public static BrickColor White() => FromName("White"); + public static BrickColor Gray() => FromName("Medium stone grey"); + public static BrickColor DarkGray() => FromName("Dark stone grey"); + public static BrickColor Black() => FromName("Black"); + public static BrickColor Red() => FromName("Bright red"); + public static BrickColor Yellow() => FromName("Bright yellow"); + public static BrickColor Green() => FromName("Dark green"); + public static BrickColor Blue() => FromName("Bright blue"); } } \ No newline at end of file diff --git a/DataTypes/ColorSequence.cs b/DataTypes/ColorSequence.cs index 543c76e..0271be4 100644 --- a/DataTypes/ColorSequence.cs +++ b/DataTypes/ColorSequence.cs @@ -24,21 +24,21 @@ namespace Roblox.DataTypes public ColorSequence(ColorSequenceKeypoint[] keypoints) { - int len = keypoints.Length; + int numKeys = keypoints.Length; - if (len < 2) + if (numKeys < 2) throw new Exception("ColorSequence: requires at least 2 keypoints"); - else if (len > 20) + else if (numKeys > 20) throw new Exception("ColorSequence: table is too long."); - for (int i = 1; i < len; i++) - if (keypoints[i-1].Time > keypoints[i].Time) + for (int key = 1; key < numKeys; key++) + if (keypoints[key - 1].Time > keypoints[key].Time) throw new Exception("ColorSequence: all keypoints must be ordered by time"); - if (keypoints[0].Time < 0) + if (Math.Abs(keypoints[0].Time) >= 10e-5f) throw new Exception("ColorSequence must start at time=0.0"); - if (keypoints[len-1].Time > 1) + if (Math.Abs(keypoints[numKeys - 1].Time - 1f) >= 10e-5f) throw new Exception("ColorSequence must end at time=1.0"); Keypoints = keypoints; diff --git a/DataTypes/NumberSequence.cs b/DataTypes/NumberSequence.cs index 442de8f..e8ccf51 100644 --- a/DataTypes/NumberSequence.cs +++ b/DataTypes/NumberSequence.cs @@ -24,21 +24,21 @@ namespace Roblox.DataTypes public NumberSequence(NumberSequenceKeypoint[] keypoints) { - int len = keypoints.Length; + int numKeys = keypoints.Length; - if (len < 2) + if (numKeys < 2) throw new Exception("NumberSequence: requires at least 2 keypoints"); - else if (len > 20) + else if (numKeys > 20) throw new Exception("NumberSequence: table is too long."); - for (int i = 1; i < len; i++) - if (keypoints[i - 1].Time > keypoints[i].Time) + for (int key = 1; key < numKeys; key++) + if (keypoints[key - 1].Time > keypoints[key].Time) throw new Exception("NumberSequence: all keypoints must be ordered by time"); - if (keypoints[0].Time < 0) + if (Math.Abs(keypoints[0].Time) >= 10e-5f) throw new Exception("NumberSequence must start at time=0.0"); - if (keypoints[len - 1].Time > 1) + if (Math.Abs(keypoints[numKeys - 1].Time - 1f) >= 10e-5f) throw new Exception("NumberSequence must end at time=1.0"); Keypoints = keypoints; diff --git a/DataTypes/PathWaypoint.cs b/DataTypes/PathWaypoint.cs deleted file mode 100644 index 934ebdf..0000000 --- a/DataTypes/PathWaypoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Roblox.Enums; - -namespace Roblox.DataTypes -{ - public struct PathWaypoint - { - public readonly Vector3 Position; - public readonly PathWaypointAction Action; - - public PathWaypoint(Vector3? position) - { - Position = position ?? Vector3.Zero; - Action = PathWaypointAction.Walk; - } - - public PathWaypoint(Vector3 position, PathWaypointAction action = PathWaypointAction.Walk) - { - Position = position; - Action = action; - } - - public override string ToString() - { - Type PathWaypointAction = typeof(PathWaypointAction); - return '{' + Position + "} " + Enum.GetName(PathWaypointAction, Action); - } - } -} diff --git a/DataTypes/PhysicalProperties.cs b/DataTypes/PhysicalProperties.cs index 294e1e0..d8d8aed 100644 --- a/DataTypes/PhysicalProperties.cs +++ b/DataTypes/PhysicalProperties.cs @@ -9,8 +9,8 @@ namespace Roblox.DataTypes public readonly float Friction; public readonly float Elasticity; - public float FrictionWeight; - public float ElasticityWeight; + public readonly float FrictionWeight; + public readonly float ElasticityWeight; public PhysicalProperties(Material material) { diff --git a/RobloxFileFormat.csproj b/RobloxFileFormat.csproj index 022f22a..d4540c4 100644 --- a/RobloxFileFormat.csproj +++ b/RobloxFileFormat.csproj @@ -33,8 +33,8 @@ - - packages\lz4net.1.0.15.93\lib\net4-client\LZ4.dll + + packages\lz4net.1.0.10.93\lib\net4-client\LZ4.dll True @@ -47,13 +47,13 @@ - + - + @@ -65,7 +65,6 @@ - @@ -80,14 +79,18 @@ + + - + + +