Add support for XML files.

XML support is now implemented and should generally be working!
This library should be useable now, but I still need to set it up to
work as a NuGet package.
If there are any bugs, let me know!
This commit is contained in:
CloneTrooper1019 2019-01-30 00:36:56 -06:00
parent 5319ae72f9
commit 50561460ac
44 changed files with 1292 additions and 99 deletions

View File

@ -6,7 +6,7 @@ using Roblox.BinaryFormat.Chunks;
namespace Roblox.BinaryFormat namespace Roblox.BinaryFormat
{ {
public class RobloxBinaryFile : IRobloxFile public class BinaryRobloxFile : IRobloxFile
{ {
// Header Specific // Header Specific
public const string MagicHeader = "<roblox!\x89\xff\x0d\x0a\x1a\x0a"; public const string MagicHeader = "<roblox!\x89\xff\x0d\x0a\x1a\x0a";
@ -17,8 +17,8 @@ namespace Roblox.BinaryFormat
public byte[] Reserved; public byte[] Reserved;
// IRobloxFile // IRobloxFile
public List<Instance> BinaryTrunk = new List<Instance>(); internal readonly Instance BinContents = new Instance("Folder", "BinaryRobloxFile");
public IReadOnlyList<Instance> Trunk => BinaryTrunk.AsReadOnly(); public Instance Contents => BinContents;
// Runtime Specific // Runtime Specific
public List<RobloxBinaryChunk> Chunks = new List<RobloxBinaryChunk>(); public List<RobloxBinaryChunk> Chunks = new List<RobloxBinaryChunk>();
@ -28,7 +28,7 @@ namespace Roblox.BinaryFormat
public META Metadata; public META Metadata;
public INST[] Types; public INST[] Types;
public void Initialize(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 (RobloxBinaryReader reader = new RobloxBinaryReader(file))
@ -38,7 +38,7 @@ namespace Roblox.BinaryFormat
string signature = Encoding.UTF7.GetString(binSignature); string signature = Encoding.UTF7.GetString(binSignature);
if (signature != MagicHeader) if (signature != MagicHeader)
throw new InvalidDataException("Provided file's signature does not match RobloxBinaryFile.MagicHeader!"); throw new InvalidDataException("Provided file's signature does not match BinaryRobloxFile.MagicHeader!");
// Read header data. // Read header data.
Version = reader.ReadUInt16(); Version = reader.ReadUInt16();
@ -69,8 +69,8 @@ namespace Roblox.BinaryFormat
PROP.ReadProperties(this, chunk); PROP.ReadProperties(this, chunk);
break; break;
case "PRNT": case "PRNT":
PRNT prnt = new PRNT(chunk); PRNT hierarchy = new PRNT(chunk);
prnt.Assemble(this); hierarchy.Assemble(this);
break; break;
case "META": case "META":
Metadata = new META(chunk); Metadata = new META(chunk);

View File

@ -26,13 +26,11 @@
} }
} }
public void Allocate(RobloxBinaryFile file) public void Allocate(BinaryRobloxFile file)
{ {
foreach (int instId in InstanceIds) foreach (int instId in InstanceIds)
{ {
Instance inst = new Instance(); Instance inst = new Instance(TypeName);
inst.ClassName = TypeName;
file.Instances[instId] = inst; file.Instances[instId] = inst;
} }

View File

@ -20,7 +20,7 @@
} }
} }
public void Assemble(RobloxBinaryFile file) public void Assemble(BinaryRobloxFile file)
{ {
for (int i = 0; i < NumRelations; i++) for (int i = 0; i < NumRelations; i++)
{ {
@ -28,16 +28,14 @@
int parentId = ParentIds[i]; int parentId = ParentIds[i];
Instance child = file.Instances[childId]; Instance child = file.Instances[childId];
Instance parent = null;
if (parentId >= 0) if (parentId >= 0)
{ parent = file.Instances[parentId];
Instance parent = file.Instances[parentId];
child.Parent = parent;
}
else else
{ parent = file.BinContents;
file.BinaryTrunk.Add(child);
} child.Parent = parent;
} }
} }
} }

View File

