diff --git a/BinaryFormat/BinaryChunk.cs b/BinaryFormat/BinaryChunk.cs index da682e2..c5f50e2 100644 --- a/BinaryFormat/BinaryChunk.cs +++ b/BinaryFormat/BinaryChunk.cs @@ -5,15 +5,20 @@ using LZ4; namespace RobloxFiles.BinaryFormat { - public class RobloxBinaryChunk + /// + /// BinaryRobloxChunk represents a generic LZ4-compressed chunk + /// of data in Roblox's Binary File Format. + /// + 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); diff --git a/BinaryFormat/BinaryReader.cs b/BinaryFormat/BinaryReader.cs index d594935..dab005e 100644 --- a/BinaryFormat/BinaryReader.cs +++ b/BinaryFormat/BinaryReader.cs @@ -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(int count, Func decode) where T : struct diff --git a/BinaryFormat/BinaryRobloxFile.cs b/BinaryFormat/BinaryRobloxFile.cs index fa31771..bc50153 100644 --- a/BinaryFormat/BinaryRobloxFile.cs +++ b/BinaryFormat/BinaryRobloxFile.cs @@ -22,7 +22,7 @@ namespace RobloxFiles.BinaryFormat public Instance Contents => BinContents; // Runtime Specific - public List Chunks = new List(); + public List Chunks = new List(); 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); diff --git a/BinaryFormat/ChunkTypes/INST.cs b/BinaryFormat/ChunkTypes/INST.cs index f42d941..e51a852 100644 --- a/BinaryFormat/ChunkTypes/INST.cs +++ b/BinaryFormat/ChunkTypes/INST.cs @@ -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(); diff --git a/BinaryFormat/ChunkTypes/META.cs b/BinaryFormat/ChunkTypes/META.cs index e07cc04..029ce0c 100644 --- a/BinaryFormat/ChunkTypes/META.cs +++ b/BinaryFormat/ChunkTypes/META.cs @@ -7,9 +7,9 @@ namespace RobloxFiles.BinaryFormat.Chunks public int NumEntries; public Dictionary 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(NumEntries); diff --git a/BinaryFormat/ChunkTypes/PRNT.cs b/BinaryFormat/ChunkTypes/PRNT.cs index 15ef4ec..6cb941c 100644 --- a/BinaryFormat/ChunkTypes/PRNT.cs +++ b/BinaryFormat/ChunkTypes/PRNT.cs @@ -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(); diff --git a/BinaryFormat/ChunkTypes/PROP.cs b/BinaryFormat/ChunkTypes/PROP.cs index 99d13b7..24baccb 100644 --- a/BinaryFormat/ChunkTypes/PROP.cs +++ b/BinaryFormat/ChunkTypes/PROP.cs @@ -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(() => reader.ReadInts(instCount)); - var readFloats = new Func(() => reader.ReadFloats(instCount)); + 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) + 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(); } } } diff --git a/Core/RobloxFile.cs b/Core/RobloxFile.cs deleted file mode 100644 index 589b2f0..0000000 --- a/Core/RobloxFile.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.IO; -using System.Text; - -using RobloxFiles.BinaryFormat; -using RobloxFiles.XmlFormat; - -namespace RobloxFiles -{ - /// - /// Interface which represents a RobloxFile implementation. - /// - public interface IRobloxFile - { - Instance Contents { get; } - void ReadFile(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 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(" - /// Treats the provided string as if you were indexing a specific child or descendant of the `RobloxFile.Contents` folder. - /// The provided string can either be: - /// - The name of a child that is parented to RobloxFile.Contents ( Example: RobloxFile["Workspace"] ) - /// - A period (.) separated path to a descendant of RobloxFile.Contents ( Example: RobloxFile["Workspace.Terrain"] ) - /// This will throw an exception if any instance in the traversal is not found. - /// - public Instance this[string accessor] => Contents[accessor]; - } -} diff --git a/DataTypes/ColorSequenceKeypoint.cs b/DataTypes/ColorSequenceKeypoint.cs index 662fb42..4c007cd 100644 --- a/DataTypes/ColorSequenceKeypoint.cs +++ b/DataTypes/ColorSequenceKeypoint.cs @@ -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() diff --git a/DataTypes/Ray.cs b/DataTypes/Ray.cs index 8bc4647..1496ee5 100644 --- a/DataTypes/Ray.cs +++ b/DataTypes/Ray.cs @@ -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; } } } diff --git a/Interfaces/IRobloxFile.cs b/Interfaces/IRobloxFile.cs new file mode 100644 index 0000000..c82edab --- /dev/null +++ b/Interfaces/IRobloxFile.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RobloxFiles +{ + /// + /// Interface which represents a RobloxFile implementation. + /// + public interface IRobloxFile + { + Instance Contents { get; } + void ReadFile(byte[] buffer); + } +} diff --git a/Interfaces/IXmlPropertyToken.cs b/Interfaces/IXmlPropertyToken.cs new file mode 100644 index 0000000..36251f2 --- /dev/null +++ b/Interfaces/IXmlPropertyToken.cs @@ -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); + } +} diff --git a/RobloxFile.cs b/RobloxFile.cs new file mode 100644 index 0000000..ff104e4 --- /dev/null +++ b/RobloxFile.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +using RobloxFiles.BinaryFormat; +using RobloxFiles.XmlFormat; + +namespace RobloxFiles +{ + /// + /// Represents a loaded *.rbxl/*.rbxm Roblox file. + /// All of the surface-level Instances are stored in the RobloxFile's 'Contents' property. + /// + public class RobloxFile : IRobloxFile + { + /// + /// Indicates if this RobloxFile has loaded data already. + /// + public bool Initialized { get; private set; } + + /// + /// A reference to the inner IRobloxFile implementation that this RobloxFile opened with. + /// It can be a BinaryRobloxFile, or an XmlRobloxFile. + /// + public IRobloxFile InnerFile { get; private set; } + + /// + /// A reference to a Folder Instance that stores all of the contents that were loaded. + /// + public Instance Contents => InnerFile.Contents; + + /// + /// Initializes the RobloxFile from the provided buffer, if it hasn't been Initialized yet. + /// + /// + 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(" + /// Creates a RobloxFile from a provided byte sequence that represents the file. + /// + /// + private RobloxFile(byte[] buffer) + { + ReadFile(buffer); + } + + /// + /// Opens a Roblox file from a byte sequence that represents the file. + /// + /// A byte sequence that represents the file. + public static RobloxFile Open(byte[] buffer) + { + return new RobloxFile(buffer); + } + + /// + /// Opens a Roblox file by reading from a provided Stream. + /// + /// The stream to read the Roblox file from. + public static RobloxFile Open(Stream stream) + { + byte[] buffer; + + using (MemoryStream memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + buffer = memoryStream.ToArray(); + } + + return Open(buffer); + } + + /// + /// Opens a Roblox file from a provided file path. + /// + /// A path to a Roblox file to be opened. + public static RobloxFile Open(string filePath) + { + byte[] buffer = File.ReadAllBytes(filePath); + return Open(buffer); + } + + /// + /// Creates and runs a Task to open a Roblox file from a byte sequence that represents the file. + /// + /// A byte sequence that represents the file. + public static Task OpenAsync(byte[] buffer) + { + return Task.Run(() => Open(buffer)); + } + + /// + /// Creates and runs a Task to open a Roblox file using a provided Stream. + /// + /// The stream to read the Roblox file from. + public static Task OpenAsync(Stream stream) + { + return Task.Run(() => Open(stream)); + } + + /// + /// Opens a Roblox file from a provided file path. + /// + /// A path to a Roblox file to be opened. + public static Task OpenAsync(string filePath) + { + return Task.Run(() => Open(filePath)); + } + + /// + /// Allows you to access a child/descendant of this file's contents, and/or one of its properties. + /// The provided string should be a period-separated (.) path to what you wish to access. + /// This will throw an exception if any part of the path cannot be found. + /// + /// ~ Examples ~ + /// var terrain = robloxFile["Workspace.Terrain"] as Instance; + /// var currentCamera = robloxFile["Workspace.CurrentCamera"] as Property; + /// + /// + public object this[string accessor] => Contents[accessor]; + } +} diff --git a/RobloxFileFormat.csproj b/RobloxFileFormat.csproj index 6466d66..c706faf 100644 --- a/RobloxFileFormat.csproj +++ b/RobloxFileFormat.csproj @@ -69,10 +69,10 @@ - - - - + + + + @@ -86,6 +86,8 @@ + + @@ -97,6 +99,7 @@ + diff --git a/Core/Enums.cs b/Tree/Enums.cs similarity index 99% rename from Core/Enums.cs rename to Tree/Enums.cs index 785326f..78a40b8 100644 --- a/Core/Enums.cs +++ b/Tree/Enums.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 diff --git a/Core/Instance.cs b/Tree/Instance.cs similarity index 73% rename from Core/Instance.cs rename to Tree/Instance.cs index 3210d60..4160707 100644 --- a/Core/Instance.cs +++ b/Tree/Instance.cs @@ -20,6 +20,7 @@ namespace RobloxFiles private List Children = new List(); private Instance rawParent; + /// The name of this Instance, if a Name property is defined. public string Name => ReadProperty("Name", ClassName); public override string ToString() => Name; @@ -108,15 +109,19 @@ namespace RobloxFiles /// public Instance[] GetDescendants() { - Instance[] results = GetChildren(); + List results = new List(); - 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(); } /// @@ -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; } + /// + /// Returns the first ancestor of this Instance whose Name is the provided string name. + /// If the instance is not found, this returns null. + /// + /// The Name of the Instance to find. + 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; + } + /// /// Returns the first Instance whose ClassName is the provided string className. If the instance is not found, this returns null. /// /// The ClassName of the Instance to find. - 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 } /// - /// Treats the provided string as if you were indexing a specific child or descendant of this Instance. - /// The provided string can either be: - /// - The name of a child that is parented to this Instance. ( Example: game["Workspace"] ) - /// - A period-separated path to a descendant of this Instance. ( Example: game["Workspace.Terrain"] ) - /// 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. + /// The provided string should be a period-separated (.) path to what you wish to access. + /// This will throw an exception if any part of the path cannot be found. + /// + /// ~ Examples ~ + /// var terrain = robloxFile["Workspace.Terrain"] as Instance; + /// var currentCamera = robloxFile["Workspace.CurrentCamera"] as Property; + /// /// - 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; } diff --git a/Core/Property.cs b/Tree/Property.cs similarity index 99% rename from Core/Property.cs rename to Tree/Property.cs index 2046d18..5e261ef 100644 --- a/Core/Property.cs +++ b/Tree/Property.cs @@ -36,8 +36,9 @@ namespace RobloxFiles public class Property { - public Instance Instance; public string Name; + public Instance Instance; + public PropertyType Type; public object Value; diff --git a/Utility/BrickColors.cs b/Utility/BrickColors.cs index fdc67e4..e3ff943 100644 --- a/Utility/BrickColors.cs +++ b/Utility/BrickColors.cs @@ -39,6 +39,7 @@ namespace RobloxFiles.Utility /// /// This contains a list of all defined BrickColors on Roblox. + /// There are some name duplicates, but that's an issue on Roblox's end. /// public static IReadOnlyList ColorMap = new List() diff --git a/Utility/MaterialInfo.cs b/Utility/MaterialInfo.cs index 58b4577..f0b5882 100644 --- a/Utility/MaterialInfo.cs +++ b/Utility/MaterialInfo.cs @@ -140,7 +140,8 @@ namespace RobloxFiles.Utility /// /// A dictionary mapping materials to their default Friction. - /// 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. + /// If it isn't in here, assume their FrictionWeight is 1. /// public static IReadOnlyDictionary FrictionWeightMap = new Dictionary() { diff --git a/XmlFormat/PropertyTokens/Vector3int16.cs b/XmlFormat/PropertyTokens/Vector3int16.cs new file mode 100644 index 0000000..1f4e82a --- /dev/null +++ b/XmlFormat/PropertyTokens/Vector3int16.cs @@ -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; + } + } +} diff --git a/XmlFormat/XmlDataReader.cs b/XmlFormat/XmlDataReader.cs index fc1396c..277d61a 100644 --- a/XmlFormat/XmlDataReader.cs +++ b/XmlFormat/XmlDataReader.cs @@ -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 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; } } diff --git a/XmlFormat/XmlPropertyTokens.cs b/XmlFormat/XmlPropertyTokens.cs index 92bd54f..1526752 100644 --- a/XmlFormat/XmlPropertyTokens.cs +++ b/XmlFormat/XmlPropertyTokens.cs @@ -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 Handlers; diff --git a/XmlFormat/XmlRobloxFile.cs b/XmlFormat/XmlRobloxFile.cs index 519506a..1f34db3 100644 --- a/XmlFormat/XmlRobloxFile.cs +++ b/XmlFormat/XmlRobloxFile.cs @@ -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);