Fixed some bugs, generally refining stuff.
This commit is contained in:
parent
ebd56d22a7
commit
2be61916de
@ -5,15 +5,20 @@ using LZ4;
|
||||
|
||||
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 int CompressedSize;
|
||||
public readonly byte[] CompressedData;
|
||||
|
||||
public readonly int Size;
|
||||
|
||||
public readonly byte[] Reserved;
|
||||
|
||||
public readonly byte[] CompressedData;
|
||||
public readonly byte[] Data;
|
||||
|
||||
public bool HasCompressedData => (CompressedSize > 0);
|
||||
@ -23,18 +28,18 @@ namespace RobloxFiles.BinaryFormat
|
||||
return ChunkType + " Chunk [" + Size + " bytes]";
|
||||
}
|
||||
|
||||
public RobloxBinaryReader GetReader(string chunkType)
|
||||
public BinaryRobloxReader GetReader(string chunkType)
|
||||
{
|
||||
if (ChunkType == chunkType)
|
||||
{
|
||||
MemoryStream buffer = new MemoryStream(Data);
|
||||
return new RobloxBinaryReader(buffer);
|
||||
return new BinaryRobloxReader(buffer);
|
||||
}
|
||||
|
||||
throw new Exception("Expected " + chunkType + " ChunkType from the input RobloxBinaryChunk");
|
||||
}
|
||||
|
||||
public RobloxBinaryChunk(RobloxBinaryReader reader)
|
||||
public BinaryRobloxChunk(BinaryRobloxReader reader)
|
||||
{
|
||||
byte[] bChunkType = reader.ReadBytes(4);
|
||||
ChunkType = Encoding.ASCII.GetString(bChunkType);
|
||||
|
@ -5,9 +5,9 @@ using System.Text;
|
||||
|
||||
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] { };
|
||||
|
||||
public T[] ReadInterlaced<T>(int count, Func<byte[], int, T> decode) where T : struct
|
||||
|
@ -22,7 +22,7 @@ namespace RobloxFiles.BinaryFormat
|
||||
public Instance Contents => BinContents;
|
||||
|
||||
// Runtime Specific
|
||||
public List<RobloxBinaryChunk> Chunks = new List<RobloxBinaryChunk>();
|
||||
public List<BinaryRobloxChunk> Chunks = new List<BinaryRobloxChunk>();
|
||||
public override string ToString() => GetType().Name;
|
||||
|
||||
public Instance[] Instances;
|
||||
@ -32,7 +32,7 @@ namespace RobloxFiles.BinaryFormat
|
||||
public void ReadFile(byte[] 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.
|
||||
byte[] binSignature = reader.ReadBytes(14);
|
||||
@ -57,7 +57,7 @@ namespace RobloxFiles.BinaryFormat
|
||||
{
|
||||
try
|
||||
{
|
||||
RobloxBinaryChunk chunk = new RobloxBinaryChunk(reader);
|
||||
BinaryRobloxChunk chunk = new BinaryRobloxChunk(reader);
|
||||
Chunks.Add(chunk);
|
||||
|
||||
switch (chunk.ChunkType)
|
||||
@ -67,7 +67,8 @@ namespace RobloxFiles.BinaryFormat
|
||||
type.Allocate(this);
|
||||
break;
|
||||
case "PROP":
|
||||
PROP.ReadProperties(this, chunk);
|
||||
PROP prop = new PROP(chunk);
|
||||
prop.ReadProperties(this);
|
||||
break;
|
||||
case "PRNT":
|
||||
PRNT hierarchy = new PRNT(chunk);
|
||||
|
@ -13,9 +13,9 @@
|
||||
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();
|
||||
TypeName = reader.ReadString();
|
||||
|
@ -7,9 +7,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
public int NumEntries;
|
||||
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();
|
||||
Entries = new Dictionary<string, string>(NumEntries);
|
||||
|
@ -8,9 +8,9 @@
|
||||
public readonly int[] ChildrenIds;
|
||||
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();
|
||||
NumRelations = reader.ReadInt32();
|
||||
|
@ -9,27 +9,34 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
{
|
||||
public class PROP
|
||||
{
|
||||
public static void ReadProperties(BinaryRobloxFile file, RobloxBinaryChunk chunk)
|
||||
{
|
||||
RobloxBinaryReader reader = chunk.GetReader("PROP");
|
||||
public readonly string Name;
|
||||
public readonly int TypeIndex;
|
||||
public readonly PropertyType Type;
|
||||
|
||||
// Read the property's header info.
|
||||
int typeIndex = reader.ReadInt32();
|
||||
string name = reader.ReadString();
|
||||
PropertyType propType;
|
||||
private BinaryRobloxReader Reader;
|
||||
|
||||
public PROP(BinaryRobloxChunk chunk)
|
||||
{
|
||||
Reader = chunk.GetReader("PROP");
|
||||
|
||||
TypeIndex = Reader.ReadInt32();
|
||||
Name = Reader.ReadString();
|
||||
|
||||
try
|
||||
{
|
||||
byte typeId = reader.ReadByte();
|
||||
propType = (PropertyType)typeId;
|
||||
byte propType = Reader.ReadByte();
|
||||
Type = (PropertyType)propType;
|
||||
}
|
||||
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];
|
||||
|
||||
int[] ids = type.InstanceIds;
|
||||
@ -37,16 +44,16 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
|
||||
for (int i = 0; i < instCount; i++)
|
||||
{
|
||||
int instId = ids[i];
|
||||
Instance inst = file.Instances[instId];
|
||||
int id = ids[i];
|
||||
Instance instance = file.Instances[id];
|
||||
|
||||
Property prop = new Property();
|
||||
prop.Name = name;
|
||||
prop.Type = propType;
|
||||
prop.Instance = inst;
|
||||
prop.Name = Name;
|
||||
prop.Type = Type;
|
||||
prop.Instance = instance;
|
||||
|
||||
props[i] = prop;
|
||||
inst.AddProperty(ref prop);
|
||||
instance.AddProperty(ref prop);
|
||||
}
|
||||
|
||||
// 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 readFloats = new Func<float[]>(() => reader.ReadFloats(instCount));
|
||||
var readInts = new Func<int[]>(() => Reader.ReadInts(instCount));
|
||||
var readFloats = new Func<float[]>(() => Reader.ReadFloats(instCount));
|
||||
|
||||
// Read the property data based on the property type.
|
||||
switch (propType)
|
||||
switch (Type)
|
||||
{
|
||||
case PropertyType.String:
|
||||
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.
|
||||
// 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);
|
||||
|
||||
return result;
|
||||
@ -80,7 +87,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
|
||||
break;
|
||||
case PropertyType.Bool:
|
||||
loadProperties(i => reader.ReadBoolean());
|
||||
loadProperties(i => Reader.ReadBoolean());
|
||||
break;
|
||||
case PropertyType.Int:
|
||||
int[] ints = readInts();
|
||||
@ -91,7 +98,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
loadProperties(i => floats[i]);
|
||||
break;
|
||||
case PropertyType.Double:
|
||||
loadProperties(i => reader.ReadDouble());
|
||||
loadProperties(i => Reader.ReadDouble());
|
||||
break;
|
||||
case PropertyType.UDim:
|
||||
float[] UDim_Scales = readFloats();
|
||||
@ -127,10 +134,10 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.Ray:
|
||||
loadProperties(i =>
|
||||
{
|
||||
float[] rawOrigin = reader.ReadFloats(3);
|
||||
float[] rawOrigin = Reader.ReadFloats(3);
|
||||
Vector3 origin = new Vector3(rawOrigin);
|
||||
|
||||
float[] rawDirection = reader.ReadFloats(3);
|
||||
float[] rawDirection = Reader.ReadFloats(3);
|
||||
Vector3 direction = new Vector3(rawDirection);
|
||||
|
||||
return new Ray(origin, direction);
|
||||
@ -140,7 +147,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.Faces:
|
||||
loadProperties(i =>
|
||||
{
|
||||
byte faces = reader.ReadByte();
|
||||
byte faces = Reader.ReadByte();
|
||||
return (Faces)faces;
|
||||
});
|
||||
|
||||
@ -148,7 +155,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.Axes:
|
||||
loadProperties(i =>
|
||||
{
|
||||
byte axes = reader.ReadByte();
|
||||
byte axes = Reader.ReadByte();
|
||||
return (Axes)axes;
|
||||
});
|
||||
|
||||
@ -213,7 +220,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
|
||||
loadProperties(i =>
|
||||
{
|
||||
int normXY = reader.ReadByte();
|
||||
int normXY = Reader.ReadByte();
|
||||
|
||||
if (normXY > 0)
|
||||
{
|
||||
@ -237,13 +244,13 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
R2.X, R2.Y, R2.Z,
|
||||
};
|
||||
}
|
||||
else if (propType == PropertyType.Quaternion)
|
||||
else if (Type == PropertyType.Quaternion)
|
||||
{
|
||||
float qx = reader.ReadFloat(), qy = reader.ReadFloat(),
|
||||
qz = reader.ReadFloat(), qw = reader.ReadFloat();
|
||||
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();
|
||||
Quaternion quaternion = new Quaternion(qx, qy, qz, qw);
|
||||
var rotation = quaternion.ToCFrame();
|
||||
|
||||
return rotation.GetComponents();
|
||||
}
|
||||
@ -253,7 +260,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
|
||||
for (int m = 0; m < 9; m++)
|
||||
{
|
||||
float value = reader.ReadFloat();
|
||||
float value = Reader.ReadFloat();
|
||||
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
|
||||
// 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]);
|
||||
|
||||
break;
|
||||
case PropertyType.Ref:
|
||||
int[] instIds = reader.ReadInstanceIds(instCount);
|
||||
int[] instIds = Reader.ReadInstanceIds(instCount);
|
||||
|
||||
loadProperties(i =>
|
||||
{
|
||||
@ -301,9 +308,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.Vector3int16:
|
||||
loadProperties(i =>
|
||||
{
|
||||
short x = reader.ReadInt16(),
|
||||
y = reader.ReadInt16(),
|
||||
z = reader.ReadInt16();
|
||||
short x = Reader.ReadInt16(),
|
||||
y = Reader.ReadInt16(),
|
||||
z = Reader.ReadInt16();
|
||||
|
||||
return new Vector3int16(x, y, z);
|
||||
});
|
||||
@ -312,14 +319,14 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.NumberSequence:
|
||||
loadProperties(i =>
|
||||
{
|
||||
int numKeys = reader.ReadInt32();
|
||||
int numKeys = Reader.ReadInt32();
|
||||
var keypoints = new NumberSequenceKeypoint[numKeys];
|
||||
|
||||
for (int key = 0; key < numKeys; key++)
|
||||
{
|
||||
float Time = reader.ReadFloat(),
|
||||
Value = reader.ReadFloat(),
|
||||
Envelope = reader.ReadFloat();
|
||||
float Time = Reader.ReadFloat(),
|
||||
Value = Reader.ReadFloat(),
|
||||
Envelope = Reader.ReadFloat();
|
||||
|
||||
keypoints[key] = new NumberSequenceKeypoint(Time, Value, Envelope);
|
||||
}
|
||||
@ -331,23 +338,20 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.ColorSequence:
|
||||
loadProperties(i =>
|
||||
{
|
||||
int numKeys = reader.ReadInt32();
|
||||
int numKeys = Reader.ReadInt32();
|
||||
var keypoints = new ColorSequenceKeypoint[numKeys];
|
||||
|
||||
for (int key = 0; key < numKeys; key++)
|
||||
{
|
||||
float Time = reader.ReadFloat(),
|
||||
R = reader.ReadFloat(),
|
||||
G = reader.ReadFloat(),
|
||||
B = reader.ReadFloat();
|
||||
float Time = Reader.ReadFloat(),
|
||||
R = Reader.ReadFloat(),
|
||||
G = Reader.ReadFloat(),
|
||||
B = Reader.ReadFloat();
|
||||
|
||||
Color3 Color = new Color3(R, G, B);
|
||||
keypoints[key] = new ColorSequenceKeypoint(Time, Color);
|
||||
Color3 Value = new Color3(R, G, B);
|
||||
byte[] Reserved = Reader.ReadBytes(4);
|
||||
|
||||
// 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);
|
||||
keypoints[key] = new ColorSequenceKeypoint(Time, Value, Reserved);
|
||||
}
|
||||
|
||||
return new ColorSequence(keypoints);
|
||||
@ -357,8 +361,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.NumberRange:
|
||||
loadProperties(i =>
|
||||
{
|
||||
float min = reader.ReadFloat();
|
||||
float max = reader.ReadFloat();
|
||||
float min = Reader.ReadFloat();
|
||||
float max = Reader.ReadFloat();
|
||||
|
||||
return new NumberRange(min, max);
|
||||
});
|
||||
@ -380,15 +384,15 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
case PropertyType.PhysicalProperties:
|
||||
loadProperties(i =>
|
||||
{
|
||||
bool custom = reader.ReadBoolean();
|
||||
bool custom = Reader.ReadBoolean();
|
||||
|
||||
if (custom)
|
||||
{
|
||||
float Density = reader.ReadFloat(),
|
||||
Friction = reader.ReadFloat(),
|
||||
Elasticity = reader.ReadFloat(),
|
||||
FrictionWeight = reader.ReadFloat(),
|
||||
ElasticityWeight = reader.ReadFloat();
|
||||
float Density = Reader.ReadFloat(),
|
||||
Friction = Reader.ReadFloat(),
|
||||
Elasticity = Reader.ReadFloat(),
|
||||
FrictionWeight = Reader.ReadFloat(),
|
||||
ElasticityWeight = Reader.ReadFloat();
|
||||
|
||||
return new PhysicalProperties
|
||||
(
|
||||
@ -405,9 +409,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
|
||||
break;
|
||||
case PropertyType.Color3uint8:
|
||||
byte[] color3uint8_R = reader.ReadBytes(instCount),
|
||||
color3uint8_G = reader.ReadBytes(instCount),
|
||||
color3uint8_B = reader.ReadBytes(instCount);
|
||||
byte[] color3uint8_R = Reader.ReadBytes(instCount),
|
||||
color3uint8_G = Reader.ReadBytes(instCount),
|
||||
color3uint8_B = Reader.ReadBytes(instCount);
|
||||
|
||||
loadProperties(i =>
|
||||
{
|
||||
@ -420,7 +424,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
|
||||
break;
|
||||
case PropertyType.Int64:
|
||||
long[] int64s = reader.ReadInterlaced(instCount, (buffer, start) =>
|
||||
long[] int64s = Reader.ReadInterlaced(instCount, (buffer, start) =>
|
||||
{
|
||||
long result = BitConverter.ToInt64(buffer, start);
|
||||
return (long)((ulong)result >> 1) ^ (-(result & 1));
|
||||
@ -430,7 +434,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
|
||||
break;
|
||||
}
|
||||
|
||||
reader.Dispose();
|
||||
Reader.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
@ -4,11 +4,13 @@
|
||||
{
|
||||
public readonly float Time;
|
||||
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;
|
||||
Value = value;
|
||||
Reserved = reserved;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -34,10 +34,10 @@
|
||||
public Vector3 ClosestPoint(Vector3 point)
|
||||
{
|
||||
Vector3 result = Origin;
|
||||
float t = Direction.Dot(point - result);
|
||||
float dist = Direction.Dot(point - result);
|
||||
|
||||
if (t >= 0)
|
||||
result += (Direction * t);
|
||||
if (dist >= 0)
|
||||
result += (Direction * dist);
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -45,7 +45,7 @@
|
||||
public float Distance(Vector3 point)
|
||||
{
|
||||
Vector3 closestPoint = ClosestPoint(point);
|
||||
return (closestPoint - point).Magnitude;
|
||||
return (point - closestPoint).Magnitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
Interfaces/IRobloxFile.cs
Normal file
17
Interfaces/IRobloxFile.cs
Normal 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);
|
||||
}
|
||||
}
|
15
Interfaces/IXmlPropertyToken.cs
Normal file
15
Interfaces/IXmlPropertyToken.cs
Normal 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
149
RobloxFile.cs
Normal 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];
|
||||
}
|
||||
}
|
@ -69,10 +69,10 @@
|
||||
<Compile Include="BinaryFormat\ChunkTypes\META.cs" />
|
||||
<Compile Include="BinaryFormat\ChunkTypes\PRNT.cs" />
|
||||
<Compile Include="BinaryFormat\ChunkTypes\PROP.cs" />
|
||||
<Compile Include="Core\Enums.cs" />
|
||||
<Compile Include="Core\Property.cs" />
|
||||
<Compile Include="Core\Instance.cs" />
|
||||
<Compile Include="Core\RobloxFile.cs" />
|
||||
<Compile Include="Tree\Enums.cs" />
|
||||
<Compile Include="Tree\Property.cs" />
|
||||
<Compile Include="Tree\Instance.cs" />
|
||||
<Compile Include="RobloxFile.cs" />
|
||||
<Compile Include="DataTypes\Axes.cs" />
|
||||
<Compile Include="DataTypes\BrickColor.cs" />
|
||||
<Compile Include="DataTypes\CFrame.cs" />
|
||||
@ -86,6 +86,8 @@
|
||||
<Compile Include="DataTypes\PhysicalProperties.cs" />
|
||||
<Compile Include="DataTypes\Ray.cs" />
|
||||
<Compile Include="DataTypes\Region3int16.cs" />
|
||||
<Compile Include="Interfaces\IRobloxFile.cs" />
|
||||
<Compile Include="Interfaces\IXmlPropertyToken.cs" />
|
||||
<Compile Include="Utility\BrickColors.cs" />
|
||||
<Compile Include="DataTypes\Vector3int16.cs" />
|
||||
<Compile Include="DataTypes\Rect.cs" />
|
||||
@ -97,6 +99,7 @@
|
||||
<Compile Include="Utility\MaterialInfo.cs" />
|
||||
<Compile Include="Utility\Quaternion.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="XmlFormat\PropertyTokens\Vector3int16.cs" />
|
||||
<Compile Include="XmlFormat\XmlPropertyTokens.cs" />
|
||||
<Compile Include="XmlFormat\XmlDataReader.cs" />
|
||||
<Compile Include="XmlFormat\XmlRobloxFile.cs" />
|
||||
|
@ -376,9 +376,10 @@ namespace RobloxFiles.Enums
|
||||
public enum ContextActionPriority
|
||||
{
|
||||
Low = 1000,
|
||||
Default = 2000,
|
||||
Medium,
|
||||
High = 3000
|
||||
Medium = 2000,
|
||||
High = 3000,
|
||||
|
||||
Default = Medium
|
||||
}
|
||||
|
||||
public enum ContextActionResult
|
@ -20,6 +20,7 @@ namespace RobloxFiles
|
||||
private List<Instance> Children = new List<Instance>();
|
||||
private Instance rawParent;
|
||||
|
||||
/// <summary>The name of this Instance, if a Name property is defined.</summary>
|
||||
public string Name => ReadProperty("Name", ClassName);
|
||||
public override string ToString() => Name;
|
||||
|
||||
@ -108,15 +109,19 @@ namespace RobloxFiles
|
||||
/// </summary>
|
||||
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();
|
||||
results = results.Concat(childResults).ToArray();
|
||||
// Add this child to the results.
|
||||
results.Add(child);
|
||||
|
||||
// Add its descendants to the results.
|
||||
Instance[] descendants = child.GetDescendants();
|
||||
results.AddRange(descendants);
|
||||
}
|
||||
|
||||
return results;
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -128,23 +133,73 @@ namespace RobloxFiles
|
||||
public Instance FindFirstChild(string name, bool recursive = false)
|
||||
{
|
||||
Instance result = null;
|
||||
var query = Children.Where((child) => name == child.Name);
|
||||
|
||||
var query = Children.Where(child => child.Name == name);
|
||||
if (query.Count() > 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Returns the first Instance whose ClassName is the provided string className. If the instance is not found, this returns null.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
var query = Children.Where((child) => className == child.ClassName);
|
||||
|
||||
var query = Children.Where(child => child.ClassName == className);
|
||||
if (query.Count() > 0)
|
||||
result = query.First();
|
||||
|
||||
@ -242,13 +297,16 @@ namespace RobloxFiles
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Treats the provided string as if you were indexing a specific child or descendant of this Instance.<para/>
|
||||
/// The provided string can either be:<para/>
|
||||
/// - The name of a child that is parented to this Instance. ( Example: game["Workspace"] )<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.
|
||||
/// Allows you to access a child/descendant of this Instance, 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 Instance this[string accessor]
|
||||
public object this[string accessor]
|
||||
{
|
||||
get
|
||||
{
|
||||
@ -259,7 +317,21 @@ namespace RobloxFiles
|
||||
Instance next = result.FindFirstChild(name);
|
||||
|
||||
if (next == null)
|
||||
throw new Exception(name + " is not a valid member of " + result.Name);
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
result = next;
|
||||
}
|
@ -36,8 +36,9 @@ namespace RobloxFiles
|
||||
|
||||
public class Property
|
||||
{
|
||||
public Instance Instance;
|
||||
public string Name;
|
||||
public Instance Instance;
|
||||
|
||||
public PropertyType Type;
|
||||
public object Value;
|
||||
|
@ -39,6 +39,7 @@ namespace RobloxFiles.Utility
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
|
||||
public static IReadOnlyList<BrickColor> ColorMap = new List<BrickColor>()
|
||||
|
@ -140,7 +140,8 @@ namespace RobloxFiles.Utility
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public static IReadOnlyDictionary<Material, float> FrictionWeightMap = new Dictionary<Material, float>()
|
||||
{
|
||||
|
40
XmlFormat/PropertyTokens/Vector3int16.cs
Normal file
40
XmlFormat/PropertyTokens/Vector3int16.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using System.Xml;
|
||||
|
||||
namespace RobloxFiles.XmlFormat
|
||||
{
|
||||
static class XmlDataReader
|
||||
public static class XmlDataReader
|
||||
{
|
||||
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
|
||||
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.
|
||||
XmlNode refToken = instNode.Attributes.GetNamedItem("referent");
|
||||
|
||||
if (refToken != null && instances != null)
|
||||
if (refToken != null && file != null)
|
||||
{
|
||||
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!");
|
||||
|
||||
instances.Add(refId, inst);
|
||||
file.Instances.Add(refId, inst);
|
||||
}
|
||||
|
||||
// Process the child nodes of this instance.
|
||||
@ -73,7 +73,7 @@ namespace RobloxFiles.XmlFormat
|
||||
}
|
||||
else if (childNode.Name == "Item")
|
||||
{
|
||||
Instance child = ReadInstance(childNode, ref instances);
|
||||
Instance child = ReadInstance(childNode, file);
|
||||
child.Parent = inst;
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,6 @@ using System.Xml;
|
||||
|
||||
namespace RobloxFiles.XmlFormat
|
||||
{
|
||||
public interface IXmlPropertyToken
|
||||
{
|
||||
string Token { get; }
|
||||
bool ReadToken(Property prop, XmlNode token);
|
||||
}
|
||||
|
||||
public static class XmlPropertyTokens
|
||||
{
|
||||
public static IReadOnlyDictionary<string, IXmlPropertyToken> Handlers;
|
||||
|
@ -47,12 +47,12 @@ namespace RobloxFiles.XmlFormat
|
||||
{
|
||||
if (child.Name == "Item")
|
||||
{
|
||||
Instance item = XmlDataReader.ReadInstance(child, ref Instances);
|
||||
Instance item = XmlDataReader.ReadInstance(child, this);
|
||||
item.Parent = XmlContents;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve references for Ref properties.
|
||||
// Resolve referent properties.
|
||||
var refProps = Instances.Values
|
||||
.SelectMany(inst => inst.Properties)
|
||||
.Where(prop => prop.Type == PropertyType.Ref);
|
||||
|
Loading…
Reference in New Issue
Block a user