@ -9,7 +9,7 @@ namespace Roblox.BinaryFormat.Chunks
{ {
public class PROP public class PROP
{ {
public static void ReadProperties(RobloxBinaryFile file, RobloxBinaryChunk chunk) public static void ReadProperties(BinaryRobloxFile file, RobloxBinaryChunk chunk)
{ {
RobloxBinaryReader reader = chunk.GetReader("PROP"); RobloxBinaryReader reader = chunk.GetReader("PROP");
@ -38,13 +38,14 @@ namespace Roblox.BinaryFormat.Chunks
for (int i = 0; i < instCount; i++) for (int i = 0; i < instCount; i++)
{ {
int instId = ids[i]; int instId = ids[i];
Instance inst = file.Instances[instId];
Property prop = new Property(); Property prop = new Property();
prop.Name = name; prop.Name = name;
prop.Type = propType; prop.Type = propType;
props[i] = prop; prop.Instance = inst;
Instance inst = file.Instances[instId]; props[i] = prop;
inst.AddProperty(ref prop); inst.AddProperty(ref prop);
} }

View File

@ -5,12 +5,15 @@ using System.Linq;
namespace Roblox namespace Roblox
{ {
/// <summary> /// <summary>
/// Describes an object in Roblox's Parent->Child hierarchy. /// Describes an object in Roblox's DataModel hierarchy.
/// Instances can have sets of properties loaded from *.rbxl/*.rbxm files. /// Instances can have sets of properties loaded from *.rbxl/*.rbxm files.
/// </summary> /// </summary>
public class Instance public class Instance
{ {
public string ClassName = ""; /// <summary>The ClassName of this Instance.</summary>
public readonly string ClassName;
/// <summary>A list of properties that are defined under this Instance.</summary>
public List<Property> Properties = new List<Property>(); public List<Property> Properties = new List<Property>();
private List<Instance> Children = new List<Instance>(); private List<Instance> Children = new List<Instance>();
@ -19,9 +22,29 @@ namespace Roblox
public string Name => ReadProperty("Name", ClassName); public string Name => ReadProperty("Name", ClassName);
public override string ToString() => Name; public override string ToString() => Name;
/// <summary> /// <summary>Creates an instance using the provided ClassName.</summary>
/// Returns true if this Instance is an ancestor to the provided Instance. /// <param name="className">The ClassName to use for this Instance.</param>
/// </summary> public Instance(string className = "Instance")
{
ClassName = className;
}
/// <summary>Creates an instance using the provided ClassName and Name.</summary>
/// <param name="className">The ClassName to use for this Instance.</param>
/// <param name="name">The Name to use for this Instance.</param>
public Instance(string className = "Instance", string name = "Instance")
{
Property propName = new Property();
propName.Name = "Name";
propName.Type = PropertyType.String;
propName.Value = name;
propName.Instance = this;
ClassName = className;
Properties.Add(propName);
}
/// <summary>Returns true if this Instance is an ancestor to the provided Instance.</summary>
/// <param name="descendant">The instance whose descendance will be tested against this Instance.</param> /// <param name="descendant">The instance whose descendance will be tested against this Instance.</param>
public bool IsAncestorOf(Instance descendant) public bool IsAncestorOf(Instance descendant)
{ {
@ -36,9 +59,7 @@ namespace Roblox
return false; return false;
} }
/// <summary> /// <summary>Returns true if this Instance is a descendant of the provided Instance.</summary>
/// Returns true if this Instance is a descendant of the provided Instance.
/// </summary>
/// <param name="ancestor">The instance whose ancestry will be tested against this Instance.</param> /// <param name="ancestor">The instance whose ancestry will be tested against this Instance.</param>
public bool IsDescendantOf(Instance ancestor) public bool IsDescendantOf(Instance ancestor)
{ {
@ -53,14 +74,17 @@ namespace Roblox
/// </summary> /// </summary>
public Instance Parent public Instance Parent
{ {
get { return rawParent; } get
{
return rawParent;
}
set set
{ {
if (IsAncestorOf(value)) if (IsAncestorOf(value))
throw new Exception("Parent would result in circular reference."); throw new Exception("Parent would result in circular reference.");
if (Parent == this) if (Parent == this)
throw new Exception("Attempt to set parent to self"); throw new Exception("Attempt to set parent to self.");
if (rawParent != null) if (rawParent != null)
rawParent.Children.Remove(this); rawParent.Children.Remove(this);
@ -70,18 +94,37 @@ namespace Roblox
} }
} }
public IEnumerable<Instance> GetChildren() /// <summary>
/// Returns a snapshot of the Instances currently parented to this Instance, as an array.
/// </summary>
public Instance[] GetChildren()
{ {
var current = Children.ToArray(); return Children.ToArray();
return current.AsEnumerable();
} }
/// <summary> /// <summary>
/// Returns the first Instance whose Name is the provided string name. If the instance is not found, this returns null. /// Returns a snapshot of the Instances that are descendants of this Instance, as an array.
/// </summary> /// </summary>
/// <param name="name">The name of the instance to find.</param> public Instance[] GetDescendants()
/// <returns>The instance that was found with this name, or null.</returns> {
public Instance FindFirstChild(string name) Instance[] results = GetChildren();
foreach (Instance child in results)
{
Instance[] childResults = child.GetDescendants();
results = results.Concat(childResults).ToArray();
}
return results;
}
/// <summary>
/// Returns the first child 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>
/// <param name="recursive">Indicates if we should search descendants as well.</param>
public Instance FindFirstChild(string name, bool recursive = false)
{ {
Instance result = null; Instance result = null;
@ -92,6 +135,38 @@ namespace Roblox
return result; return result;
} }
/// <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)
{
Instance result = null;
var query = Children.Where(child => child.ClassName == className);
if (query.Count() > 0)
result = query.First();
return result;
}
/// <summary>
/// Returns a string descrbing the index traversal of this Instance, starting from its root ancestor.
/// </summary>
public string GetFullName()
{
string fullName = Name;
Instance at = Parent;
while (at != null)
{
fullName = at.Name + '.' + fullName;
at = at.Parent;
}
return fullName;
}
/// <summary> /// <summary>
/// Looks for a property with the specified property name, and returns it as an object. /// Looks for a property with the specified property name, and returns it as an object.
/// <para/>The resulting value may be null if the property is not serialized. /// <para/>The resulting value may be null if the property is not serialized.
@ -126,7 +201,7 @@ namespace Roblox
object result = ReadProperty(propertyName); object result = ReadProperty(propertyName);
return (T)result; return (T)result;
} }
catch (Exception e) catch
{ {
return nullFallback; return nullFallback;
} }
@ -134,7 +209,8 @@ namespace Roblox
/// <summary> /// <summary>
/// Looks for a property with the specified property name. If found, it will try to set the value of the referenced outValue to its value.<para/> /// Looks for a property with the specified property name. If found, it will try to set the value of the referenced outValue to its value.<para/>
/// Returns true if the property was found and its value was casted to the referenced outValue. If it returns false, the outValue has not been set. /// Returns true if the property was found and its value was casted to the referenced outValue.<para/>
/// If it returns false, the outValue will not have its value set.
/// </summary> /// </summary>
/// <typeparam name="T">The value type to convert to when finding the specified property name.</typeparam> /// <typeparam name="T">The value type to convert to when finding the specified property name.</typeparam>
/// <param name="propertyName">The name of the property to be fetched from this Instance.</param> /// <param name="propertyName">The name of the property to be fetched from this Instance.</param>

View File

@ -36,6 +36,7 @@ namespace Roblox
public class Property public class Property
{ {
public Instance Instance;
public string Name; public string Name;
public PropertyType Type; public PropertyType Type;
public object Value; public object Value;
@ -47,7 +48,7 @@ namespace Roblox
{ {
if (RawBuffer == null && Value != null) if (RawBuffer == null && Value != null)
{ {
// Infer what the buffer should be if this is a primitive. // Improvise what the buffer should be if this is a primitive.
switch (Type) switch (Type)
{ {
case PropertyType.Int: case PropertyType.Int:
@ -72,6 +73,16 @@ namespace Roblox
} }
} }
public string GetFullName()
{
string result = Name;
if (Instance != null)
result = Instance.GetFullName() + '.' + result;
return result;
}
public override string ToString() public override string ToString()
{ {
string typeName = Enum.GetName(typeof(PropertyType), Type); string typeName = Enum.GetName(typeof(PropertyType), Type);

View File

@ -13,8 +13,8 @@ namespace Roblox
/// </summary> /// </summary>
public interface IRobloxFile public interface IRobloxFile
{ {
IReadOnlyList<Instance> Trunk { get; } Instance Contents { get; }
void Initialize(byte[] buffer); void ReadFile(byte[] buffer);
} }
/// <summary> /// <summary>
@ -26,9 +26,9 @@ namespace Roblox
public bool Initialized { get; private set; } public bool Initialized { get; private set; }
public IRobloxFile InnerFile { get; private set; } public IRobloxFile InnerFile { get; private set; }
public IReadOnlyList<Instance> Trunk => InnerFile.Trunk; public Instance Contents => InnerFile.Contents;
public void Initialize(byte[] buffer) public void ReadFile(byte[] buffer)
{ {
if (!Initialized) if (!Initialized)
{ {
@ -37,14 +37,14 @@ namespace Roblox
string header = Encoding.UTF7.GetString(buffer, 0, 14); string header = Encoding.UTF7.GetString(buffer, 0, 14);
IRobloxFile file = null; IRobloxFile file = null;
if (header == RobloxBinaryFile.MagicHeader) if (header == BinaryRobloxFile.MagicHeader)
file = new RobloxBinaryFile(); file = new BinaryRobloxFile();
else if (header.StartsWith("<roblox")) else if (header.StartsWith("<roblox"))
file = new RobloxXmlFile(); file = new XmlRobloxFile();
if (file != null) if (file != null)
{ {
file.Initialize(buffer); file.ReadFile(buffer);
InnerFile = file; InnerFile = file;
Initialized = true; Initialized = true;
@ -58,7 +58,7 @@ namespace Roblox
public RobloxFile(byte[] buffer) public RobloxFile(byte[] buffer)
{ {
Initialize(buffer); ReadFile(buffer);
} }
public RobloxFile(Stream stream) public RobloxFile(Stream stream)
@ -71,13 +71,13 @@ namespace Roblox
buffer = memoryStream.ToArray(); buffer = memoryStream.ToArray();
} }
Initialize(buffer); ReadFile(buffer);
} }
public RobloxFile(string filePath) public RobloxFile(string filePath)
{ {
byte[] buffer = File.ReadAllBytes(filePath); byte[] buffer = File.ReadAllBytes(filePath);
Initialize(buffer); ReadFile(buffer);
} }
} }
} }

View File

@ -6,8 +6,8 @@ namespace Roblox.DataTypes
[Flags] [Flags]
public enum Axes public enum Axes
{ {
X = 0 << Axis.X, X = 1 << Axis.X,
Y = 0 << Axis.Y, Y = 1 << Axis.Y,
Z = 0 << Axis.Z, Z = 1 << Axis.Z,
} }
} }

View File

@ -26,11 +26,11 @@ namespace Roblox.DataTypes
private const string DefaultName = "Medium stone grey"; private const string DefaultName = "Medium stone grey";
private const int DefaultNumber = 194; private const int DefaultNumber = 194;
internal BrickColor(int number, int rgb, string name) internal BrickColor(int number, uint rgb, string name)
{ {
int r = (rgb / 65536) % 256; uint r = (rgb / 65536) % 256;
int g = (rgb / 256) % 256; uint g = (rgb / 256) % 256;
int b = rgb % 256; uint b = (rgb % 256);
Name = name; Name = name;
Number = number; Number = number;

View File

@ -18,7 +18,7 @@ namespace Roblox.DataTypes
return string.Join(", ", R, G, B); return string.Join(", ", R, G, B);
} }
public static Color3 fromRGB(int r = 0, int g = 0, int b = 0) public static Color3 fromRGB(uint r = 0, uint g = 0, uint b = 0)
{ {
return new Color3(r / 255f, g / 255f, b / 255f); return new Color3(r / 255f, g / 255f, b / 255f);
} }

View File

@ -3,17 +3,17 @@
public struct ColorSequenceKeypoint public struct ColorSequenceKeypoint
{ {
public readonly float Time; public readonly float Time;
public readonly Color3 Color; public readonly Color3 Value;
public ColorSequenceKeypoint(float time, Color3 color) public ColorSequenceKeypoint(float time, Color3 value)
{ {
Time = time; Time = time;
Color = color; Value = value;
} }
public override string ToString() public override string ToString()
{ {
return string.Join(" ", Time, Color.R, Color.G, Color.B, 0); return string.Join(" ", Time, Value.R, Value.G, Value.B, 0);
} }
} }
} }

View File

@ -6,11 +6,11 @@ namespace Roblox.DataTypes
[Flags] [Flags]
public enum Faces public enum Faces
{ {
Right = 0 << NormalId.Right, Right = 1 << NormalId.Right,
Top = 0 << NormalId.Top, Top = 1 << NormalId.Top,
Back = 0 << NormalId.Back, Back = 1 << NormalId.Back,
Left = 0 << NormalId.Left, Left = 1 << NormalId.Left,
Bottom = 0 << NormalId.Bottom, Bottom = 1 << NormalId.Bottom,
Front = 0 << NormalId.Front, Front = 1 << NormalId.Front,
} }
} }

View File

@ -28,7 +28,7 @@
public override string ToString() public override string ToString()
{ {
return '{' + Origin + "}, {" + Direction + '}'; return '{' + Origin.ToString() + "}, {" + Direction.ToString() + '}';
} }
public Vector3 ClosestPoint(Vector3 point) public Vector3 ClosestPoint(Vector3 point)

View File

@ -48,7 +48,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="BinaryFormat\BinaryChunk.cs" /> <Compile Include="BinaryFormat\BinaryChunk.cs" />
<Compile Include="BinaryFormat\BinaryFile.cs" /> <Compile Include="BinaryFormat\BinaryRobloxFile.cs" />
<Compile Include="BinaryFormat\ChunkTypes\INST.cs" /> <Compile Include="BinaryFormat\ChunkTypes\INST.cs" />
<Compile Include="BinaryFormat\ChunkTypes\META.cs" /> <Compile Include="BinaryFormat\ChunkTypes\META.cs" />
<Compile Include="BinaryFormat\ChunkTypes\PRNT.cs" /> <Compile Include="BinaryFormat\ChunkTypes\PRNT.cs" />
@ -83,14 +83,40 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Core\RobloxFile.cs" /> <Compile Include="Core\RobloxFile.cs" />
<Compile Include="Core\Instance.cs" /> <Compile Include="Core\Instance.cs" />
<Compile Include="XmlFormat\XmlFile.cs" /> <Compile Include="XmlFormat\PropertyTokens\PhysicalProperties.cs" />
<Compile Include="XmlFormat\PropertyTokens\NumberRange.cs" />
<Compile Include="XmlFormat\PropertyTokens\NumberSequence.cs" />
<Compile Include="XmlFormat\PropertyTokens\ColorSequence.cs" />
<Compile Include="XmlFormat\PropertyTokens\Faces.cs" />
<Compile Include="XmlFormat\PropertyTokens\Axes.cs" />
<Compile Include="XmlFormat\PropertyTokens\Color3uint8.cs" />
<Compile Include="XmlFormat\PropertyTokens\Color3.cs" />
<Compile Include="XmlFormat\PropertyTokens\Content.cs" />
<Compile Include="XmlFormat\PropertyTokens\BinaryString.cs" />
<Compile Include="XmlFormat\PropertyTokens\Rect.cs" />
<Compile Include="XmlFormat\PropertyTokens\String.cs" />
<Compile Include="XmlFormat\PropertyTokens\Double.cs" />
<Compile Include="XmlFormat\PropertyTokens\Float.cs" />
<Compile Include="XmlFormat\PropertyTokens\Boolean.cs" />
<Compile Include="XmlFormat\PropertyTokens\BrickColor.cs" />
<Compile Include="XmlFormat\PropertyTokens\Enum.cs" />
<Compile Include="XmlFormat\PropertyTokens\Int64.cs" />
<Compile Include="XmlFormat\PropertyTokens\Int.cs" />
<Compile Include="XmlFormat\PropertyTokens\Ref.cs" />
<Compile Include="XmlFormat\PropertyTokens\UDim2.cs" />
<Compile Include="XmlFormat\PropertyTokens\Vector2.cs" />
<Compile Include="XmlFormat\PropertyTokens\CFrame.cs" />
<Compile Include="XmlFormat\PropertyTokens\Ray.cs" />
<Compile Include="XmlFormat\PropertyTokens\UDim.cs" />
<Compile Include="XmlFormat\PropertyTokens\Vector3.cs" />
<Compile Include="XmlFormat\XmlPropertyTokens.cs" />
<Compile Include="XmlFormat\XmlDataReader.cs" />
<Compile Include="XmlFormat\XmlRobloxFile.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup />
<Folder Include="XmlFormat\TokenHandlers\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -0,0 +1,31 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class AxesToken : IXmlPropertyToken
{
public string Token => "Axes";
public bool ReadToken(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadTokenGeneric<uint>(prop, PropertyType.Axes, token);
if (success)
{
uint value = (uint)prop.Value;
try
{
Axes axes = (Axes)value;
prop.Value = axes;
}
catch
{
success = false;
}
}
return success;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class BinaryStringToken : IXmlPropertyToken
{
public string Token => "BinaryString";
public bool ReadToken(Property prop, XmlNode token)
{
// BinaryStrings are encoded in base64
string base64 = token.InnerText;
prop.Type = PropertyType.String;
prop.Value = base64;
byte[] buffer = Convert.FromBase64String(base64);
prop.SetRawBuffer(buffer);
return true;
}
}
}

View File

@ -0,0 +1,14 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class BoolToken : IXmlPropertyToken
{
public string Token => "bool";
public bool ReadToken(Property prop, XmlNode token)
{
return XmlPropertyTokens.ReadTokenGeneric<bool>(prop, PropertyType.Bool, token);
}
}
}

View File

@ -0,0 +1,35 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class BrickColorToken : IXmlPropertyToken
{
public string Token => "BrickColor";
// ^ This is a lie: The token is actually int, but that would cause a name collision.
// Since BrickColors are written as ints, the IntToken class will try to redirect
// to this handler if it believes that its representing a BrickColor.
public bool ReadToken(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadTokenGeneric<int>(prop, PropertyType.BrickColor, token);
if (success)
{
int value = (int)prop.Value;
try
{
BrickColor brickColor = BrickColor.FromNumber(value);
prop.Value = brickColor;
}
catch
{
// Invalid BrickColor Id?
success = false;
}
}
return success;
}
}
}

View File

@ -0,0 +1,47 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class CFrameToken : IXmlPropertyToken
{
public string Token => "CoordinateFrame; CFrame";
private static string[] Coords = new string[12] { "X", "Y", "Z", "R00", "R01", "R02", "R10", "R11", "R12", "R20", "R21", "R22"};
public static CFrame ReadCFrame(XmlNode token)
{
float[] components = new float[12];
for (int i = 0; i < 12; i++)
{
string key = Coords[i];
try
{
var coord = token[key];
components[i] = float.Parse(coord.InnerText);
}
catch
{
return null;
}
}
return new CFrame(components);
}
public bool ReadToken(Property property, XmlNode token)
{
CFrame result = ReadCFrame(token);
bool success = (result != null);
if (success)
{
property.Type = PropertyType.CFrame;
property.Value = result;
}
return success;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class Color3Token : IXmlPropertyToken
{
public string Token => "Color3";
private string[] LegacyFields = new string[3] { "R", "G", "B" };
public bool ReadToken(Property prop, XmlNode token)
{
var color3uint8 = XmlPropertyTokens.GetHandler<Color3uint8Token>();
bool success = color3uint8.ReadToken(prop, token);
if (!success)
{
// Try the legacy technique.
float[] fields = new float[LegacyFields.Length];
for (int i = 0; i < fields.Length; i++)
{
string key = LegacyFields[i];
try
{
var coord = token[key];
fields[i] = XmlPropertyTokens.ParseFloat(coord.InnerText);
}
catch
{
return false;
}
}
float r = fields[0],
g = fields[1],
b = fields[2];
prop.Type = PropertyType.Color3;
prop.Value = new Color3(r, g, b);
success = true;
}
return success;
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class Color3uint8Token : IXmlPropertyToken
{
public string Token => "Color3uint8";
public bool ReadToken(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadTokenGeneric<uint>(prop, PropertyType.Color3, token);
if (success)
{
uint value = (uint)prop.Value;
uint r = (value >> 16) & 0xFF;
uint g = (value >> 8) & 0xFF;
uint b = value & 0xFF;
prop.Value = Color3.fromRGB(r, g, b);
}
return success;
}
}
}

View File

@ -0,0 +1,48 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class ColorSequenceToken : IXmlPropertyToken
{
public string Token => "ColorSequence";
public bool ReadToken(Property prop, XmlNode token)
{
string contents = token.InnerText.Trim();
string[] buffer = contents.Split(' ');
int length = buffer.Length;
bool valid = (length % 5 == 0);
if (valid)
{
try
{
ColorSequenceKeypoint[] keypoints = new ColorSequenceKeypoint[length / 5];
for (int i = 0; i < length; i += 5)
{
float Time = float.Parse(buffer[i]);
float R = float.Parse(buffer[i + 1]);
float G = float.Parse(buffer[i + 2]);
float B = float.Parse(buffer[i + 3]);
Color3 Value = new Color3(R, G, B);
keypoints[i / 5] = new ColorSequenceKeypoint(Time, Value);
}
prop.Type = PropertyType.ColorSequence;
prop.Value = new ColorSequence(keypoints);
}
catch
{
valid = false;
}
}
return valid;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class ContentToken : IXmlPropertyToken
{
public string Token => "Content";
public bool ReadToken(Property prop, XmlNode token)
{
string content = token.InnerText;
prop.Type = PropertyType.String;
prop.Value = content;
if (token.HasChildNodes)
{
XmlNode childNode = token.FirstChild;
string contentType = childNode.Name;
if (contentType.StartsWith("binary") || contentType == "hash")
{
// Roblox technically doesn't support this anymore, but load it anyway :P
byte[] buffer = Convert.FromBase64String(content);
prop.SetRawBuffer(buffer);
}
}
return true;
}
}
}

View File

@ -0,0 +1,14 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class DoubleToken : IXmlPropertyToken
{
public string Token => "double";
public bool ReadToken(Property prop, XmlNode token)
{
return XmlPropertyTokens.ReadTokenGeneric<double>(prop, PropertyType.Double, token);
}
}
}

View File

@ -0,0 +1,14 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class EnumToken : IXmlPropertyToken
{
public string Token => "token";
public bool ReadToken(Property prop, XmlNode token)
{
return XmlPropertyTokens.ReadTokenGeneric<uint>(prop, PropertyType.Enum, token);
}
}
}

View File

@ -0,0 +1,31 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class FacesToken : IXmlPropertyToken
{
public string Token => "Faces";
public bool ReadToken(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadTokenGeneric<uint>(prop, PropertyType.Faces, token);
if (success)
{
uint value = (uint)prop.Value;
try
{
Faces faces = (Faces)value;
prop.Value = faces;
}
catch
{
success = false;
}
}
return success;
}
}
}

View File

@ -0,0 +1,24 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class FloatToken : IXmlPropertyToken
{
public string Token => "float";
public bool ReadToken(Property prop, XmlNode token)
{
try
{
float value = XmlPropertyTokens.ParseFloat(token.InnerText);
prop.Type = PropertyType.Float;
prop.Value = value;
return true;
}
catch
{
return false;
}
}
}
}

View File

@ -0,0 +1,25 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class IntToken : IXmlPropertyToken
{
public string Token => "int";
public bool ReadToken(Property prop, XmlNode token)
{
// BrickColors are represented by ints, see if
// we can infer when they should be a BrickColor.
if (prop.Name.Contains("Color") || prop.Instance.ClassName.Contains("Color"))
{
var brickColorToken = XmlPropertyTokens.GetHandler<BrickColorToken>();
return brickColorToken.ReadToken(prop, token);
}
else
{
return XmlPropertyTokens.ReadTokenGeneric<int>(prop, PropertyType.Int, token);
}
}
}
}

View File

@ -0,0 +1,14 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class Int64Token : IXmlPropertyToken
{
public string Token => "int64";
public bool ReadToken(Property prop, XmlNode token)
{
return XmlPropertyTokens.ReadTokenGeneric<long>(prop, PropertyType.Int64, token);
}
}
}

View File

@ -0,0 +1,35 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class NumberRangeToken : IXmlPropertyToken
{
public string Token => "NumberRange";
public bool ReadToken(Property prop, XmlNode token)
{
string contents = token.InnerText.Trim();
string[] buffer = contents.Split(' ');
bool valid = (buffer.Length == 2);
if (valid)
{
try
{
float min = float.Parse(buffer[0]);
float max = float.Parse(buffer[1]);
prop.Type = PropertyType.NumberRange;
prop.Value = new NumberRange(min, max);
}
catch
{
valid = false;
}
}
return valid;
}
}
}

View File

@ -0,0 +1,45 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class NumberSequenceToken : IXmlPropertyToken
{
public string Token => "NumberSequence";
public bool ReadToken(Property prop, XmlNode token)
{
string contents = token.InnerText.Trim();
string[] buffer = contents.Split(' ');
int length = buffer.Length;
bool valid = (length % 3 == 0);
if (valid)
{
try
{
NumberSequenceKeypoint[] keypoints = new NumberSequenceKeypoint[length / 3];
for (int i = 0; i < length; i += 3)
{
float Time = float.Parse(buffer[ i ]);
float Value = float.Parse(buffer[i + 1]);
float Envelope = float.Parse(buffer[i + 2]);
keypoints[i / 3] = new NumberSequenceKeypoint(Time, Value, Envelope);
}
prop.Type = PropertyType.NumberSequence;
prop.Value = new NumberSequence(keypoints);
}
catch
{
valid = false;
}
}
return valid;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class PhysicalPropertiesToken : IXmlPropertyToken
{
public string Token => "PhysicalProperties";
private Func<string, T> createReader<T>(Func<string, T> parse, XmlNode token) where T : struct
{
return new Func<string, T>(key =>
{
XmlElement node = token[key];
return parse(node.InnerText);
});
}
public bool ReadToken(Property prop, XmlNode token)
{
var readBool = createReader(bool.Parse, token);
var readFloat = createReader(XmlPropertyTokens.ParseFloat, token);
try
{
bool custom = readBool("CustomPhysics");
if (custom)
{
prop.Value = new PhysicalProperties
(
readFloat("Density"),
readFloat("Friction"),
readFloat("Elasticity"),
readFloat("FrictionWeight"),
readFloat("ElasticityWeight")
);
prop.Type = PropertyType.PhysicalProperties;
}
return true;
}
catch
{
return false;
}
}
}
}

View File

@ -0,0 +1,40 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class RayToken : IXmlPropertyToken
{
public string Token => "Ray";
private static string[] Fields = new string[2] { "origin", "direction" };
public bool ReadToken(Property prop, XmlNode token)
{
Vector3[] read = new Vector3[Fields.Length];
for (int i = 0; i < read.Length; i++)
{
string field = Fields[i];
try
{
var fieldToken = token[field];
Vector3? vector3 = Vector3Token.ReadVector3(fieldToken);
read[i] = vector3.Value;
}
catch
{
return false;
}
}
Vector3 origin = read[0],
direction = read[1];
Ray ray = new Ray(origin, direction);
prop.Type = PropertyType.Ray;
prop.Value = ray;
return true;
}
}
}

View File

@ -0,0 +1,40 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class RectToken : IXmlPropertyToken
{
public string Token => "Rect2D";
private static string[] Fields = new string[2] { "min", "max" };
public bool ReadToken(Property prop, XmlNode token)
{
Vector2[] read = new Vector2[Fields.Length];
for (int i = 0; i < read.Length; i++)
{
string field = Fields[i];
try
{
var fieldToken = token[field];
Vector2? vector2 = Vector2Token.ReadVector2(fieldToken);
read[i] = vector2.Value;
}
catch
{
return false;
}
}
Vector2 min = read[0],
max = read[1];
Rect rect = new Rect(min, max);
prop.Type = PropertyType.Rect;
prop.Value = rect;
return true;
}
}
}

View File

@ -0,0 +1,18 @@
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class RefToken : IXmlPropertyToken
{
public string Token => "Ref";
public bool ReadToken(Property prop, XmlNode token)
{
string refId = token.InnerText;
prop.Type = PropertyType.Ref;
prop.Value = refId;
return true;
}
}
}

View File

@ -0,0 +1,22 @@
using System.Text;
using System.Xml;
namespace Roblox.XmlFormat.PropertyTokens
{
public class StringToken : IXmlPropertyToken
{
public string Token => "string; ProtectedString";
public bool ReadToken(Property prop, XmlNode token)
{
string contents = token.InnerText;
prop.Type = PropertyType.String;
prop.Value = contents;
byte[] buffer = Encoding.UTF8.GetBytes(contents);
prop.SetRawBuffer(buffer);
return true;
}
}
}

View File

@ -0,0 +1,42 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class UDimToken : IXmlPropertyToken
{
public string Token => "UDim";
public static UDim? ReadUDim(XmlNode token, string prefix = "")
{
try
{
XmlElement scaleToken = token[prefix + 'S'];
float scale = XmlPropertyTokens.ParseFloat(scaleToken.InnerText);
XmlElement offsetToken = token[prefix + 'O'];
int offset = int.Parse(offsetToken.InnerText);
return new UDim(scale, offset);
}
catch
{
return null;
}
}
public bool ReadToken(Property property, XmlNode token)
{
UDim? result = ReadUDim(token);
bool success = (result != null);
if (success)
{
property.Type = PropertyType.UDim;
property.Value = result;
}
return success;
}
}
}

