Fixed some bugs, generally refining stuff.

This commit is contained in:
CloneTrooper1019 2019-02-04 13:30:33 -06:00
parent ebd56d22a7
commit 2be61916de
23 changed files with 436 additions and 221 deletions

View File

@ -5,15 +5,20 @@ using LZ4;
namespace RobloxFiles.BinaryFormat namespace RobloxFiles.BinaryFormat
{ {
public class RobloxBinaryChunk /// <summary>
/// BinaryRobloxChunk represents a generic LZ4-compressed chunk
/// of data in Roblox's Binary File Format.
/// </summary>
public class BinaryRobloxChunk
{ {
public readonly string ChunkType; public readonly string ChunkType;
public readonly int CompressedSize; public readonly int CompressedSize;
public readonly byte[] CompressedData;
public readonly int Size; public readonly int Size;
public readonly byte[] Reserved; public readonly byte[] Reserved;
public readonly byte[] CompressedData;
public readonly byte[] Data; public readonly byte[] Data;
public bool HasCompressedData => (CompressedSize > 0); public bool HasCompressedData => (CompressedSize > 0);
@ -23,18 +28,18 @@ namespace RobloxFiles.BinaryFormat
return ChunkType + " Chunk [" + Size + " bytes]"; return ChunkType + " Chunk [" + Size + " bytes]";
} }
public RobloxBinaryReader GetReader(string chunkType) public BinaryRobloxReader GetReader(string chunkType)
{ {
if (ChunkType == chunkType) if (ChunkType == chunkType)
{ {
MemoryStream buffer = new MemoryStream(Data); MemoryStream buffer = new MemoryStream(Data);
return new RobloxBinaryReader(buffer); return new BinaryRobloxReader(buffer);
} }
throw new Exception("Expected " + chunkType + " ChunkType from the input RobloxBinaryChunk"); throw new Exception("Expected " + chunkType + " ChunkType from the input RobloxBinaryChunk");
} }
public RobloxBinaryChunk(RobloxBinaryReader reader) public BinaryRobloxChunk(BinaryRobloxReader reader)
{ {
byte[] bChunkType = reader.ReadBytes(4); byte[] bChunkType = reader.ReadBytes(4);
ChunkType = Encoding.ASCII.GetString(bChunkType); ChunkType = Encoding.ASCII.GetString(bChunkType);

View File

@ -5,9 +5,9 @@ using System.Text;
namespace RobloxFiles.BinaryFormat namespace RobloxFiles.BinaryFormat
{ {
public class RobloxBinaryReader : BinaryReader public class BinaryRobloxReader : BinaryReader
{ {
public RobloxBinaryReader(Stream stream) : base(stream) { } public BinaryRobloxReader(Stream stream) : base(stream) { }
private byte[] lastStringBuffer = new byte[0] { }; private byte[] lastStringBuffer = new byte[0] { };
public T[] ReadInterlaced<T>(int count, Func<byte[], int, T> decode) where T : struct public T[] ReadInterlaced<T>(int count, Func<byte[], int, T> decode) where T : struct

View File

@ -22,7 +22,7 @@ namespace RobloxFiles.BinaryFormat
public Instance Contents => BinContents; public Instance Contents => BinContents;
// Runtime Specific // Runtime Specific
public List<RobloxBinaryChunk> Chunks = new List<RobloxBinaryChunk>(); public List<BinaryRobloxChunk> Chunks = new List<BinaryRobloxChunk>();
public override string ToString() => GetType().Name; public override string ToString() => GetType().Name;
public Instance[] Instances; public Instance[] Instances;
@ -32,7 +32,7 @@ namespace RobloxFiles.BinaryFormat
public void ReadFile(byte[] contents) public void ReadFile(byte[] contents)
{ {
using (MemoryStream file = new MemoryStream(contents)) using (MemoryStream file = new MemoryStream(contents))
using (RobloxBinaryReader reader = new RobloxBinaryReader(file)) using (BinaryRobloxReader reader = new BinaryRobloxReader(file))
{ {
// Verify the signature of the file. // Verify the signature of the file.
byte[] binSignature = reader.ReadBytes(14); byte[] binSignature = reader.ReadBytes(14);
@ -57,7 +57,7 @@ namespace RobloxFiles.BinaryFormat
{ {
try try
{ {
RobloxBinaryChunk chunk = new RobloxBinaryChunk(reader); BinaryRobloxChunk chunk = new BinaryRobloxChunk(reader);
Chunks.Add(chunk); Chunks.Add(chunk);
switch (chunk.ChunkType) switch (chunk.ChunkType)
@ -67,7 +67,8 @@ namespace RobloxFiles.BinaryFormat
type.Allocate(this); type.Allocate(this);
break; break;
case "PROP": case "PROP":
PROP.ReadProperties(this, chunk); PROP prop = new PROP(chunk);
prop.ReadProperties(this);
break; break;
case "PRNT": case "PRNT":
PRNT hierarchy = new PRNT(chunk); PRNT hierarchy = new PRNT(chunk);

View File

@ -13,9 +13,9 @@
return TypeName; return TypeName;
} }
public INST(RobloxBinaryChunk chunk) public INST(BinaryRobloxChunk chunk)
{ {
using (RobloxBinaryReader reader = chunk.GetReader("INST")) using (BinaryRobloxReader reader = chunk.GetReader("INST"))
{ {
TypeIndex = reader.ReadInt32(); TypeIndex = reader.ReadInt32();
TypeName = reader.ReadString(); TypeName = reader.ReadString();

View File

@ -7,9 +7,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
public int NumEntries; public int NumEntries;
public Dictionary<string, string> Entries; public Dictionary<string, string> Entries;
public META(RobloxBinaryChunk chunk) public META(BinaryRobloxChunk chunk)
{ {
using (RobloxBinaryReader reader = chunk.GetReader("META")) using (BinaryRobloxReader reader = chunk.GetReader("META"))
{ {
NumEntries = reader.ReadInt32(); NumEntries = reader.ReadInt32();
Entries = new Dictionary<string, string>(NumEntries); Entries = new Dictionary<string, string>(NumEntries);

View File

@ -8,9 +8,9 @@
public readonly int[] ChildrenIds; public readonly int[] ChildrenIds;
public readonly int[] ParentIds; public readonly int[] ParentIds;
public PRNT(RobloxBinaryChunk chunk) public PRNT(BinaryRobloxChunk chunk)
{ {
using (RobloxBinaryReader reader = chunk.GetReader("PRNT")) using (BinaryRobloxReader reader = chunk.GetReader("PRNT"))
{ {
Format = reader.ReadByte(); Format = reader.ReadByte();
NumRelations = reader.ReadInt32(); NumRelations = reader.ReadInt32();

View File

@ -9,27 +9,34 @@ namespace RobloxFiles.BinaryFormat.Chunks
{ {
public class PROP public class PROP
{ {
public static void ReadProperties(BinaryRobloxFile file, RobloxBinaryChunk chunk) public readonly string Name;
{ public readonly int TypeIndex;
RobloxBinaryReader reader = chunk.GetReader("PROP"); public readonly PropertyType Type;
// Read the property's header info. private BinaryRobloxReader Reader;
int typeIndex = reader.ReadInt32();
string name = reader.ReadString(); public PROP(BinaryRobloxChunk chunk)
PropertyType propType; {
Reader = chunk.GetReader("PROP");
TypeIndex = Reader.ReadInt32();
Name = Reader.ReadString();
try try
{ {
byte typeId = reader.ReadByte(); byte propType = Reader.ReadByte();
propType = (PropertyType)typeId; Type = (PropertyType)propType;
} }
catch catch
{ {
propType = PropertyType.Unknown; Type = PropertyType.Unknown;
} }
// Create access arrays for the objects we will be adding properties to. }
INST type = file.Types[typeIndex];
public void ReadProperties(BinaryRobloxFile file)
{
INST type = file.Types[TypeIndex];
Property[] props = new Property[type.NumInstances]; Property[] props = new Property[type.NumInstances];
int[] ids = type.InstanceIds; int[] ids = type.InstanceIds;
@ -37,16 +44,16 @@ namespace RobloxFiles.BinaryFormat.Chunks
for (int i = 0; i < instCount; i++) for (int i = 0; i < instCount; i++)
{ {
int instId = ids[i]; int id = ids[i];
Instance inst = file.Instances[instId]; Instance instance = file.Instances[id];
Property prop = new Property(); Property prop = new Property();
prop.Name = name; prop.Name = Name;
prop.Type = propType; prop.Type = Type;
prop.Instance = inst; prop.Instance = instance;
props[i] = prop; props[i] = prop;
inst.AddProperty(ref prop); instance.AddProperty(ref prop);
} }
// Setup some short-hand functions for actions frequently used during the read procedure. // Setup some short-hand functions for actions frequently used during the read procedure.
@ -59,20 +66,20 @@ namespace RobloxFiles.BinaryFormat.Chunks
} }
}); });
var readInts = new Func<int[]>(() => reader.ReadInts(instCount)); var readInts = new Func<int[]>(() => Reader.ReadInts(instCount));
var readFloats = new Func<float[]>(() => reader.ReadFloats(instCount)); var readFloats = new Func<float[]>(() => Reader.ReadFloats(instCount));
// Read the property data based on the property type. // Read the property data based on the property type.
switch (propType) switch (Type)
{ {
case PropertyType.String: case PropertyType.String:
loadProperties(i => loadProperties(i =>
{ {
string result = reader.ReadString(); string result = Reader.ReadString();
// Leave an access point for the original byte sequence, in case this is a BinaryString. // 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. // This will allow the developer to read the sequence without any mangling from C# strings.
byte[] buffer = reader.GetLastStringBuffer(); byte[] buffer = Reader.GetLastStringBuffer();
props[i].SetRawBuffer(buffer); props[i].SetRawBuffer(buffer);
return result; return result;
@ -80,7 +87,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
break; break;
case PropertyType.Bool: case PropertyType.Bool:
loadProperties(i => reader.ReadBoolean()); loadProperties(i => Reader.ReadBoolean());
break; break;
case PropertyType.Int: case PropertyType.Int:
int[] ints = readInts(); int[] ints = readInts();
@ -91,7 +98,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
loadProperties(i => floats[i]); loadProperties(i => floats[i]);
break; break;
case PropertyType.Double: case PropertyType.Double:
loadProperties(i => reader.ReadDouble()); loadProperties(i => Reader.ReadDouble());
break; break;
case PropertyType.UDim: case PropertyType.UDim:
float[] UDim_Scales = readFloats(); float[] UDim_Scales = readFloats();
@ -127,10 +134,10 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.Ray: case PropertyType.Ray:
loadProperties(i => loadProperties(i =>
{ {
float[] rawOrigin = reader.ReadFloats(3); float[] rawOrigin = Reader.ReadFloats(3);
Vector3 origin = new Vector3(rawOrigin); Vector3 origin = new Vector3(rawOrigin);
float[] rawDirection = reader.ReadFloats(3); float[] rawDirection = Reader.ReadFloats(3);
Vector3 direction = new Vector3(rawDirection); Vector3 direction = new Vector3(rawDirection);
return new Ray(origin, direction); return new Ray(origin, direction);
@ -140,7 +147,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.Faces: case PropertyType.Faces:
loadProperties(i => loadProperties(i =>
{ {
byte faces = reader.ReadByte(); byte faces = Reader.ReadByte();
return (Faces)faces; return (Faces)faces;
}); });
@ -148,7 +155,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.Axes: case PropertyType.Axes:
loadProperties(i => loadProperties(i =>
{ {
byte axes = reader.ReadByte(); byte axes = Reader.ReadByte();
return (Axes)axes; return (Axes)axes;
}); });
@ -213,7 +220,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
loadProperties(i => loadProperties(i =>
{ {
int normXY = reader.ReadByte(); int normXY = Reader.ReadByte();
if (normXY > 0) if (normXY > 0)
{ {
@ -237,13 +244,13 @@ namespace RobloxFiles.BinaryFormat.Chunks
R2.X, R2.Y, R2.Z, R2.X, R2.Y, R2.Z,
}; };
} }
else if (propType == PropertyType.Quaternion) else if (Type == PropertyType.Quaternion)
{ {
float qx = reader.ReadFloat(), qy = reader.ReadFloat(), float qx = Reader.ReadFloat(), qy = Reader.ReadFloat(),
qz = reader.ReadFloat(), qw = reader.ReadFloat(); qz = Reader.ReadFloat(), qw = Reader.ReadFloat();
Quaternion quat = new Quaternion(qx, qy, qz, qw); Quaternion quaternion = new Quaternion(qx, qy, qz, qw);
var rotation = quat.ToCFrame(); var rotation = quaternion.ToCFrame();
return rotation.GetComponents(); return rotation.GetComponents();
} }
@ -253,7 +260,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
for (int m = 0; m < 9; m++) for (int m = 0; m < 9; m++)
{ {
float value = reader.ReadFloat(); float value = Reader.ReadFloat();
matrix[m] = value; matrix[m] = value;
} }
@ -284,12 +291,12 @@ namespace RobloxFiles.BinaryFormat.Chunks
// TODO: I want to map these values to actual Roblox enums, but I'll have to add an // 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. // interpreter for the JSON API Dump to do it properly.
uint[] enums = reader.ReadInterlaced(instCount, BitConverter.ToUInt32); uint[] enums = Reader.ReadInterlaced(instCount, BitConverter.ToUInt32);
loadProperties(i => enums[i]); loadProperties(i => enums[i]);
break; break;
case PropertyType.Ref: case PropertyType.Ref:
int[] instIds = reader.ReadInstanceIds(instCount); int[] instIds = Reader.ReadInstanceIds(instCount);
loadProperties(i => loadProperties(i =>
{ {
@ -301,9 +308,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.Vector3int16: case PropertyType.Vector3int16:
loadProperties(i => loadProperties(i =>
{ {
short x = reader.ReadInt16(), short x = Reader.ReadInt16(),
y = reader.ReadInt16(), y = Reader.ReadInt16(),
z = reader.ReadInt16(); z = Reader.ReadInt16();
return new Vector3int16(x, y, z); return new Vector3int16(x, y, z);
}); });
@ -312,14 +319,14 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.NumberSequence: case PropertyType.NumberSequence:
loadProperties(i => loadProperties(i =>
{ {
int numKeys = reader.ReadInt32(); int numKeys = Reader.ReadInt32();
var keypoints = new NumberSequenceKeypoint[numKeys]; var keypoints = new NumberSequenceKeypoint[numKeys];
for (int key = 0; key < numKeys; key++) for (int key = 0; key < numKeys; key++)
{ {
float Time = reader.ReadFloat(), float Time = Reader.ReadFloat(),
Value = reader.ReadFloat(), Value = Reader.ReadFloat(),
Envelope = reader.ReadFloat(); Envelope = Reader.ReadFloat();
keypoints[key] = new NumberSequenceKeypoint(Time, Value, Envelope); keypoints[key] = new NumberSequenceKeypoint(Time, Value, Envelope);
} }
@ -331,23 +338,20 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.ColorSequence: case PropertyType.ColorSequence:
loadProperties(i => loadProperties(i =>
{ {
int numKeys = reader.ReadInt32(); int numKeys = Reader.ReadInt32();
var keypoints = new ColorSequenceKeypoint[numKeys]; var keypoints = new ColorSequenceKeypoint[numKeys];
for (int key = 0; key < numKeys; key++) for (int key = 0; key < numKeys; key++)
{ {
float Time = reader.ReadFloat(), float Time = Reader.ReadFloat(),
R = reader.ReadFloat(), R = Reader.ReadFloat(),
G = reader.ReadFloat(), G = Reader.ReadFloat(),
B = reader.ReadFloat(); B = Reader.ReadFloat();
Color3 Color = new Color3(R, G, B); Color3 Value = new Color3(R, G, B);
keypoints[key] = new ColorSequenceKeypoint(Time, Color); byte[] Reserved = Reader.ReadBytes(4);
// ColorSequenceKeypoint has an unused `Envelope` float which has to be read. keypoints[key] = new ColorSequenceKeypoint(Time, Value, Reserved);
// 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); return new ColorSequence(keypoints);
@ -357,8 +361,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.NumberRange: case PropertyType.NumberRange:
loadProperties(i => loadProperties(i =>
{ {
float min = reader.ReadFloat(); float min = Reader.ReadFloat();
float max = reader.ReadFloat(); float max = Reader.ReadFloat();
return new NumberRange(min, max); return new NumberRange(min, max);
}); });
@ -380,15 +384,15 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.PhysicalProperties: case PropertyType.PhysicalProperties:
loadProperties(i => loadProperties(i =>
{ {
bool custom = reader.ReadBoolean(); bool custom = Reader.ReadBoolean();
if (custom) if (custom)
{ {
float Density = reader.ReadFloat(), float Density = Reader.ReadFloat(),
Friction = reader.ReadFloat(), Friction = Reader.ReadFloat(),
Elasticity = reader.ReadFloat(), Elasticity = Reader.ReadFloat(),
FrictionWeight = reader.ReadFloat(), FrictionWeight = Reader.ReadFloat(),
ElasticityWeight = reader.ReadFloat(); ElasticityWeight = Reader.ReadFloat();
return new PhysicalProperties return new PhysicalProperties
( (
@ -405,9 +409,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
break; break;
case PropertyType.Color3uint8: case PropertyType.Color3uint8:
byte[] color3uint8_R = reader.ReadBytes(instCount), byte[] color3uint8_R = Reader.ReadBytes(instCount),
color3uint8_G = reader.ReadBytes(instCount), color3uint8_G = Reader.ReadBytes(instCount),
color3uint8_B = reader.ReadBytes(instCount); color3uint8_B = Reader.ReadBytes(instCount);
loadProperties(i => loadProperties(i =>
{ {
@ -420,7 +424,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
break; break;
case PropertyType.Int64: case PropertyType.Int64:
long[] int64s = reader.ReadInterlaced(instCount, (buffer, start) => long[] int64s = Reader.ReadInterlaced(instCount, (buffer, start) =>
{ {
long result = BitConverter.ToInt64(buffer, start); long result = BitConverter.ToInt64(buffer, start);
return (long)((ulong)result >> 1) ^ (-(result & 1)); return (long)((ulong)result >> 1) ^ (-(result & 1));
@ -430,7 +434,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
break; break;
} }
reader.Dispose(); Reader.Dispose();
} }
} }
} }

View File

@ -1,91 +0,0 @@
using System;
using System.IO;
using System.Text;
using RobloxFiles.BinaryFormat;
using RobloxFiles.XmlFormat;
namespace RobloxFiles
{
/// <summary>
/// Interface which represents a RobloxFile implementation.
/// </summary>
public interface IRobloxFile
{
Instance Contents { get; }
void ReadFile(byte[] buffer);
}
/// <summary>
/// Represents a loaded *.rbxl/*.rbxm Roblox file.
/// All of the surface-level Instances are stored in the RobloxFile's Trunk property.
/// </summary>
public class RobloxFile : IRobloxFile
{
public bool Initialized { get; private set; }
public IRobloxFile InnerFile { get; private set; }
public Instance Contents => InnerFile.Contents;
public void ReadFile(byte[] buffer)
{
if (!Initialized)
{
if (buffer.Length > 14)
{
string header = Encoding.UTF7.GetString(buffer, 0, 14);
IRobloxFile file = null;
if (header == BinaryRobloxFile.MagicHeader)
file = new BinaryRobloxFile();
else if (header.StartsWith("<roblox"))
file = new XmlRobloxFile();
if (file != null)
{
file.ReadFile(buffer);
InnerFile = file;
Initialized = true;
return;
}
}
throw new Exception("Unrecognized header!");
}
}
public RobloxFile(byte[] buffer)
{
ReadFile(buffer);
}
public RobloxFile(Stream stream)
{
byte[] buffer;
using (MemoryStream memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
buffer = memoryStream.ToArray();
}
ReadFile(buffer);
}
public RobloxFile(string filePath)
{
byte[] buffer = File.ReadAllBytes(filePath);
ReadFile(buffer);
}
/// <summary>
/// Treats the provided string as if you were indexing a specific child or descendant of the `RobloxFile.Contents` folder.<para/>
/// The provided string can either be:<para/>
/// - The name of a child that is parented to RobloxFile.Contents ( Example: RobloxFile["Workspace"] )<para/>
/// - A period (.) separated path to a descendant of RobloxFile.Contents ( Example: RobloxFile["Workspace.Terrain"] )<para/>
/// This will throw an exception if any instance in the traversal is not found.
/// </summary>
public Instance this[string accessor] => Contents[accessor];
}
}

View File

@ -4,11 +4,13 @@
{ {
public readonly float Time; public readonly float Time;
public readonly Color3 Value; public readonly Color3 Value;
public readonly byte[] Reserved;
public ColorSequenceKeypoint(float time, Color3 value) public ColorSequenceKeypoint(float time, Color3 value, byte[] reserved = null)
{ {
Time = time; Time = time;
Value = value; Value = value;
Reserved = reserved;
} }
public override string ToString() public override string ToString()

View File

@ -34,10 +34,10 @@
public Vector3 ClosestPoint(Vector3 point) public Vector3 ClosestPoint(Vector3 point)
{ {
Vector3 result = Origin; Vector3 result = Origin;
float t = Direction.Dot(point - result); float dist = Direction.Dot(point - result);
if (t >= 0) if (dist >= 0)
result += (Direction * t); result += (Direction * dist);
return result; return result;
} }
@ -45,7 +45,7 @@
public float Distance(Vector3 point) public float Distance(Vector3 point)
{ {
Vector3 closestPoint = ClosestPoint(point); Vector3 closestPoint = ClosestPoint(point);
return (closestPoint - point).Magnitude; return (point - closestPoint).Magnitude;
} }
} }
} }

17
Interfaces/IRobloxFile.cs Normal file
View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RobloxFiles
{
/// <summary>
/// Interface which represents a RobloxFile implementation.
/// </summary>
public interface IRobloxFile
{
Instance Contents { get; }
void ReadFile(byte[] buffer);
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace RobloxFiles.XmlFormat
{
public interface IXmlPropertyToken
{
string Token { get; }
bool ReadToken(Property prop, XmlNode token);
}
}

149
RobloxFile.cs Normal file
View File

@ -0,0 +1,149 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using RobloxFiles.BinaryFormat;
using RobloxFiles.XmlFormat;
namespace RobloxFiles
{
/// <summary>
/// Represents a loaded *.rbxl/*.rbxm Roblox file.
/// All of the surface-level Instances are stored in the RobloxFile's 'Contents' property.
/// </summary>
public class RobloxFile : IRobloxFile
{
/// <summary>
/// Indicates if this RobloxFile has loaded data already.
/// </summary>
public bool Initialized { get; private set; }
/// <summary>
/// A reference to the inner IRobloxFile implementation that this RobloxFile opened with.<para/>
/// It can be a BinaryRobloxFile, or an XmlRobloxFile.
/// </summary>
public IRobloxFile InnerFile { get; private set; }
/// <summary>
/// A reference to a Folder Instance that stores all of the contents that were loaded.
/// </summary>
public Instance Contents => InnerFile.Contents;
/// <summary>
/// Initializes the RobloxFile from the provided buffer, if it hasn't been Initialized yet.
/// </summary>
/// <param name="buffer"></param>
public void ReadFile(byte[] buffer)
{
if (!Initialized)
{
if (buffer.Length > 14)
{
string header = Encoding.UTF7.GetString(buffer, 0, 14);
IRobloxFile file = null;
if (header == BinaryRobloxFile.MagicHeader)
file = new BinaryRobloxFile();
else if (header.StartsWith("<roblox"))
file = new XmlRobloxFile();
if (file != null)
{
file.ReadFile(buffer);
InnerFile = file;
Initialized = true;
return;
}
}
throw new Exception("Unrecognized header!");
}
}
/// <summary>
/// Creates a RobloxFile from a provided byte sequence that represents the file.
/// </summary>
/// <param name="buffer"></param>
private RobloxFile(byte[] buffer)
{
ReadFile(buffer);
}
/// <summary>
/// Opens a Roblox file from a byte sequence that represents the file.
/// </summary>
/// <param name="buffer">A byte sequence that represents the file.</param>
public static RobloxFile Open(byte[] buffer)
{
return new RobloxFile(buffer);
}
/// <summary>
/// Opens a Roblox file by reading from a provided Stream.
/// </summary>
/// <param name="stream">The stream to read the Roblox file from.</param>
public static RobloxFile Open(Stream stream)
{
byte[] buffer;
using (MemoryStream memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
buffer = memoryStream.ToArray();
}
return Open(buffer);
}
/// <summary>
/// Opens a Roblox file from a provided file path.
/// </summary>
/// <param name="filePath">A path to a Roblox file to be opened.</param>
public static RobloxFile Open(string filePath)
{
byte[] buffer = File.ReadAllBytes(filePath);
return Open(buffer);
}
/// <summary>
/// Creates and runs a Task to open a Roblox file from a byte sequence that represents the file.
/// </summary>
/// <param name="buffer">A byte sequence that represents the file.</param>
public static Task<RobloxFile> OpenAsync(byte[] buffer)
{
return Task.Run(() => Open(buffer));
}
/// <summary>
/// Creates and runs a Task to open a Roblox file using a provided Stream.
/// </summary>
/// <param name="stream">The stream to read the Roblox file from.</param>
public static Task<RobloxFile> OpenAsync(Stream stream)
{
return Task.Run(() => Open(stream));
}
/// <summary>
/// Opens a Roblox file from a provided file path.
/// </summary>
/// <param name="filePath">A path to a Roblox file to be opened.</param>
public static Task<RobloxFile> OpenAsync(string filePath)
{
return Task.Run(() => Open(filePath));
}
/// <summary>
/// Allows you to access a child/descendant of this file's contents, and/or one of its properties.<para/>
/// The provided string should be a period-separated (.) path to what you wish to access.<para/>
/// This will throw an exception if any part of the path cannot be found.<para/>
///
/// ~ Examples ~<para/>
/// var terrain = robloxFile["Workspace.Terrain"] as Instance;<para/>
/// var currentCamera = robloxFile["Workspace.CurrentCamera"] as Property;<para/>
///
/// </summary>
public object this[string accessor] => Contents[accessor];
}
}

View File

@ -69,10 +69,10 @@
<Compile Include="BinaryFormat\ChunkTypes\META.cs" /> <Compile Include="BinaryFormat\ChunkTypes\META.cs" />
<Compile Include="BinaryFormat\ChunkTypes\PRNT.cs" /> <Compile Include="BinaryFormat\ChunkTypes\PRNT.cs" />
<Compile Include="BinaryFormat\ChunkTypes\PROP.cs" /> <Compile Include="BinaryFormat\ChunkTypes\PROP.cs" />
<Compile Include="Core\Enums.cs" /> <Compile Include="Tree\Enums.cs" />
<Compile Include="Core\Property.cs" /> <Compile Include="Tree\Property.cs" />
<Compile Include="Core\Instance.cs" /> <Compile Include="Tree\Instance.cs" />
<Compile Include="Core\RobloxFile.cs" /> <Compile Include="RobloxFile.cs" />
<Compile Include="DataTypes\Axes.cs" /> <Compile Include="DataTypes\Axes.cs" />
<Compile Include="DataTypes\BrickColor.cs" /> <Compile Include="DataTypes\BrickColor.cs" />
<Compile Include="DataTypes\CFrame.cs" /> <Compile Include="DataTypes\CFrame.cs" />
@ -86,6 +86,8 @@
<Compile Include="DataTypes\PhysicalProperties.cs" /> <Compile Include="DataTypes\PhysicalProperties.cs" />
<Compile Include="DataTypes\Ray.cs" /> <Compile Include="DataTypes\Ray.cs" />
<Compile Include="DataTypes\Region3int16.cs" /> <Compile Include="DataTypes\Region3int16.cs" />
<Compile Include="Interfaces\IRobloxFile.cs" />
<Compile Include="Interfaces\IXmlPropertyToken.cs" />
<Compile Include="Utility\BrickColors.cs" /> <Compile Include="Utility\BrickColors.cs" />
<Compile Include="DataTypes\Vector3int16.cs" /> <Compile Include="DataTypes\Vector3int16.cs" />
<Compile Include="DataTypes\Rect.cs" /> <Compile Include="DataTypes\Rect.cs" />
@ -97,6 +99,7 @@
<Compile Include="Utility\MaterialInfo.cs" /> <Compile Include="Utility\MaterialInfo.cs" />
<Compile Include="Utility\Quaternion.cs" /> <Compile Include="Utility\Quaternion.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="XmlFormat\PropertyTokens\Vector3int16.cs" />
<Compile Include="XmlFormat\XmlPropertyTokens.cs" /> <Compile Include="XmlFormat\XmlPropertyTokens.cs" />
<Compile Include="XmlFormat\XmlDataReader.cs" /> <Compile Include="XmlFormat\XmlDataReader.cs" />
<Compile Include="XmlFormat\XmlRobloxFile.cs" /> <Compile Include="XmlFormat\XmlRobloxFile.cs" />

View File

@ -376,9 +376,10 @@ namespace RobloxFiles.Enums
public enum ContextActionPriority public enum ContextActionPriority
{ {
Low = 1000, Low = 1000,
Default = 2000, Medium = 2000,
Medium, High = 3000,
High = 3000
Default = Medium
} }
public enum ContextActionResult public enum ContextActionResult

View File

@ -20,6 +20,7 @@ namespace RobloxFiles
private List<Instance> Children = new List<Instance>(); private List<Instance> Children = new List<Instance>();
private Instance rawParent; private Instance rawParent;
/// <summary>The name of this Instance, if a Name property is defined.</summary>
public string Name => ReadProperty("Name", ClassName); public string Name => ReadProperty("Name", ClassName);
public override string ToString() => Name; public override string ToString() => Name;
@ -108,15 +109,19 @@ namespace RobloxFiles
/// </summary> /// </summary>
public Instance[] GetDescendants() public Instance[] GetDescendants()
{ {
Instance[] results = GetChildren(); List<Instance> results = new List<Instance>();
foreach (Instance child in results) foreach (Instance child in Children)
{ {
Instance[] childResults = child.GetDescendants(); // Add this child to the results.
results = results.Concat(childResults).ToArray(); results.Add(child);
// Add its descendants to the results.
Instance[] descendants = child.GetDescendants();
results.AddRange(descendants);
} }
return results; return results.ToArray();
} }
/// <summary> /// <summary>
@ -128,23 +133,73 @@ namespace RobloxFiles
public Instance FindFirstChild(string name, bool recursive = false) public Instance FindFirstChild(string name, bool recursive = false)
{ {
Instance result = null; Instance result = null;
var query = Children.Where((child) => name == child.Name);
var query = Children.Where(child => child.Name == name);
if (query.Count() > 0) if (query.Count() > 0)
{
result = query.First(); result = query.First();
}
else if (recursive)
{
foreach (Instance child in Children)
{
Instance found = child.FindFirstChild(name, true);
if (found != null)
{
result = found;
break;
}
}
}
return result; return result;
} }
/// <summary>
/// Returns the first ancestor of this Instance whose Name is the provided string name.
/// If the instance is not found, this returns null.
/// </summary>
/// <param name="name">The Name of the Instance to find.</param>
public Instance FindFirstAncestor(string name)
{
Instance ancestor = Parent;
while (ancestor != null)
{
if (ancestor.Name == name)
break;
ancestor = ancestor.Parent;
}
return ancestor;
}
public Instance FindFirstAncestorOfClass(string className)
{
Instance ancestor = Parent;
while (ancestor != null)
{
if (ancestor.ClassName == className)
break;
ancestor = ancestor.Parent;
}
return ancestor;
}
/// <summary> /// <summary>
/// Returns the first Instance whose ClassName is the provided string className. If the instance is not found, this returns null. /// Returns the first Instance whose ClassName is the provided string className. If the instance is not found, this returns null.
/// </summary> /// </summary>
/// <param name="className">The ClassName of the Instance to find.</param> /// <param name="className">The ClassName of the Instance to find.</param>
public Instance FindFirstChildOfClass(string className) public Instance FindFirstChildOfClass(string className, bool recursive = false)
{ {
Instance result = null; Instance result = null;
var query = Children.Where((child) => className == child.ClassName);
var query = Children.Where(child => child.ClassName == className);
if (query.Count() > 0) if (query.Count() > 0)
result = query.First(); result = query.First();
@ -242,13 +297,16 @@ namespace RobloxFiles
} }
/// <summary> /// <summary>
/// Treats the provided string as if you were indexing a specific child or descendant of this Instance.<para/> /// Allows you to access a child/descendant of this Instance, and/or one of its properties.<para/>
/// The provided string can either be:<para/> /// The provided string should be a period-separated (.) path to what you wish to access.<para/>
/// - The name of a child that is parented to this Instance. ( Example: game["Workspace"] )<para/> /// This will throw an exception if any part of the path cannot be found.<para/>
/// - A period-separated path to a descendant of this Instance. ( Example: game["Workspace.Terrain"] )<para/> ///
/// This will throw an exception if any instance in the traversal is not found. /// ~ Examples ~<para/>
/// var terrain = robloxFile["Workspace.Terrain"] as Instance;<para/>
/// var currentCamera = robloxFile["Workspace.CurrentCamera"] as Property;<para/>
///
/// </summary> /// </summary>
public Instance this[string accessor] public object this[string accessor]
{ {
get get
{ {
@ -259,7 +317,21 @@ namespace RobloxFiles
Instance next = result.FindFirstChild(name); Instance next = result.FindFirstChild(name);
if (next == null) if (next == null)
{
// Check if there is any property with this name.
var propQuery = result.Properties
.Where((prop) => name == prop.Name);
if (propQuery.Count() > 0)
{
var prop = propQuery.First();
return prop;
}
else
{
throw new Exception(name + " is not a valid member of " + result.Name); throw new Exception(name + " is not a valid member of " + result.Name);
}
}
result = next; result = next;
} }

View File

@ -36,8 +36,9 @@ namespace RobloxFiles
public class Property public class Property
{ {
public Instance Instance;
public string Name; public string Name;
public Instance Instance;
public PropertyType Type; public PropertyType Type;
public object Value; public object Value;

View File

@ -39,6 +39,7 @@ namespace RobloxFiles.Utility
/// <summary> /// <summary>
/// This contains a list of all defined BrickColors on Roblox. /// This contains a list of all defined BrickColors on Roblox.
/// There are some name duplicates, but that's an issue on Roblox's end.
/// </summary> /// </summary>
public static IReadOnlyList<BrickColor> ColorMap = new List<BrickColor>() public static IReadOnlyList<BrickColor> ColorMap = new List<BrickColor>()

View File

@ -140,7 +140,8 @@ namespace RobloxFiles.Utility
/// <summary> /// <summary>
/// A dictionary mapping materials to their default Friction.<para/> /// A dictionary mapping materials to their default Friction.<para/>
/// NOTE: This only maps materials that have different FrictionWeights. If it isn't in here, assume their FrictionWeight is 1. /// NOTE: This only maps materials that have different FrictionWeights.<para/>
/// If it isn't in here, assume their FrictionWeight is 1.
/// </summary> /// </summary>
public static IReadOnlyDictionary<Material, float> FrictionWeightMap = new Dictionary<Material, float>() public static IReadOnlyDictionary<Material, float> FrictionWeightMap = new Dictionary<Material, float>()
{ {

View File

@ -0,0 +1,40 @@
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
public class Vector3int16Token : IXmlPropertyToken
{
public string Token => "Vector3int16";
private static string[] Coords = new string[3] { "X", "Y", "Z" };
public bool ReadToken(Property property, XmlNode token)
{
short[] xyz = new short[3];
for (int i = 0; i < 3; i++)
{
string key = Coords[i];
try
{
var coord = token[key];
xyz[i] = short.Parse(coord.InnerText);
}
catch
{
return false;
}
}
short x = xyz[0],
y = xyz[1],
z = xyz[2];
property.Type = PropertyType.Vector3int16;
property.Value = new Vector3int16(x, y, z);
return true;
}
}
}

View File

@ -4,7 +4,7 @@ using System.Xml;
namespace RobloxFiles.XmlFormat namespace RobloxFiles.XmlFormat
{ {
static class XmlDataReader public static class XmlDataReader
{ {
public static void ReadProperties(Instance instance, XmlNode propsNode) public static void ReadProperties(Instance instance, XmlNode propsNode)
{ {
@ -39,7 +39,7 @@ namespace RobloxFiles.XmlFormat
} }
} }
public static Instance ReadInstance(XmlNode instNode, ref Dictionary<string, Instance> instances) public static Instance ReadInstance(XmlNode instNode, XmlRobloxFile file = null)
{ {
// Process the instance itself // Process the instance itself
if (instNode.Name != "Item") if (instNode.Name != "Item")
@ -54,14 +54,14 @@ namespace RobloxFiles.XmlFormat
// The 'referent' attribute is optional, but should be defined if a Ref property needs to link to this Instance. // The 'referent' attribute is optional, but should be defined if a Ref property needs to link to this Instance.
XmlNode refToken = instNode.Attributes.GetNamedItem("referent"); XmlNode refToken = instNode.Attributes.GetNamedItem("referent");
if (refToken != null && instances != null) if (refToken != null && file != null)
{ {
string refId = refToken.InnerText; string refId = refToken.InnerText;
if (instances.ContainsKey(refId)) if (file.Instances.ContainsKey(refId))
throw new Exception("XmlDataReader.ReadItem: Got an Item with a duplicate 'referent' attribute!"); throw new Exception("XmlDataReader.ReadItem: Got an Item with a duplicate 'referent' attribute!");
instances.Add(refId, inst); file.Instances.Add(refId, inst);
} }
// Process the child nodes of this instance. // Process the child nodes of this instance.
@ -73,7 +73,7 @@ namespace RobloxFiles.XmlFormat
} }
else if (childNode.Name == "Item") else if (childNode.Name == "Item")
{ {
Instance child = ReadInstance(childNode, ref instances); Instance child = ReadInstance(childNode, file);
child.Parent = inst; child.Parent = inst;
} }
} }

View File

@ -6,12 +6,6 @@ using System.Xml;
namespace RobloxFiles.XmlFormat namespace RobloxFiles.XmlFormat
{ {
public interface IXmlPropertyToken
{
string Token { get; }
bool ReadToken(Property prop, XmlNode token);
}
public static class XmlPropertyTokens public static class XmlPropertyTokens
{ {
public static IReadOnlyDictionary<string, IXmlPropertyToken> Handlers; public static IReadOnlyDictionary<string, IXmlPropertyToken> Handlers;

View File

@ -47,12 +47,12 @@ namespace RobloxFiles.XmlFormat
{ {
if (child.Name == "Item") if (child.Name == "Item")
{ {
Instance item = XmlDataReader.ReadInstance(child, ref Instances); Instance item = XmlDataReader.ReadInstance(child, this);
item.Parent = XmlContents; item.Parent = XmlContents;
} }
} }
// Resolve references for Ref properties. // Resolve referent properties.
var refProps = Instances.Values var refProps = Instances.Values
.SelectMany(inst => inst.Properties) .SelectMany(inst => inst.Properties)
.Where(prop => prop.Type == PropertyType.Ref); .Where(prop => prop.Type == PropertyType.Ref);