View File

@ -0,0 +1,26 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class UDim2Token : IXmlPropertyToken
{
public string Token => "UDim2";
public bool ReadToken(Property property, XmlNode token)
{
UDim? xDim = UDimToken.ReadUDim(token, "X");
UDim? yDim = UDimToken.ReadUDim(token, "Y");
if (xDim != null && yDim != null)
{
property.Type = PropertyType.UDim2;
property.Value = new UDim2(xDim.Value, yDim.Value);
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,48 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class Vector2Token : IXmlPropertyToken
{
public string Token => "Vector2";
private static string[] Coords = new string[2] { "X", "Y" };
public static Vector2? ReadVector2(XmlNode token)
{
float[] xy = new float[2];
for (int i = 0; i < 2; i++)
{
string key = Coords[i];
try
{
var coord = token[key];
string text = coord.InnerText;
xy[i] = XmlPropertyTokens.ParseFloat(text);
}
catch
{
return null;
}
}
return new Vector2(xy);
}
public bool ReadToken(Property property, XmlNode token)
{
Vector2? result = ReadVector2(token);
bool success = (result != null);
if (success)
{
property.Type = PropertyType.Vector2;
property.Value = result;
}
return success;
}
}
}

View File

@ -0,0 +1,47 @@
using System.Xml;
using Roblox.DataTypes;
namespace Roblox.XmlFormat.PropertyTokens
{
public class Vector3Token : IXmlPropertyToken
{
public string Token => "Vector3";
private static string[] Coords = new string[3] { "X", "Y", "Z" };
public static Vector3? ReadVector3(XmlNode token)
{
float[] xyz = new float[3];
for (int i = 0; i < 3; i++)
{
string key = Coords[i];
try
{
var coord = token[key];
xyz[i] = XmlPropertyTokens.ParseFloat(coord.InnerText);
}
catch
{
return null;
}
}
return new Vector3(xyz);
}
public bool ReadToken(Property property, XmlNode token)
{
Vector3? result = ReadVector3(token);
bool success = (result != null);
if (success)
{
property.Type = PropertyType.Vector3;
property.Value = result;
}
return success;
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Xml;
namespace Roblox.XmlFormat
{
static class XmlDataReader
{
public static void ReadProperties(Instance instance, XmlNode propsNode)
{
if (propsNode.Name != "Properties")
throw new Exception("XmlDataReader.ReadProperties: Provided XmlNode's class should be 'Properties'!");
foreach (XmlNode propNode in propsNode.ChildNodes)
{
string propType = propNode.Name;
XmlNode propName = propNode.Attributes.GetNamedItem("name");
if (propName == null)
throw new Exception("XmlDataReader.ReadProperties: Got a property node without a 'name' attribute!");
IXmlPropertyToken tokenHandler = XmlPropertyTokens.GetHandler(propType);
if (tokenHandler != null)
{
Property prop = new Property();
prop.Name = propName.InnerText;
prop.Instance = instance;
if (!tokenHandler.ReadToken(prop, propNode))
Console.WriteLine("XmlDataReader.ReadProperties: Could not read property: " + prop.GetFullName() + '!');
instance.AddProperty(ref prop);
}
else
{
Console.WriteLine("XmlDataReader.ReadProperties: No IXmlPropertyToken found for property type: " + propType + '!');
}
}
}
public static Instance ReadInstance(XmlNode instNode, ref Dictionary<string, Instance> instances)
{
// Process the instance itself
if (instNode.Name != "Item")
throw new Exception("XmlDataReader.ReadItem: Provided XmlNode's class should be 'Item'!");
XmlNode classToken = instNode.Attributes.GetNamedItem("class");
if (classToken == null)
throw new Exception("XmlDataReader.ReadItem: Got an Item without a defined 'class' attribute!");
Instance inst = new Instance(classToken.InnerText);
// The 'reference' 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)
{
string refId = refToken.InnerText;
if (instances.ContainsKey(refId))
throw new Exception("XmlDataReader.ReadItem: Got an Item with a duplicate 'referent' attribute!");
instances.Add(refId, inst);
}
// Process the child nodes of this instance.
foreach (XmlNode childNode in instNode.ChildNodes)
{
if (childNode.Name == "Properties")
{
ReadProperties(inst, childNode);
}
else if (childNode.Name == "Item")
{
Instance child = ReadInstance(childNode, ref instances);
child.Parent = inst;
}
}
return inst;
}
}
}

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace Roblox.XmlFormat
{
public class RobloxXmlFile : IRobloxFile
{
private List<Instance> XmlTrunk = new List<Instance>();
public IReadOnlyList<Instance> Trunk => XmlTrunk;
public void Initialize(byte[] buffer)
{
// TODO!
}
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml;
namespace Roblox.XmlFormat
{
public interface IXmlPropertyToken
{
string Token { get; }
bool ReadToken(Property prop, XmlNode token);
}
public static class XmlPropertyTokens
{
public static IReadOnlyDictionary<string, IXmlPropertyToken> Handlers;
static XmlPropertyTokens()
{
// Initialize the PropertyToken handler singletons.
Type IXmlPropertyToken = typeof(IXmlPropertyToken);
var handlerTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => type != IXmlPropertyToken)
.Where(type => IXmlPropertyToken.IsAssignableFrom(type));
var propTokens = handlerTypes.Select(handlerType => Activator.CreateInstance(handlerType) as IXmlPropertyToken);
var tokenHandlers = new Dictionary<string, IXmlPropertyToken>();
foreach (IXmlPropertyToken propToken in propTokens)
{
var tokens = propToken.Token.Split(';')
.Select(token => token.Trim())
.ToList();
tokens.ForEach(token => tokenHandlers.Add(token, propToken));
}
Handlers = tokenHandlers;
}
public static bool ReadTokenGeneric<T>(Property prop, PropertyType propType, XmlNode token) where T : struct
{
Type resultType = typeof(T);
TypeConverter converter = TypeDescriptor.GetConverter(resultType);
if (converter != null)
{
object result = converter.ConvertFromString(token.InnerText);
prop.Type = propType;
prop.Value = result;
return true;
}
return false;
}
public static IXmlPropertyToken GetHandler(string tokenName)
{
IXmlPropertyToken result = null;
if (Handlers.ContainsKey(tokenName))
result = Handlers[tokenName];
return result;
}
public static T GetHandler<T>() where T : IXmlPropertyToken
{
IXmlPropertyToken result = Handlers.Values
.Where(token => token is T)
.First();
return (T)result;
}
public static float ParseFloat(string value)
{
float result;
if (value == "INF")
result = float.PositiveInfinity;
else if (value == "-INF")
result = float.NegativeInfinity;
else if (value == "NAN")
result = float.NaN;
else
result = float.Parse(value);
return result;
}
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
namespace Roblox.XmlFormat
{
public class XmlRobloxFile : IRobloxFile
{
// IRobloxFile
internal readonly Instance XmlContents = new Instance("Folder", "XmlRobloxFile");
public Instance Contents => XmlContents;
// Runtime Specific
public readonly XmlDocument Root = new XmlDocument();
public Dictionary<string, Instance> Instances = new Dictionary<string, Instance>();
public void ReadFile(byte[] buffer)
{
try
{
string xml = Encoding.UTF8.GetString(buffer);
Root.LoadXml(xml);
}
catch
{
throw new Exception("XmlRobloxFile: Could not read XML!");
}
XmlNode roblox = Root.FirstChild;
if (roblox != null && roblox.Name == "roblox")
{
// Verify the version we are using.
XmlNode version = roblox.Attributes.GetNamedItem("version");
int schemaVersion;
if (version == null || !int.TryParse(version.Value, out schemaVersion))
throw new Exception("XmlRobloxFile: No version number defined!");
else if (schemaVersion < 4)
throw new Exception("XmlRobloxFile: Provided version must be at least 4!");
// Process the instances.
foreach (XmlNode child in roblox.ChildNodes)
{
if (child.Name == "Item")
{
Instance item = XmlDataReader.ReadInstance(child, ref Instances);
item.Parent = XmlContents;
}
}
// Resolve references for Ref properties.
var refProps = Instances.Values
.SelectMany(inst => inst.Properties)
.Where(prop => prop.Type == PropertyType.Ref);
foreach (Property refProp in refProps)
{
string refId = refProp.Value as string;
if (Instances.ContainsKey(refId))
{
Instance refInst = Instances[refId];
refProp.Value = refInst;
}
else if (refId != "null")
{
Console.WriteLine("XmlRobloxFile: Could not resolve reference for " + refProp.GetFullName());
}
}
}
else
{
throw new Exception("XmlRobloxFile: No `roblox` tag found!");
}
}
}
}