Large scale refactor to add class support!

Instance classes are now strongly typed with real property fields that
are derived from the JSON API Dump! This required a lot of reworking
across the board:

- Classes and Enums are auto-generated in the 'Generated' folder now.
This is done using a custom built-in plugin, which can be found in
the Plugins folder of this project.
- Property objects are now tied to .NET's reflection system. Reading
and writing from them will try to redirect into a field of the
Instance they are bound to.
- Property types that were loosely defined now have proper data types
(such as Color3uint8, Content, ProtectedString, SharedString, etc)
- Fixed an error with the CFrame directional vectors.
- The binary PRNT chunk now writes instances in child->parent order.
- Enums are now generated correctly, with up-to-date values.
- INST chunks are now referred to as 'Classes' instead of 'Types'.
- Unary operator added to Vector2 and Vector3.
- CollectionService tags can now be manipulated per-instance using
the Instance.Tags member.
- The Instance.Archivable property now works correctly.
- XML files now save/load metadata correctly.
- Cleaned up the property tokens directory.

I probably missed a few things, but that's a general overview of
everything that changed.
This commit is contained in:
CloneTrooper1019 2019-06-30 17:01:19 -05:00
parent 8e01f33d6b
commit de8df15d3f
67 changed files with 6297 additions and 667 deletions

View File

@ -35,9 +35,8 @@ namespace RobloxFiles.BinaryFormat
public override string ToString()
{
string chunkType = ChunkType.Replace('\0', ' ');
int bytes = (HasCompressedData ? CompressedSize : Size);
return $"'{chunkType}' Chunk ({bytes} bytes)";
return $"'{chunkType}' Chunk ({Size} bytes) [{Handler?.ToString()}]";
}
public BinaryRobloxFileChunk(BinaryRobloxFileReader reader)

View File

@ -4,9 +4,11 @@ using System.IO;
using System.Linq;
using System.Text;
using RobloxFiles.BinaryFormat;
using RobloxFiles.BinaryFormat.Chunks;
using RobloxFiles.DataTypes;
namespace RobloxFiles.BinaryFormat
namespace RobloxFiles
{
public class BinaryRobloxFile : RobloxFile
{
@ -14,7 +16,7 @@ namespace RobloxFiles.BinaryFormat
public const string MagicHeader = "<roblox!\x89\xff\x0d\x0a\x1a\x0a";
public ushort Version;
public uint NumTypes;
public uint NumClasses;
public uint NumInstances;
public long Reserved;
@ -23,7 +25,7 @@ namespace RobloxFiles.BinaryFormat
public override string ToString() => GetType().Name;
public Instance[] Instances;
public INST[] Types;
public INST[] Classes;
internal META META = null;
internal SSTR SSTR = null;
@ -32,9 +34,9 @@ namespace RobloxFiles.BinaryFormat
public Dictionary<string, string> Metadata => META?.Data;
public bool HasSharedStrings => (SSTR != null);
public IReadOnlyDictionary<uint, string> SharedStrings => SSTR?.Strings;
public IReadOnlyDictionary<uint, SharedString> SharedStrings => SSTR?.Strings;
internal BinaryRobloxFile()
public BinaryRobloxFile()
{
Name = "BinaryRobloxFile";
ParentLocked = true;
@ -55,14 +57,14 @@ namespace RobloxFiles.BinaryFormat
// Read header data.
Version = reader.ReadUInt16();
NumTypes = reader.ReadUInt32();
NumClasses = reader.ReadUInt32();
NumInstances = reader.ReadUInt32();
Reserved = reader.ReadInt64();
// Begin reading the file chunks.
bool reading = true;
Types = new INST[NumTypes];
Classes = new INST[NumClasses];
Instances = new Instance[NumInstances];
while (reading)
@ -92,6 +94,7 @@ namespace RobloxFiles.BinaryFormat
handler = new SSTR();
break;
case "END\0":
Chunks.Add(chunk);
reading = false;
break;
default:
@ -129,42 +132,35 @@ namespace RobloxFiles.BinaryFormat
Chunks.Clear();
NumInstances = 0;
NumTypes = 0;
NumClasses = 0;
SSTR = null;
// Record all instances and types.
// Recursively capture all instances and classes.
writer.RecordInstances(Children);
// Apply the type values.
INST.ApplyTypeMap(writer);
// Apply the recorded instances and classes.
writer.ApplyClassMap();
// Write the INST chunks.
foreach (INST type in Types)
{
var instChunk = type.SaveAsChunk(writer);
Chunks.Add(instChunk);
}
foreach (INST inst in Classes)
writer.SaveChunk(inst);
// Write the PROP chunks.
foreach (INST type in Types)
foreach (INST inst in Classes)
{
Dictionary<string, PROP> props = PROP.CollectProperties(writer, type);
Dictionary<string, PROP> props = PROP.CollectProperties(writer, inst);
foreach (string propName in props.Keys)
{
PROP prop = props[propName];
var chunk = prop.SaveAsChunk(writer);
Chunks.Add(chunk);
writer.SaveChunk(prop);
}
}
// Write the PRNT chunk.
PRNT parents = new PRNT();
var parentChunk = parents.SaveAsChunk(writer);
Chunks.Add(parentChunk);
writer.SaveChunk(parents);
// Write the SSTR chunk.
if (HasSharedStrings)
{
@ -180,15 +176,12 @@ namespace RobloxFiles.BinaryFormat
}
// Write the END_ chunk.
writer.StartWritingChunk("END\0");
writer.WriteString("</roblox>", true);
var endChunk = writer.FinishWritingChunk(false);
var endChunk = writer.WriteEndChunk();
Chunks.Add(endChunk);
}
//////////////////////////////////////////////////////////////////////////
// Write the chunks with the header & footer data
// Write the chunk buffers with the header data
//////////////////////////////////////////////////////////////////////////
using (BinaryWriter writer = new BinaryWriter(stream))
@ -196,20 +189,15 @@ namespace RobloxFiles.BinaryFormat
stream.Position = 0;
stream.SetLength(0);
byte[] magicHeader = MagicHeader
writer.Write(MagicHeader
.Select(ch => (byte)ch)
.ToArray();
writer.Write(magicHeader);
.ToArray());
writer.Write(Version);
writer.Write(NumTypes);
writer.Write(NumClasses);
writer.Write(NumInstances);
writer.Write(Reserved);
// Write the 8 reserved-bytes.
writer.Write(0L);
// Write all of the chunks.
foreach (BinaryRobloxFileChunk chunk in Chunks)
{
if (chunk.HasWriteBuffer)

View File

@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using System;
using System.Collections.Generic;
namespace RobloxFiles.BinaryFormat.Chunks
{
public class INST : IBinaryFileChunk
{
public int TypeIndex { get; internal set; }
public string TypeName { get; internal set; }
public int ClassIndex { get; internal set; }
public string ClassName { get; internal set; }
public bool IsService { get; internal set; }
public List<bool> RootedServices { get; internal set; }
@ -14,17 +14,14 @@ namespace RobloxFiles.BinaryFormat.Chunks
public int NumInstances { get; internal set; }
public List<int> InstanceIds { get; internal set; }
public override string ToString()
{
return TypeName;
}
public override string ToString() => ClassName;
public void LoadFromReader(BinaryRobloxFileReader reader)
{
BinaryRobloxFile file = reader.File;
TypeIndex = reader.ReadInt32();
TypeName = reader.ReadString();
ClassIndex = reader.ReadInt32();
ClassName = reader.ReadString();
IsService = reader.ReadBoolean();
NumInstances = reader.ReadInt32();
@ -44,32 +41,30 @@ namespace RobloxFiles.BinaryFormat.Chunks
for (int i = 0; i < NumInstances; i++)
{
int instId = InstanceIds[i];
Type instType = Type.GetType($"RobloxFiles.{ClassName}") ?? typeof(Instance);
var inst = new Instance()
{
ClassName = TypeName,
IsService = IsService,
Referent = instId.ToString()
};
var inst = Activator.CreateInstance(instType) as Instance;
inst.Referent = instId.ToString();
inst.IsService = IsService;
if (IsService)
{
bool rooted = RootedServices[i];
inst.IsRootedService = rooted;
bool isRooted = RootedServices[i];
inst.Parent = (isRooted ? file : null);
}
file.Instances[instId] = inst;
}
file.Types[TypeIndex] = this;
file.Classes[ClassIndex] = this;
}
public BinaryRobloxFileChunk SaveAsChunk(BinaryRobloxFileWriter writer)
{
writer.StartWritingChunk(this);
writer.Write(TypeIndex);
writer.WriteString(TypeName);
writer.Write(ClassIndex);
writer.WriteString(ClassName);
writer.Write(IsService);
writer.Write(NumInstances);
@ -82,30 +77,13 @@ namespace RobloxFiles.BinaryFormat.Chunks
foreach (int instId in InstanceIds)
{
Instance service = file.Instances[instId];
writer.Write(service.IsRootedService);
bool isRooted = (service.Parent == file);
writer.Write(isRooted);
}
}
return writer.FinishWritingChunk();
}
internal static void ApplyTypeMap(BinaryRobloxFileWriter writer)
{
BinaryRobloxFile file = writer.File;
file.Instances = writer.Instances.ToArray();
var types = writer.TypeMap
.OrderBy(type => type.Key)
.Select(type => type.Value)
.ToArray();
for (int i = 0; i < types.Length; i++, file.NumTypes++)
{
INST type = types[i];
type.TypeIndex = i;
}
file.Types = types;
}
}
}

View File

@ -27,21 +27,21 @@ namespace RobloxFiles.BinaryFormat.Chunks
Instance child = file.Instances[childId];
child.Parent = (parentId >= 0 ? file.Instances[parentId] : file);
child.ParentLocked = child.IsService;
}
}
public BinaryRobloxFileChunk SaveAsChunk(BinaryRobloxFileWriter writer)
{
BinaryRobloxFile file = writer.File;
writer.StartWritingChunk(this);
Format = 0;
NumRelations = file.Instances.Length;
NumRelations = 0;
ChildrenIds = new List<int>();
ParentIds = new List<int>();
foreach (Instance inst in file.Instances)
foreach (Instance inst in writer.PostInstances)
{
Instance parent = inst.Parent;
@ -53,6 +53,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
ChildrenIds.Add(childId);
ParentIds.Add(parentId);
NumRelations++;
}
writer.Write(Format);

View File

@ -1,50 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Reflection;
using System.Text;
using RobloxFiles.Enums;
using RobloxFiles.DataTypes;
using RobloxFiles.Utility;
using System.Text;
namespace RobloxFiles.BinaryFormat.Chunks
{
public class PROP : IBinaryFileChunk
{
public string Name { get; internal set; }
public int TypeIndex { get; internal set; }
public int ClassIndex { get; internal set; }
public string ClassName { get; private set; }
public PropertyType Type { get; internal set; }
public byte TypeId
{
get { return (byte)Type; }
internal set { Type = (PropertyType)value; }
}
public override string ToString()
{
return $"{Type} {ClassName}.{Name}";
}
public void LoadFromReader(BinaryRobloxFileReader reader)
{
BinaryRobloxFile file = reader.File;
TypeIndex = reader.ReadInt32();
ClassIndex = reader.ReadInt32();
Name = reader.ReadString();
TypeId = reader.ReadByte();
INST type = file.Types[TypeIndex];
Property[] props = new Property[type.NumInstances];
byte propType = reader.ReadByte();
Type = (PropertyType)propType;
INST inst = file.Classes[ClassIndex];
ClassName = inst.ClassName;
var ids = type.InstanceIds;
int instCount = type.NumInstances;
Property[] props = new Property[inst.NumInstances];
var ids = inst.InstanceIds;
int instCount = inst.NumInstances;
for (int i = 0; i < instCount; i++)
{
int id = ids[i];
Instance inst = file.Instances[id];
Instance instance = file.Instances[id];
Property prop = new Property(inst, this);
Property prop = new Property(instance, this);
props[i] = prop;
inst.AddProperty(ref prop);
instance.AddProperty(ref prop);
}
// Setup some short-hand functions for actions used during the read procedure.
@ -66,14 +78,36 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.String:
readProperties(i =>
{
string result = reader.ReadString();
string value = reader.ReadString();
// Leave an access point for the original byte sequence, in case this is a BinaryString.
// This will allow the developer to read the sequence without any mangling from C# strings.
byte[] buffer = reader.GetLastStringBuffer();
props[i].RawBuffer = buffer;
return result;
// Check if this is going to be casted as a BinaryString.
// BinaryStrings should use a type of byte[] instead.
Property prop = props[i];
Instance instance = prop.Instance;
Type instType = instance.GetType();
FieldInfo field = instType.GetField(Name);
if (field != null)
{
object result = value;
Type fieldType = field.FieldType;
if (fieldType == typeof(byte[]))
result = buffer;
return result;
}
else
{
return value;
}
});
break;
@ -213,15 +247,16 @@ namespace RobloxFiles.BinaryFormat.Chunks
case PropertyType.Quaternion:
// Temporarily load the rotation matrices into their properties.
// We'll update them to CFrames once we iterate over the position data.
float[][] matrices = new float[instCount][];
readProperties(i =>
for (int i = 0; i < instCount; i++)
{
byte b_OrientId = reader.ReadByte();
byte rawOrientId = reader.ReadByte();
if (b_OrientId > 0)
if (rawOrientId > 0)
{
// Make sure this value is in a safe range.
int orientId = (b_OrientId - 1) % 36;
int orientId = (rawOrientId - 1) % 36;
NormalId xColumn = (NormalId)(orientId / 6);
Vector3 R0 = Vector3.FromNormalId(xColumn);
@ -232,8 +267,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
// Compute R2 using the cross product of R0 and R1.
Vector3 R2 = R0.Cross(R1);
// Generate the rotation matrix and return it.
return new float[9]
// Generate the rotation matrix.
matrices[i] = new float[9]
{
R0.X, R0.Y, R0.Z,
R1.X, R1.Y, R1.Z,
@ -247,8 +282,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
Quaternion quaternion = new Quaternion(qx, qy, qz, qw);
var rotation = quaternion.ToCFrame();
return rotation.GetComponents();
matrices[i] = rotation.GetComponents();
}
else
{
@ -260,9 +294,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
matrix[m] = value;
}
return matrix;
matrices[i] = matrix;
}
});
}
float[] CFrame_X = readFloats(),
CFrame_Y = readFloats(),
@ -270,25 +304,54 @@ namespace RobloxFiles.BinaryFormat.Chunks
readProperties(i =>
{
float[] matrix = props[i].Value as float[];
float[] matrix = matrices[i];
float x = CFrame_X[i],
y = CFrame_Y[i],
z = CFrame_Z[i];
float[] position = new float[3] { x, y, z };
float[] components = position.Concat(matrix).ToArray();
float[] components;
if (matrix.Length == 12)
{
matrix[0] = x;
matrix[1] = y;
matrix[2] = z;
components = matrix;
}
else
{
float[] position = new float[3] { x, y, z };
components = position.Concat(matrix).ToArray();
}
return new CFrame(components);
});
break;
case PropertyType.Enum:
// 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.ReadUInts(instCount);
readProperties(i => enums[i]);
readProperties(i =>
{
Property prop = props[i];
Instance instance = prop.Instance;
Type instType = instance.GetType();
uint value = enums[i];
try
{
FieldInfo info = instType.GetField(Name, Property.BindingFlags);
return Enum.Parse(info.FieldType, value.ToInvariantString());
}
catch
{
Console.WriteLine($"Enum cast failed for {inst.ClassName}.{Name} using value {value}!");
return value;
}
});
break;
case PropertyType.Ref:
@ -345,9 +408,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
B = reader.ReadFloat();
Color3 Value = new Color3(R, G, B);
byte[] Reserved = reader.ReadBytes(4);
int Envelope = reader.ReadInt32();
keypoints[key] = new ColorSequenceKeypoint(Time, Value, Reserved);
keypoints[key] = new ColorSequenceKeypoint(Time, Value, Envelope);
}
return new ColorSequence(keypoints);
@ -415,7 +478,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
g = Color3uint8_G[i],
b = Color3uint8_B[i];
return Color3.FromRGB(r, g, b);
Color3uint8 result = Color3.FromRGB(r, g, b);
return result;
});
break;
@ -434,6 +498,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
readProperties(i =>
{
uint key = SharedKeys[i];
return file.SharedStrings[key];
});
@ -455,11 +520,9 @@ namespace RobloxFiles.BinaryFormat.Chunks
foreach (int instId in inst.InstanceIds)
{
Instance instance = file.Instances[instId];
var props = instance.RefreshProperties();
var props = instance.Properties;
var propNames = props.Keys;
foreach (string propName in propNames)
foreach (string propName in props.Keys)
{
if (!propMap.ContainsKey(propName))
{
@ -469,7 +532,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
{
Name = prop.Name,
Type = prop.Type,
TypeIndex = inst.TypeIndex
ClassIndex = inst.ClassIndex
};
propMap.Add(propName, propChunk);
@ -484,13 +548,13 @@ namespace RobloxFiles.BinaryFormat.Chunks
{
BinaryRobloxFile file = writer.File;
INST inst = file.Types[TypeIndex];
INST inst = file.Classes[ClassIndex];
var props = new List<Property>();
foreach (int instId in inst.InstanceIds)
{
Instance instance = file.Instances[instId];
Property prop = instance.GetProperty(Name);
Property prop = instance.Properties[Name];
if (prop == null)
throw new Exception($"Property {Name} must be defined in {instance.GetFullName()}!");
@ -502,7 +566,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
}
writer.StartWritingChunk(this);
writer.Write(TypeIndex);
writer.Write(ClassIndex);
writer.WriteString(Name);
writer.Write(TypeId);
@ -526,7 +590,12 @@ namespace RobloxFiles.BinaryFormat.Chunks
break;
case PropertyType.Bool:
props.ForEach(prop => prop.WriteValue<bool>());
props.ForEach(prop =>
{
bool value = prop.CastValue<bool>();
writer.Write(value);
});
break;
case PropertyType.Int:
var ints = new List<int>();
@ -737,7 +806,15 @@ namespace RobloxFiles.BinaryFormat.Chunks
props.ForEach(prop =>
{
uint value = prop.CastValue<uint>();
if (prop.Value is uint)
{
uint raw = prop.CastValue<uint>();
Enums.Add(raw);
return;
}
int signed = (int)prop.Value;
uint value = (uint)signed;
Enums.Add(value);
});
@ -805,7 +882,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
writer.Write(color.G);
writer.Write(color.B);
writer.Write(0);
writer.Write(keyPoint.Envelope);
}
});
@ -873,21 +950,20 @@ namespace RobloxFiles.BinaryFormat.Chunks
props.ForEach(prop =>
{
Color3 value = prop.CastValue<Color3>();
byte r = (byte)(value.R * 255);
Color3uint8_R.Add(r);
byte g = (byte)(value.G * 255);
Color3uint8_G.Add(g);
byte b = (byte)(value.B * 255);
Color3uint8_B.Add(b);
Color3uint8 value = prop.CastValue<Color3uint8>();
Color3uint8_R.Add(value.R);
Color3uint8_G.Add(value.G);
Color3uint8_B.Add(value.B);
});
writer.Write(Color3uint8_R.ToArray());
writer.Write(Color3uint8_G.ToArray());
writer.Write(Color3uint8_B.ToArray());
byte[] rBuffer = Color3uint8_R.ToArray();
writer.Write(rBuffer);
byte[] gBuffer = Color3uint8_G.ToArray();
writer.Write(gBuffer);
byte[] bBuffer = Color3uint8_B.ToArray();
writer.Write(bBuffer);
break;
case PropertyType.Int64:
@ -918,27 +994,18 @@ namespace RobloxFiles.BinaryFormat.Chunks
props.ForEach(prop =>
{
uint sharedKey = 0;
string value = prop.CastValue<string>();
byte[] buffer = Encoding.UTF8.GetBytes(value);
using (MD5 md5 = MD5.Create())
SharedString shared = prop.CastValue<SharedString>();
string key = shared.MD5_Key;
if (!sstr.Lookup.ContainsKey(key))
{
byte[] hash = md5.ComputeHash(buffer);
string key = Convert.ToBase64String(hash);
if (!sstr.Lookup.ContainsKey(key))
{
uint id = (uint)(sstr.NumHashes++);
sstr.Strings.Add(id, value);
sstr.Lookup.Add(key, id);
}
sharedKey = sstr.Lookup[key];
uint id = (uint)(sstr.NumHashes++);
sstr.Strings.Add(id, shared);
sstr.Lookup.Add(key, id);
}
sharedKeys.Add(sharedKey);
uint hashId = sstr.Lookup[key];
sharedKeys.Add(hashId);
});
writer.WriteInterleaved(sharedKeys);

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using RobloxFiles.DataTypes;
namespace RobloxFiles.BinaryFormat.Chunks
{
@ -9,7 +10,7 @@ namespace RobloxFiles.BinaryFormat.Chunks
public int NumHashes;
public Dictionary<string, uint> Lookup = new Dictionary<string, uint>();
public Dictionary<uint, string> Strings = new Dictionary<uint, string>();
public Dictionary<uint, SharedString> Strings = new Dictionary<uint, SharedString>();
public void LoadFromReader(BinaryRobloxFileReader reader)
{
@ -25,10 +26,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
int length = reader.ReadInt32();
byte[] data = reader.ReadBytes(length);
string key = Convert.ToBase64String(md5);
string value = Convert.ToBase64String(data);
Lookup.Add(key, id);
SharedString value = SharedString.FromBuffer(data);
Lookup.Add(value.MD5_Key, id);
Strings.Add(id, value);
}
@ -49,8 +48,8 @@ namespace RobloxFiles.BinaryFormat.Chunks
byte[] md5 = Convert.FromBase64String(key);
writer.Write(md5);
string value = Strings[pair.Value];
byte[] buffer = Convert.FromBase64String(value);
SharedString value = Strings[pair.Value];
byte[] buffer = SharedString.FindRecord(value.MD5_Key);
writer.Write(buffer.Length);
writer.Write(buffer);

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
@ -16,10 +18,15 @@ namespace RobloxFiles.BinaryFormat
public string ChunkType { get; private set; }
public long ChunkStart { get; private set; }
public Dictionary<string, INST> TypeMap;
public Dictionary<string, INST> ClassMap;
public readonly BinaryRobloxFile File;
// Instances in parent->child order
public List<Instance> Instances;
// Instances in child->parent order
public List<Instance> PostInstances;
public BinaryRobloxFileWriter(BinaryRobloxFile file, Stream workBuffer = null) : base(workBuffer ?? new MemoryStream())
{
File = file;
@ -28,7 +35,9 @@ namespace RobloxFiles.BinaryFormat
ChunkType = "";
Instances = new List<Instance>();
TypeMap = new Dictionary<string, INST>();
PostInstances = new List<Instance>();
ClassMap = new Dictionary<string, INST>();
}
private static byte[] GetBytes<T>(T value, int bufferSize, IntPtr converter)
@ -144,36 +153,64 @@ namespace RobloxFiles.BinaryFormat
{
foreach (Instance instance in instances)
{
int instId = (int)(File.NumInstances++);
if (!instance.Archivable)
continue;
int instId = (int)(File.NumInstances++);
instance.Referent = instId.ToString();
Instances.Add(instance);
string className = instance.ClassName;
INST inst = null;
if (!TypeMap.ContainsKey(className))
if (!ClassMap.ContainsKey(className))
{
inst = new INST()
{
TypeName = className,
ClassName = className,
InstanceIds = new List<int>(),
IsService = instance.IsService
};
TypeMap.Add(className, inst);
ClassMap.Add(className, inst);
}
else
{
inst = TypeMap[className];
inst = ClassMap[className];
}
inst.NumInstances++;
inst.InstanceIds.Add(instId);
RecordInstances(instance.GetChildren());
PostInstances.Add(instance);
}
}
internal void ApplyClassMap()
{
File.Instances = Instances.ToArray();
var classNames = ClassMap
.Select(type => type.Key)
.ToList();
classNames.Sort(StringComparer.Ordinal);
var classes = classNames
.Select(className => ClassMap[className])
.ToArray();
for (int i = 0; i < classes.Length; i++, File.NumClasses++)
{
string className = classNames[i];
INST inst = ClassMap[className];
inst.ClassIndex = i;
}
File.Classes = classes;
}
// Marks that we are writing a chunk.
public bool StartWritingChunk(string chunkType)
@ -236,5 +273,19 @@ namespace RobloxFiles.BinaryFormat
return chunk;
}
public void SaveChunk(IBinaryFileChunk handler)
{
var chunk = handler.SaveAsChunk(this);
File.Chunks.Add(chunk);
}
public BinaryRobloxFileChunk WriteEndChunk()
{
StartWritingChunk("END\0");
WriteString("</roblox>", true);
return FinishWritingChunk(false);
}
}
}

View File

@ -14,11 +14,23 @@ namespace RobloxFiles.DataTypes
public float Y => m24;
public float Z => m34;
public Vector3 Position => new Vector3(X, Y, Z);
public Vector3 Position
{
get
{
return new Vector3(X, Y, Z);
}
set
{
m14 = value.X;
m24 = value.Y;
m34 = value.Z;
}
}
public Vector3 LookVector => new Vector3(-m13, -m23, -m33);
public Vector3 RightVector => new Vector3( m11, m21, m31);
public Vector3 UpVector => new Vector3( m12, m22, m32);
public Vector3 RightVector => new Vector3( m11, m12, m13);
public Vector3 UpVector => new Vector3( m21, m22, m23);
public Vector3 LookVector => new Vector3(-m31, -m32, -m33);
public CFrame()
{
@ -41,7 +53,6 @@ namespace RobloxFiles.DataTypes
m34 = nz;
}
public CFrame(Vector3 eye, Vector3 look)
{
Vector3 zAxis = (eye - look).Unit;
@ -127,9 +138,9 @@ namespace RobloxFiles.DataTypes
{
float[] ac = a.GetComponents();
float x = ac[0], y = ac[1], z = ac[2],
m11 = ac[3], m12 = ac[4], m13 = ac[5],
m21 = ac[6], m22 = ac[7], m23 = ac[8],
float x = ac[0], y = ac[1], z = ac[2],
m11 = ac[3], m12 = ac[4], m13 = ac[5],
m21 = ac[6], m22 = ac[7], m23 = ac[8],
m31 = ac[9], m32 = ac[10], m33 = ac[11];
return new CFrame(x - b.X, y - b.Y, z - b.Z, m11, m12, m13, m21, m22, m23, m31, m32, m33);
@ -138,9 +149,10 @@ namespace RobloxFiles.DataTypes
public static Vector3 operator *(CFrame a, Vector3 b)
{
float[] ac = a.GetComponents();
float x = ac[0], y = ac[1], z = ac[2],
m11 = ac[3], m12 = ac[4], m13 = ac[5],
m21 = ac[6], m22 = ac[7], m23 = ac[8],
float x = ac[0], y = ac[1], z = ac[2],
m11 = ac[3], m12 = ac[4], m13 = ac[5],
m21 = ac[6], m22 = ac[7], m23 = ac[8],
m31 = ac[9], m32 = ac[10], m33 = ac[11];
Vector3 right = new Vector3(m11, m21, m31);
@ -154,14 +166,14 @@ namespace RobloxFiles.DataTypes
float[] ac = a.GetComponents();
float[] bc = b.GetComponents();
float a14 = ac[0], a24 = ac[1], a34 = ac[2],
a11 = ac[3], a12 = ac[4], a13 = ac[5],
a21 = ac[6], a22 = ac[7], a23 = ac[8],
float a14 = ac[0], a24 = ac[1], a34 = ac[2],
a11 = ac[3], a12 = ac[4], a13 = ac[5],
a21 = ac[6], a22 = ac[7], a23 = ac[8],
a31 = ac[9], a32 = ac[10], a33 = ac[11];
float b14 = bc[0], b24 = bc[1], b34 = bc[2],
b11 = bc[3], b12 = bc[4], b13 = bc[5],
b21 = bc[6], b22 = bc[7], b23 = bc[8],
float b14 = bc[0], b24 = bc[1], b34 = bc[2],
b11 = bc[3], b12 = bc[4], b13 = bc[5],
b21 = bc[6], b22 = bc[7], b23 = bc[8],
b31 = bc[9], b32 = bc[10], b33 = bc[11];
float n11 = a11 * b11 + a12 * b21 + a13 * b31 + a14 * m41;
@ -345,14 +357,14 @@ namespace RobloxFiles.DataTypes
for (int i = 3; i < 12; i++)
{
float t = matrix[i];
float t = Math.Abs(matrix[i]);
if (Math.Abs(t - 1f) < 10e-5f)
if (t.FuzzyEquals(1))
{
// Approximately ±1
sum1++;
}
else if (Math.Abs(t) < 10e-5f)
else if (t.FuzzyEquals(0))
{
// Approximately ±0
sum0++;
@ -364,10 +376,10 @@ namespace RobloxFiles.DataTypes
private static bool IsLegalOrientId(int orientId)
{
int xNormalAbs = (orientId / 6) % 3;
int yNormalAbs = orientId % 3;
int xOrientId = (orientId / 6) % 3;
int yOrientId = orientId % 3;
return (xNormalAbs != yNormalAbs);
return (xOrientId != yOrientId);
}
public int GetOrientId()

41
DataTypes/Color3uint8.cs Normal file
View File

@ -0,0 +1,41 @@
namespace RobloxFiles.DataTypes
{
/// <summary>
/// Color3uint8 functions as an interconvertible storage medium for Color3 types.
/// It is used by property types that want their Color3 value encoded with bytes instead of floats.
/// </summary>
public class Color3uint8
{
public readonly byte R, G, B;
public Color3uint8(byte r = 0, byte g = 0, byte b = 0)
{
R = r;
G = g;
B = b;
}
public override string ToString()
{
return string.Join(", ", R, G, B);
}
public static implicit operator Color3(Color3uint8 color)
{
float r = color.R / 255f;
float g = color.G / 255f;
float b = color.B / 255f;
return new Color3(r, g, b);
}
public static implicit operator Color3uint8(Color3 color)
{
byte r = (byte)(color.R * 255);
byte g = (byte)(color.G * 255);
byte b = (byte)(color.B * 255);
return new Color3uint8(r, g, b);
}
}
}

View File

@ -6,12 +6,8 @@ namespace RobloxFiles.DataTypes
{
public readonly ColorSequenceKeypoint[] Keypoints;
public ColorSequence(Color3 c)
public ColorSequence(Color3 c) : this(c, c)
{
ColorSequenceKeypoint a = new ColorSequenceKeypoint(0, c);
ColorSequenceKeypoint b = new ColorSequenceKeypoint(1, c);
Keypoints = new ColorSequenceKeypoint[2] { a, b };
}
public ColorSequence(Color3 c0, Color3 c1)
@ -35,10 +31,13 @@ namespace RobloxFiles.DataTypes
if (keypoints[key - 1].Time > keypoints[key].Time)
throw new Exception("ColorSequence: all keypoints must be ordered by time");
if (Math.Abs(keypoints[0].Time) >= 10e-5f)
var first = keypoints[0];
var last = keypoints[numKeys - 1];
if (!first.Time.FuzzyEquals(0))
throw new Exception("ColorSequence must start at time=0.0");
if (Math.Abs(keypoints[numKeys - 1].Time - 1f) >= 10e-5f)
if (!last.Time.FuzzyEquals(1))
throw new Exception("ColorSequence must end at time=1.0");
Keypoints = keypoints;

View File

@ -4,18 +4,18 @@
{
public readonly float Time;
public readonly Color3 Value;
public readonly byte[] Reserved;
public readonly int Envelope;
public ColorSequenceKeypoint(float time, Color3 value, byte[] reserved = null)
public ColorSequenceKeypoint(float time, Color3 value, int envelope = 0)
{
Time = time;
Value = value;
Reserved = reserved;
Envelope = envelope;
}
public override string ToString()
{
return string.Join(" ", Time, Value.R, Value.G, Value.B, 0);
return string.Join(" ", Time, Value.R, Value.G, Value.B, Envelope);
}
}
}

32
DataTypes/Content.cs Normal file
View File

@ -0,0 +1,32 @@
namespace RobloxFiles.DataTypes
{
/// <summary>
/// Content is a type used by most url-based XML properties.
/// Here, it only exists as a wrapper class for strings.
/// </summary>
public class Content
{
// TODO: Maybe introduce constraints to the value?
public readonly string Data;
public override string ToString()
{
return Data;
}
public Content(string data)
{
Data = data;
}
public static implicit operator string(Content content)
{
return content.Data;
}
public static implicit operator Content(string data)
{
return new Content(data);
}
}
}

View File

@ -7,6 +7,12 @@ namespace RobloxFiles.DataTypes
public readonly float Min;
public readonly float Max;
public NumberRange(float num)
{
Min = num;
Max = num;
}
public NumberRange(float min = 0, float max = 0)
{
if (max - min < 0)

View File

@ -35,10 +35,13 @@ namespace RobloxFiles.DataTypes
if (keypoints[key - 1].Time > keypoints[key].Time)
throw new Exception("NumberSequence: all keypoints must be ordered by time");
if (Math.Abs(keypoints[0].Time) >= 10e-5f)
var first = keypoints[0];
var last = keypoints[numKeys - 1];
if (!first.Time.FuzzyEquals(0))
throw new Exception("NumberSequence must start at time=0.0");
if (Math.Abs(keypoints[numKeys - 1].Time - 1f) >= 10e-5f)
if (!last.Time.FuzzyEquals(1))
throw new Exception("NumberSequence must end at time=1.0");
Keypoints = keypoints;

View File

@ -0,0 +1,31 @@
namespace RobloxFiles.DataTypes
{
/// <summary>
/// ProtectedString is a type used by some of the XML properties.
/// Here, it only exists as a wrapper class for strings.
/// </summary>
public class ProtectedString
{
public readonly string Value;
public override string ToString()
{
return Value;
}
public ProtectedString(string value)
{
Value = value;
}
public static implicit operator string(ProtectedString protectedString)
{
return protectedString.Value;
}
public static implicit operator ProtectedString(string value)
{
return new ProtectedString(value);
}
}
}

208
DataTypes/Quaternion.cs Normal file
View File

@ -0,0 +1,208 @@
using System;
using RobloxFiles.DataTypes;
namespace RobloxFiles.Utility
{
/// <summary>
/// Quaternion is a utility used by the CFrame DataType to handle rotation interpolation.
/// It can be used as an independent Quaternion implementation if you so please!
/// </summary>
public class Quaternion
{
public readonly float X, Y, Z, W;
public float Magnitude
{
get
{
float squared = Dot(this);
double magnitude = Math.Sqrt(squared);
return (float)magnitude;
}
}
public Quaternion(float x, float y, float z, float w)
{
X = x;
Y = y;
Z = z;
W = w;
}
public Quaternion(Vector3 qv, float qw)
{
X = qv.X;
Y = qv.Y;
Z = qv.Z;
W = qw;
}
public Quaternion(CFrame cf)
{
CFrame matrix = (cf - cf.Position);
float[] ac = cf.GetComponents();
float m11 = ac[3], m12 = ac[4], m13 = ac[5],
m21 = ac[6], m22 = ac[7], m23 = ac[8],
m31 = ac[9], m32 = ac[10], m33 = ac[11];
float trace = m11 + m22 + m33;
if (trace > 0)
{
float s = (float)Math.Sqrt(1 + trace);
float r = 0.5f / s;
W = s * 0.5f;
X = (m32 - m23) * r;
Y = (m13 - m31) * r;
Z = (m21 - m12) * r;
}
else
{
float big = Math.Max(Math.Max(m11, m22), m33);
if (big == m11)
{
float s = (float)Math.Sqrt(1 + m11 - m22 - m33);
float r = 0.5f / s;
W = (m32 - m23) * r;
X = 0.5f * s;
Y = (m21 + m12) * r;
Z = (m13 + m31) * r;
}
else if (big == m22)
{
float s = (float)Math.Sqrt(1 - m11 + m22 - m33);
float r = 0.5f / s;
W = (m13 - m31) * r;
X = (m21 + m12) * r;
Y = 0.5f * s;
Z = (m32 + m23) * r;
}
else if (big == m33)
{
float s = (float)Math.Sqrt(1 - m11 - m22 + m33);
float r = 0.5f / s;
W = (m21 - m12) * r;
X = (m13 + m31) * r;
Y = (m32 + m23) * r;
Z = 0.5f * s;
}
}
}
public float Dot(Quaternion other)
{
return (X * other.X) + (Y * other.Y) + (Z * other.Z) + (W * other.W);
}
public Quaternion Lerp(Quaternion other, float alpha)
{
Quaternion result = this * (1.0f - alpha) + other * alpha;
return result / result.Magnitude;
}
public Quaternion Slerp(Quaternion other, float alpha)
{
float cosAng = Dot(other);
if (cosAng < 0)
{
other = -other;
cosAng = -cosAng;
}
double ang = Math.Acos(cosAng);
if (ang >= 0.05f)
{
float scale0 = (float)Math.Sin((1.0f - alpha) * ang);
float scale1 = (float)Math.Sin(alpha * ang);
float denom = (float)Math.Sin(ang);
return ((this * scale0) + (other * scale1)) / denom;
}
else
{
return Lerp(other, alpha);
}
}
public CFrame ToCFrame()
{
float xc = X * 2f;
float yc = Y * 2f;
float zc = Z * 2f;
float xx = X * xc;
float xy = X * yc;
float xz = X * zc;
float wx = W * xc;
float wy = W * yc;
float wz = W * zc;
float yy = Y * yc;
float yz = Y * zc;
float zz = Z * zc;
return new CFrame
(
0, 0, 0,
1f - (yy + zz),
xy - wz,
xz + wy,
xy + wz,
1f - (xx + zz),
yz - wx,
xz - wy,
yz + wx,
1f - (xx + yy)
);
}
public static Quaternion operator +(Quaternion a, Quaternion b)
{
return new Quaternion(a.X + b.X, a.Y + b.Y, a.Z + b.Z, a.W + b.W);
}
public static Quaternion operator -(Quaternion a, Quaternion b)
{
return new Quaternion(a.X - b.X, a.Y - b.Y, a.Z - b.Z, a.W - b.W);
}
public static Quaternion operator *(Quaternion a, float f)
{
return new Quaternion(a.X * f, a.Y * f, a.Z * f, a.W * f);
}
public static Quaternion operator /(Quaternion a, float f)
{
return new Quaternion(a.X / f, a.Y / f, a.Z / f, a.W / f);
}
public static Quaternion operator -(Quaternion a)
{
return new Quaternion(-a.X, -a.Y, -a.Z, -a.W);
}
public static Quaternion operator *(Quaternion a, Quaternion b)
{
Vector3 v1 = new Vector3(a.X, a.Y, a.Z);
float s1 = a.W;
Vector3 v2 = new Vector3(b.X, b.Y, b.Z);
float s2 = b.W;
return new Quaternion(s1 * v2 + s2 * v1 + v1.Cross(v2), s1 * s2 - v1.Dot(v2));
}
}
}

View File

@ -20,10 +20,10 @@
}
}
public Ray(Vector3 origin, Vector3 direction)
public Ray(Vector3 origin = null, Vector3 direction = null)
{
Origin = origin;
Direction = direction;
Origin = origin ?? new Vector3();
Direction = direction ?? new Vector3();
}
public override string ToString()

62
DataTypes/SharedString.cs Normal file
View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
namespace RobloxFiles.DataTypes
{
public class SharedString
{
private static Dictionary<string, byte[]> Records = new Dictionary<string, byte[]>();
public readonly string MD5_Key;
public byte[] SharedValue => FindRecord(MD5_Key);
public override string ToString() => $"MD5 Key: {MD5_Key}";
internal SharedString(string md5)
{
MD5_Key = md5;
}
private SharedString(byte[] buffer)
{
using (MD5 md5 = MD5.Create())
{
byte[] hash = md5.ComputeHash(buffer);
MD5_Key = Convert.ToBase64String(hash);
}
if (Records.ContainsKey(MD5_Key))
return;
Records.Add(MD5_Key, buffer);
}
public static byte[] FindRecord(string key)
{
byte[] result = null;
if (Records.ContainsKey(key))
result = Records[key];
return result;
}
public static SharedString FromBuffer(byte[] buffer)
{
return new SharedString(buffer);
}
public static SharedString FromString(string value)
{
byte[] buffer = Encoding.UTF8.GetBytes(value);
return new SharedString(buffer);
}
public static SharedString FromBase64(string base64)
{
byte[] buffer = Convert.FromBase64String(base64);
return new SharedString(buffer);
}
}
}

View File

@ -28,7 +28,7 @@ namespace RobloxFiles.DataTypes
Y = y;
}
public Vector2(float[] coords)
internal Vector2(float[] coords)
{
X = coords.Length > 0 ? coords[0] : 0;
Y = coords.Length > 1 ? coords[1] : 0;
@ -69,6 +69,11 @@ namespace RobloxFiles.DataTypes
public static Vector2 operator /(Vector2 v, float n) => upcastFloatOp(v, n, div);
public static Vector2 operator /(float n, Vector2 v) => upcastFloatOp(n, v, div);
public static Vector2 operator -(Vector2 v)
{
return new Vector2(-v.X, -v.Y);
}
public static Vector2 Zero => new Vector2(0, 0);
public override string ToString()

View File

@ -30,7 +30,7 @@ namespace RobloxFiles.DataTypes
Z = z;
}
public Vector3(float[] coords)
internal Vector3(float[] coords)
{
X = coords.Length > 0 ? coords[0] : 0;
Y = coords.Length > 1 ? coords[1] : 0;
@ -92,6 +92,11 @@ namespace RobloxFiles.DataTypes
public static Vector3 operator /(Vector3 v, float n) => upcastFloatOp(v, n, div);
public static Vector3 operator /(float n, Vector3 v) => upcastFloatOp(n, v, div);
public static Vector3 operator -(Vector3 v)
{
return new Vector3(-v.X, -v.Y, -v.Z);
}
public static Vector3 Zero => new Vector3(0, 0, 0);
public static Vector3 Right => new Vector3(1, 0, 0);
public static Vector3 Up => new Vector3(0, 1, 0);
@ -141,7 +146,7 @@ namespace RobloxFiles.DataTypes
float dotProd = normal.Dot(this);
if (Math.Abs(dotProd - 1f) < 10e-5f)
if (dotProd.FuzzyEquals(1))
{
result = i;
break;

3130
Generated/Classes.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
// This is an auto-generated list of all available enums on Roblox!
// Updated as of 0.370.0.274702
// Auto-generated list of Roblox enums.
// Updated as of 0.392.0.316618
namespace RobloxFiles.Enums
{
@ -26,6 +26,12 @@ namespace RobloxFiles.Enums
Servo
}
public enum AlignType
{
Parallel,
Perpendicular
}
public enum AnimationPriority
{
Idle,
@ -55,9 +61,15 @@ namespace RobloxFiles.Enums
ScaleWithParentSize
}
public enum AssetFetchStatus
{
Success,
Failure
}
public enum AssetType
{
Image,
Image = 1,
TeeShirt,
Audio,
Mesh,
@ -99,7 +111,8 @@ namespace RobloxFiles.Enums
WalkAnimation,
PoseAnimation,
EarAccessory,
EyeAccessory
EyeAccessory,
EmoteAnimation = 61
}
public enum AutoJointsMode
@ -113,7 +126,8 @@ namespace RobloxFiles.Enums
{
Friend,
Chat,
Emote
Emote,
InspectMenu
}
public enum AvatarJointPositionType
@ -169,6 +183,14 @@ namespace RobloxFiles.Enums
Unknown = 17
}
public enum BreakReason
{
Other,
Error,
SpecialBreakpoint,
UserBreakpoint
}
public enum Button
{
Dismount = 8,
@ -250,7 +272,7 @@ namespace RobloxFiles.Enums
public enum CenterDialogType
{
UnsolicitedDialog,
UnsolicitedDialog = 1,
PlayerInitiatedDialog,
ModalDialog,
QuitDialog
@ -258,7 +280,7 @@ namespace RobloxFiles.Enums
public enum ChatCallbackType
{
OnCreatingChatWindow,
OnCreatingChatWindow = 1,
OnClientSendingMessage,
OnClientFormattingMessage,
OnServerReceivingMessage = 17
@ -342,6 +364,8 @@ namespace RobloxFiles.Enums
DisconnectIdle,
DisconnectRaknetErrors,
DisconnectWrongVersion,
DisconnectBySecurityPolicy,
DisconnectBlockedIP,
PlacelaunchErrors = 512,
PlacelaunchDisabled = 515,
PlacelaunchError,
@ -376,10 +400,9 @@ namespace RobloxFiles.Enums
public enum ContextActionPriority
{
Low = 1000,
Default = 2000,
Medium = 2000,
High = 3000,
Default = Medium
High = 3000
}
public enum ContextActionResult
@ -400,7 +423,8 @@ namespace RobloxFiles.Enums
Health,
Backpack,
Chat,
All
All,
EmotesMenu
}
public enum CreatorType
@ -423,12 +447,6 @@ namespace RobloxFiles.Enums
Follow
}
public enum DEPRECATED_DebuggerDataModelPreference
{
Server,
Client
}
public enum DataStoreRequestType
{
GetAsync,
@ -506,6 +524,14 @@ namespace RobloxFiles.Enums
Navigation
}
public enum DeviceType
{
Unknown,
Desktop,
Tablet,
Phone
}
public enum DialogBehaviorType
{
SinglePlayer,
@ -735,7 +761,7 @@ namespace RobloxFiles.Enums
public enum GraphicsMode
{
Automatic,
Automatic = 1,
Direct3D11,
Direct3D9,
OpenGL,
@ -811,6 +837,12 @@ namespace RobloxFiles.Enums
Localization = 24
}
public enum HumanoidCollisionType
{
OuterBox,
InnerBox
}
public enum HumanoidDisplayDistanceType
{
Viewer,
@ -875,6 +907,13 @@ namespace RobloxFiles.Enums
Float
}
public enum InlineAlignment
{
Bottom,
Center,
Top
}
public enum InputType
{
NoInput,
@ -891,7 +930,7 @@ namespace RobloxFiles.Enums
public enum JointType
{
Weld,
Weld = 1,
Snap = 3,
Rotate = 7,
RotateP,
@ -1169,6 +1208,13 @@ namespace RobloxFiles.Enums
Default
}
public enum LanguagePreference
{
SystemDefault,
English,
SimplifiedChinese
}
public enum LeftRight
{
Left,
@ -1244,6 +1290,7 @@ namespace RobloxFiles.Enums
Ice = 1536,
Glacier = 1552,
Glass = 1568,
ForceField = 1584,
Air = 1792,
Water = 2048
}
@ -1253,7 +1300,8 @@ namespace RobloxFiles.Enums
None,
BuildersClub,
TurboBuildersClub,
OutrageousBuildersClub
OutrageousBuildersClub,
Premium
}
public enum MeshType
@ -1619,7 +1667,7 @@ namespace RobloxFiles.Enums
public enum ScrollingDirection
{
X,
X = 1,
Y,
XY = 4
}
@ -1688,6 +1736,13 @@ namespace RobloxFiles.Enums
Confusion
}
public enum StreamingPauseMode
{
Default,
Disabled,
ClientPhysicsPause
}
public enum StudioStyleGuideColor
{
MainBackground,
@ -1773,7 +1828,15 @@ namespace RobloxFiles.Enums
CheckedFieldIndicator,
HeaderSection,
Midlight,
StatusBar
StatusBar,
DialogButton,
DialogButtonText,
DialogButtonBorder,
DialogMainButton,
DialogMainButtonText,
Merge3HighlightOriginal,
Merge3HighlightMine,
Merge3HighlightTheirs
}
public enum StudioStyleGuideModifier
@ -1800,6 +1863,12 @@ namespace RobloxFiles.Enums
Motor
}
public enum SurfaceGuiSizingMode
{
FixedSize,
PixelsPerStud
}
public enum SurfaceType
{
Smooth,
@ -1832,7 +1901,9 @@ namespace RobloxFiles.Enums
public enum Technology
{
Legacy,
Voxel
Voxel,
Compatibility,
ShadowMap
}
public enum TeleportResult
@ -1865,7 +1936,7 @@ namespace RobloxFiles.Enums
public enum TextFilterContext
{
PublicChat,
PublicChat = 1,
PrivateChat
}
@ -2038,6 +2109,7 @@ namespace RobloxFiles.Enums
Gamepad7,
Gamepad8,
TextInput,
InputMethod,
None
}
@ -2115,4 +2187,4 @@ namespace RobloxFiles.Enums
Global,
Sibling
}
}
}

Binary file not shown.

View File

@ -0,0 +1,607 @@
local HttpService = game:GetService("HttpService")
local ServerStorage = game:GetService("ServerStorage")
local StarterPlayer = game:GetService("StarterPlayer")
local StudioService = game:GetService("StudioService")
local classes = {}
local outStream = ""
local stackLevel = 0
local singletons =
{
Terrain = workspace:WaitForChild("Terrain");
StarterPlayerScripts = StarterPlayer:WaitForChild("StarterPlayerScripts");
StarterCharacterScripts = StarterPlayer:WaitForChild("StarterCharacterScripts");
}
local isCoreScript = pcall(function ()
local restricted = game:GetService("RobloxPluginGuiService")
return tostring(restricted)
end)
local function write(formatString, ...)
local tabs = string.rep(' ', stackLevel * 4)
local fmt = formatString or ""
local value = tabs .. fmt:format(...)
outStream = outStream .. value
end
local function writeLine(formatString, ...)
if not formatString then
outStream = outStream .. '\n'
return
end
write(formatString .. '\n', ...)
end
local function openStack()
writeLine('{')
stackLevel = stackLevel + 1
end
local function closeStack()
stackLevel = stackLevel - 1
writeLine('}')
end
local function clearStream()
stackLevel = 0
outStream = ""
end
local function exportStream(label)
local results = outStream:gsub("\n\n\n", "\n\n")
if plugin then
local export = Instance.new("Script")
export.Archivable = false
export.Source = results
export.Name = label
plugin:OpenScript(export)
end
if isCoreScript then
StudioService:CopyToClipboard(results)
elseif not plugin then
warn(label)
print(results)
end
end
local function getTags(object)
local tags = {}
if object.Tags ~= nil then
for _,tag in pairs(object.Tags) do
tags[tag] = true
end
end
if object.Name == "Terrain" then
tags.NotCreatable = nil
end
return tags
end
local function upcastInheritance(class, root)
local superClass = classes[class.Superclass]
if not superClass then
return
end
if not root then
root = class
end
if not superClass.Inherited then
superClass.Inherited = root
end
upcastInheritance(superClass, root)
end
local function canCreateClass(class)
local tags = getTags(class)
local canCreate = true
if tags.NotCreatable then
canCreate = false
end
if tags.Service then
canCreate = true
end
if tags.Settings then
canCreate = false
end
if singletons[class.Name] then
canCreate = true
end
return canCreate
end
local function collectProperties(class)
local propMap = {}
for _,member in ipairs(class.Members) do
if member.MemberType == "Property" then
local propName = member.Name
propMap[propName] = member
end
end
return propMap
end
local function createProperty(propName, propType)
local category = "DataType";
local name = propType
if propType:find(':') then
local data = string.split(propType, ':')
category = data[1]
name = data[2]
end
return
{
Name = propName;
Serialization =
{
CanSave = true;
CanLoad = true;
};
ValueType =
{
Category = category;
Name = name;
};
Security = "None";
}
end
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Formatting
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
local formatting = require(script.Parent.Formatting)
local formatLinks =
{
["int"] = "Int";
["nil"] = "Null";
["long"] = "Int";
["float"] = "Float";
["byte[]"] = "Bytes";
["double"] = "Double";
["string"] = "String";
["Content"] = "String";
["Instance"] = "Null";
["Color3uint8"] = "Color3";
["ProtectedString"] = "String";
}
local function getFormatFunction(valueType)
if not formatting[valueType] then
valueType = formatLinks[valueType]
end
return formatting[valueType]
end
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Property Patches
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
local patches = require(script.Parent.PropertyPatches)
local patchIndex = {}
function patchIndex:__index(key)
if not rawget(self, key) then
rawset(self, key, {})
end
return self[key]
end
local function getPatches(className)
local classPatches = patches[className]
return setmetatable(classPatches, patchIndex)
end
setmetatable(patches, patchIndex)
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Main
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
local baseUrl = "https://raw.githubusercontent.com/CloneTrooper1019/Roblox-Client-Tracker/roblox/"
local toolbar, classButton, enumButton
if plugin then
toolbar = plugin:CreateToolbar("C# API Dump")
classButton = toolbar:CreateButton(
"Dump Classes",
"Generates a C# dump of Roblox's Class API.",
"rbxasset://textures/Icon_Stream_Off@2x.png"
)
enumButton = toolbar:CreateButton(
"Dump Enums",
"Generates a C# dump of Roblox's Enum API.",
"rbxasset://textures/Icon_Stream_Off@2x.png"
)
end
local function getAsync(url)
local enabled
if isCoreScript then
enabled = HttpService:GetHttpEnabled()
HttpService:SetHttpEnabled(true)
end
local result = HttpService:GetAsync(url)
if isCoreScript then
HttpService:SetHttpEnabled(enabled)
end
return result
end
local function generateClasses()
local version = getAsync(baseUrl .. "version.txt")
local apiDump = getAsync(baseUrl .. "API-Dump.json")
apiDump = HttpService:JSONDecode(apiDump)
local classNames = {}
classes = {}
for _,class in ipairs(apiDump.Classes) do
local className = class.Name
local superClass = classes[class.Superclass]
if singletons[className] then
class.Singleton = true
class.Object = singletons[className]
end
if superClass and canCreateClass(class) then
local classTags = getTags(class)
if classTags.Service then
pcall(function ()
if not className:find("Network") then
class.Object = game:GetService(className)
end
end)
elseif not classTags.NotCreatable then
pcall(function ()
class.Object = Instance.new(className)
if ServerStorage:FindFirstChild("DumpFolder") then
class.Object.Name = className
class.Object.Parent = ServerStorage.DumpFolder
end
end)
end
upcastInheritance(class)
end
classes[className] = class
table.insert(classNames, className)
end
outStream = ""
writeLine("// Auto-generated list of creatable Roblox classes.")
writeLine("// Updated as of %s", version)
writeLine()
writeLine("using System;")
writeLine()
writeLine("using RobloxFiles.DataTypes;")
writeLine("using RobloxFiles.Enums;")
writeLine("using RobloxFiles.Utility;")
writeLine()
writeLine("namespace RobloxFiles")
openStack()
for i,className in ipairs(classNames) do
local class = classes[className]
local classTags = getTags(class)
local registerClass = canCreateClass(class)
if class.Inherited then
registerClass = true
end
if class.Name == "Instance" or class.Name == "Studio" then
registerClass = false
end
local object = class.Object
if not object then
if class.Inherited then
object = class.Inherited.Object
elseif singletons[className] then
object = singletons[className]
else
registerClass = false
end
end
if registerClass then
local objectType
if classTags.NotCreatable and class.Inherited and not class.Singleton then
objectType = "abstract class"
else
objectType = "class"
end
writeLine("public %s %s : %s", objectType, className, class.Superclass)
openStack()
local classPatches = getPatches(className)
local redirectProps = classPatches.Redirect
local propMap = collectProperties(class)
local propNames = {}
for _,propName in pairs(classPatches.Remove) do
propMap[propName] = nil
end
for propName in pairs(propMap) do
table.insert(propNames, propName)
end
for propName, propType in pairs(classPatches.Add) do
if not propMap[propName] then
propMap[propName] = createProperty(propName, propType)
table.insert(propNames, propName)
else
propMap[propName].Serialization.CanLoad = true
end
end
local firstLine = true
table.sort(propNames)
if classTags.Service then
writeLine("public %s()", className)
openStack()
writeLine("IsService = true;")
closeStack()
if #propNames > 0 then
writeLine()
end
end
for i, propName in ipairs(propNames) do
local prop = propMap[propName]
local serial = prop.Serialization
local valueType = prop.ValueType.Name
if serial.CanLoad then
local propTags = getTags(prop)
local redirect = redirectProps[propName]
local name = propName
local default = ""
if propName == className then
name = name .. '_'
end
if valueType == "int64" then
valueType = "long"
elseif valueType == "BinaryString" then
valueType = "byte[]"
end
local first = name:sub(1, 1)
if first == first:lower() then
local pascal = first:upper() .. name:sub(2)
if propMap[pascal] ~= nil and propTags.Deprecated then
redirect = pascal
end
end
if redirect then
local get, set
if typeof(redirect) == "string" then
get = redirect
set = redirect .. " = value"
else
get = redirect.Get
set = redirect.Set
end
if not firstLine then
writeLine()
end
if propTags.Deprecated then
writeLine("[Obsolete]")
end
writeLine("public %s %s", valueType, name)
openStack()
writeLine("get { return %s; }", get)
writeLine("set { %s; }", set)
closeStack()
if (i ~= #propNames) then
writeLine()
end
else
local value = classPatches.Defaults[propName]
local gotValue = (value ~= nil)
if not gotValue then
gotValue, value = pcall(function ()
return object[propName]
end)
end
local comment = " // Default missing!"
local category = prop.ValueType.Category
if gotValue then
local category = prop.ValueType.Category
local formatFunc = getFormatFunction(valueType)
if not formatFunc then
local literal = typeof(value)
formatFunc = getFormatFunction(literal)
end
if not formatFunc then
formatFunc = tostring
end
local result
if typeof(formatFunc) == "string" then
result = formatFunc
else
result = formatFunc(value)
end
if not serial.CanSave and not propTags.Deprecated then
comment = " // [Load-only]"
else
comment = ""
end
default = " = " .. result
end
if propTags.Deprecated then
if not firstLine then
writeLine()
end
writeLine("[Obsolete]")
end
if category == "Class" then
default = " = null"
comment = ""
end
writeLine("public %s %s%s;%s", valueType, name, default, comment)
if propTags.Deprecated and i ~= #propNames then
writeLine()
end
end
firstLine = false
end
end
closeStack()
if (i ~= #classNames) then
writeLine()
end
end
end
closeStack()
exportStream("Classes")
end
local function generateEnums()
local version = getfenv().version():gsub("%. ", ".")
clearStream()
writeLine("// Auto-generated list of Roblox enums.")
writeLine("// Updated as of %s", version)
writeLine()
writeLine("namespace RobloxFiles.Enums")
openStack()
local enums = Enum:GetEnums()
for i, enum in ipairs(enums) do
writeLine("public enum %s", tostring(enum))
openStack()
local enumItems = enum:GetEnumItems()
local lastValue = -1
table.sort(enumItems, function (a, b)
return a.Value < b.Value
end)
for i, enumItem in ipairs(enumItems) do
local text = ""
local comma = ','
local name = enumItem.Name
local value = enumItem.Value
if (value - lastValue) ~= 1 then
text = " = " .. value;
end
if i == #enumItems then
comma = ""
end
lastValue = value
writeLine("%s%s%s", name, text, comma)
end
closeStack()
if i ~= #enums then
writeLine()
end
end
closeStack()
exportStream("Enums")
end
if plugin then
classButton.Click:Connect(generateClasses)
enumButton.Click:Connect(generateEnums)
else
generateClasses()
generateEnums()
end

View File

@ -0,0 +1,273 @@
local Format = {}
function Format.Null(value)
return "null"
end
function Format.Bytes(value)
if #value > 0 then
local fmt = "Convert.FromBase64String(%q)"
return fmt:format(value)
else
return "new byte[0]"
end
end
function Format.String(value)
return string.format("%q", value)
end
function Format.Int(value)
return string.format("%i", value)
end
function Format.Number(value)
local int = math.floor(value)
if math.abs(value - int) < 0.001 then
return Format.Int(int)
end
local result = string.format("%.5f", value)
result = result:gsub("%.?0+$", "")
return result
end
function Format.Double(value)
local result = Format.Number(value)
if result == "inf" then
return "double.MaxValue"
elseif result == "-inf" then
return "double.MinValue"
else
return result
end
end
function Format.Float(value)
local result = Format.Number(value)
if result == "inf" then
return "float.MaxValue"
elseif result == "-inf" then
return "float.MinValue"
else
if result:find("%.") then
result = result .. 'f'
end
return result
end
end
function Format.Flags(flag, enum)
local value = 0
for _,item in pairs(enum:GetEnumItems()) do
if flag[item.Name] then
value = value + (2 ^ item.Value)
end
end
return value
end
function Format.Axes(axes)
return "(Axes)" .. Format.Flags(axes, Enum.Axis)
end
function Format.Faces(faces)
return "(Faces)" .. Format.Flags(faces, Enum.NormalId)
end
function Format.EnumItem(item)
local enum = tostring(item.EnumType)
return enum .. '.' .. item.Name
end
function Format.BrickColor(brickColor)
local fmt = "BrickColor.FromNumber(%i)"
return fmt:format(brickColor.Number)
end
function Format.Color3(color)
if color == Color3.new() then
return "new Color3()"
end
local r = Format.Float(color.r)
local g = Format.Float(color.g)
local b = Format.Float(color.b)
local fmt = "%s(%s, %s, %s)";
local constructor = "new Color3";
if string.find(r .. g .. b, 'f') then
r = Format.Int(color.r * 255)
g = Format.Int(color.g * 255)
b = Format.Int(color.b * 255)
constructor = "Color3.FromRGB"
end
return fmt:format(constructor, r, g, b)
end
function Format.UDim(udim)
if udim == UDim.new() then
return "new UDim()"
end
local scale = Format.Float(udim.Scale)
local offset = Format.Int(udim.Offset)
local fmt = "new UDim(%s, %s)"
return fmt:format(scale, offset)
end
function Format.UDim2(udim2)
if udim2 == UDim2.new() then
return "new UDim2()"
end
local xScale = Format.Float(udim2.X.Scale)
local yScale = Format.Float(udim2.Y.Scale)
local xOffset = Format.Int(udim2.X.Offset)
local yOffset = Format.Int(udim2.Y.Offset)
local fmt = "new UDim2(%s, %s, %s, %s)"
return fmt:format(xScale, xOffset, yScale, yOffset)
end
function Format.Vector2(v2)
if v2.Magnitude < 0.001 then
return "new Vector2()"
end
local x = Format.Float(v2.X)
local y = Format.Float(v2.Y)
local fmt = "new Vector2(%s, %s)"
return fmt:format(x, y)
end
function Format.Vector3(v3)
if v3.Magnitude < 0.001 then
return "new Vector3()"
end
local x = Format.Float(v3.X)
local y = Format.Float(v3.Y)
local z = Format.Float(v3.Z)
local fmt = "new Vector3(%s, %s, %s)"
return fmt:format(x, y, z)
end
function Format.CFrame(cf)
local blankCF = CFrame.new()
if cf == blankCF then
return "new CFrame()"
end
local rot = cf - cf.p
if rot == blankCF then
local fmt = "new CFrame(%s, %s, %s)"
local x = Format.Float(cf.X)
local y = Format.Float(cf.Y)
local z = Format.Float(cf.Z)
return fmt:format(x, y, z)
else
local comp = { cf:GetComponents() }
for i = 1,12 do
comp[i] = Format.Float(comp[i])
end
local fmt = "new CFrame(%s)"
local matrix = table.concat(comp, ", ")
return fmt:format(matrix)
end
end
function Format.NumberRange(nr)
local min = nr.Min
local max = nr.Max
local fmt = "new NumberRange(%s)"
local value = Format.Float(min)
if min ~= max then
value = value .. ", " .. Format.Float(max)
end
return fmt:format(value)
end
function Format.Ray(ray)
if ray == Ray.new() then
return "new Ray()"
end
local fmt = "new Ray(%s, %s)"
local origin = Format.Vector3(ray.Origin)
local direction = Format.Vector3(ray.Direction)
return fmt:format(origin, direction)
end
function Format.Rect(rect)
local fmt = "new Rect(%s, %s)"
local min = Format.Vector2(rect.Min)
local max = Format.Vector2(rect.Max)
return fmt:format(min, max)
end
function Format.ColorSequence(cs)
local csKey = cs.Keypoints[1]
local fmt = "new ColorSequence(%s)"
local value = Format.Color3(csKey.Value)
return fmt:format(value)
end
function Format.NumberSequence(ns)
local nsKey = ns.Keypoints[1]
local fmt = "new NumberSequence(%s)"
local value = Format.Float(nsKey.Value)
return fmt:format(value)
end
function Format.Vector3int16(v3)
if v3 == Vector3int16.new() then
return "new Vector3int16()"
end
local x = Format.Int(v3.X)
local y = Format.Int(v3.Y)
local z = Format.Int(v3.Z)
local fmt = "new Vector3int16(%s, %s, %s)"
return fmt:format(x, y, z)
end
function Format.SharedString(str)
local fmt = "SharedString.FromBase64(%q)"
return fmt:format(str)
end
return Format

View File

@ -0,0 +1,674 @@
local function UseColor3(propName)
return
{
Get = "BrickColor.FromColor3(" .. propName .. ')';
Set = propName .. " = value.Color";
}
end
local GuiTextMixIn =
{
Redirect =
{
FontSize =
{
Get = "FontUtility.GetFontSize(TextSize)";
Set = "TextSize = FontUtility.GetFontSize(value)";
};
TextColor = UseColor3("TextColor3");
TextWrap = "TextWrapped";
};
}
return
{
Accoutrement =
{
Remove =
{
"AttachmentUp";
"AttachmentPos";
"AttachmentRight";
"AttachmentForward";
};
};
AnalyticsService =
{
Defaults = { ApiKey = "" }
};
Attachment =
{
Remove =
{
"Axis";
"Orientation";
"Position";
"SecondaryAxis";
"WorldAxis";
"WorldCFrame";
"WorldOrientation";
"WorldPosition";
"WorldSecondaryAxis";
};
};
BasePart =
{
Add =
{
Color3uint8 = "Color3uint8";
size = "Vector3";
};
Redirect =
{
Position = "CFrame.Position";
BrickColor = UseColor3("Color");
Color = "Color3uint8";
Size = "size";
};
Defaults =
{
Color3uint8 = Color3.fromRGB(163, 162, 165);
size = Vector3.new(4, 1.2, 2);
};
Remove =
{
"Orientation";
"Rotation";
}
};
BinaryStringValue =
{
Add =
{
Value = "BinaryString";
};
Defaults =
{
Value = "";
};
};
BodyColors =
{
Redirect =
{
HeadColor = UseColor3("HeadColor3");
LeftArmColor = UseColor3("LeftArmColor3");
RightArmColor = UseColor3("RightArmColor3");
LeftLegColor = UseColor3("LeftLegColor3");
RightLegColor = UseColor3("RightLegColor3");
TorsoColor = UseColor3("TorsoColor3");
}
};
BodyAngularVelocity =
{
Redirect = { angularvelocity = "AngularVelocity" };
};
BodyGyro =
{
Redirect = { cframe = "CFrame" };
};
Camera =
{
Redirect = { CoordinateFrame = "CFrame" }
};
DataModelMesh =
{
Add =
{
LODX = "Enum:LevelOfDetailSetting";
LODY = "Enum:LevelOfDetailSetting";
};
Defaults =
{
LODX = Enum.LevelOfDetailSetting.High;
LODY = Enum.LevelOfDetailSetting.High;
};
};
DataStoreService =
{
Defaults =
{
AutomaticRetry = true;
LegacyNamingScheme = false;
}
};
DebuggerWatch =
{
Defaults = { Expression = "" };
};
DoubleConstrainedValue =
{
Redirect = { ConstrainedValue = "Value" }
};
Fire =
{
Add =
{
heat_xml = "float";
size_xml = "float";
};
Defaults =
{
heat_xml = 9;
size_xml = 5;
};
Redirect =
{
Heat = "heat_xml";
Size = "size_xml";
};
};
FormFactorPart =
{
Add =
{
formFactorRaw = "Enum:FormFactor";
};
Defaults =
{
formFactorRaw = Enum.FormFactor.Brick;
};
Redirect =
{
FormFactor = "formFactorRaw";
};
};
GuiBase2d =
{
Redirect = { Localize = "AutoLocalize" }
};
GuiBase3d =
{
Redirect = { Color = UseColor3("Color3") }
};
GuiObject =
{
Redirect =
{
BackgroundColor = UseColor3("BackgroundColor3");
BorderColor = UseColor3("BorderColor3");
Transparency = "BackgroundTransparency";
}
};
HttpService =
{
Defaults = { HttpEnabled = false }
};
Humanoid =
{
Add =
{
Health_XML = "float";
InternalHeadScale = "float";
InternalBodyScale = "Vector3";
};
Defaults =
{
Health_XML = 100;
InternalHeadScale = 1;
InternalBodyScale = Vector3.new(1, 1, 1);
};
Redirect =
{
Health = "Health_XML";
};
Remove =
{
"Jump";
"Torso";
"LeftLeg";
"RightLeg";
};
};
HumanoidDescription =
{
Add =
{
EmotesDataInternal = "string";
EquippedEmotesDataInternal = "string";
};
Defaults =
{
EmotesDataInternal = "";
EquippedEmotesDataInternal = "";
};
};
InsertService =
{
Add = { AllowClientInsertModels = "bool" };
Defaults = { AllowClientInsertModels = false };
};
IntConstrainedValue =
{
Redirect = { ConstrainedValue = "Value" }
};
JointInstance =
{
Add = { IsAutoJoint = "bool" };
Defaults = { IsAutoJoint = true };
};
Lighting =
{
Add =
{
Technology = "Enum:Technology";
};
Defaults =
{
LegacyOutlines = false;
Technology = Enum.Technology.Compatibility;
};
Redirect =
{
Outlines = "LegacyOutlines";
};
Remove =
{
"ClockTime";
};
};
LocalizationService =
{
Remove =
{
"ForcePlayModeGameLocaleId";
"ForcePlayModeRobloxLocaleId";
"RobloxForcePlayModeGameLocaleId";
"RobloxForcePlayModeRobloxLocaleId";
}
};
LocalizationTable =
{
Add = { Contents = "string" };
Defaults = { Contents = "[]" };
Redirect =
{
DevelopmentLanguage = "SourceLocaleId";
}
};
ManualSurfaceJointInstance =
{
Add =
{
Surface0 = "int";
Surface1 = "int";
};
Defaults =
{
Surface0 = -1;
Surface1 = -1;
}
};
MeshPart =
{
Redirect = { MeshID = "MeshId" }
};
Model =
{
Add = { ModelInPrimary = "CFrame" };
Defaults = { ModelInPrimary = CFrame.new() };
};
NotificationService =
{
Remove = {"SelectedTheme"}
};
Part =
{
Add = { shape = "Enum:PartType" };
Redirect = { Shape = "shape" };
};
ParticleEmitter =
{
Redirect =
{
VelocitySpread =
{
Get = "SpreadAngle.X";
Set = "SpreadAngle = new Vector2(value, value)";
}
}
};
PartOperation =
{
Add =
{
AssetId = "Content";
ChildData = "BinaryString";
MeshData = "BinaryString";
};
Defaults =
{
AssetId = "";
ChildData = "";
MeshData = "";
};
};
PartOperationAsset =
{
Add =
{
ChildData = "BinaryString";
MeshData = "BinaryString";
};
Defaults =
{
ChildData = "";
MeshData = "";
};
};
Players =
{
Defaults =
{
MaxPlayersInternal = 16;
PreferredPlayersInternal = 0;
}
};
RenderingTest =
{
Remove =
{
"Position";
"Orientation";
};
};
ScriptContext =
{
Remove = { "ScriptsDisabled" }
};
SelectionBox =
{
Redirect = { SurfaceColor = UseColor3("SurfaceColor3") }
};
SelectionSphere =
{
Redirect = { SurfaceColor = UseColor3("SurfaceColor3") }
};
ServerScriptService =
{
Defaults = { LoadStringEnabled = false }
};
Smoke =
{
Add =
{
size_xml = "float";
opacity_xml = "float";
riseVelocity_xml = "float";
};
Defaults =
{
size_xml = 1;
opacity_xml = 0.5;
riseVelocity_xml = 1;
};
Redirect =
{
Size = "size_xml";
Opacity = "opacity_xml";
RiseVelocity = "riseVelocity_xml";
};
};
Sound =
{
Add =
{
MaxDistance = "float"; -- ?!
xmlRead_MaxDistance_3 = "float";
xmlRead_MinDistance_3 = "float";
};
Defaults =
{
xmlRead_MinDistance_3 = 10;
xmlRead_MaxDistance_3 = 10000;
};
Redirect =
{
EmitterSize = "xmlRead_MinDistance_3";
MaxDistance = "xmlRead_MaxDistance_3";
MinDistance = "EmitterSize";
Pitch = "PlaybackSpeed";
};
};
Sparkles =
{
Redirect = { Color = "SparkleColor" };
};
StudioData =
{
Defaults =
{
SrcPlaceId = 0;
SrcUniverseId = 0;
};
};
TextBox = GuiTextMixIn;
TextLabel = GuiTextMixIn;
TextButton = GuiTextMixIn;
Terrain =
{
Add =
{
ClusterGrid = "string";
ClusterGridV2 = "string";
ClusterGridV3 = "BinaryString";
SmoothGrid = "BinaryString";
PhysicsGrid = "BinaryString";
};
Defaults =
{
ClusterGrid = "";
ClusterGridV2 = "";
ClusterGridV3 = "";
SmoothGrid = "AQU=";
PhysicsGrid = "AgMAAAAAAAAAAAAAAAA=";
MaterialColors = "AAAAAAAAan8/P39rf2Y/ilY+j35fi21PZmxvZbDqw8faiVpHOi4kHh4lZlw76JxKc3trhHtagcLgc4RKxr21zq2UlJSM";
};
};
TerrainRegion =
{
Add =
{
ExtentsMax = "Vector3int16";
ExtentsMin = "Vector3int16";
GridV3 = "BinaryString";
SmoothGrid = "BinaryString";
};
Defaults =
{
ExtentsMax = Vector3int16.new();
ExtentsMin = Vector3int16.new();
GridV3 = "";
SmoothGrid = "AQU=";
};
};
Tool =
{
Remove =
{
"GripForward";
"GripPos";
"GripRight";
"GripUp";
};
};
TriangleMeshPart =
{
Add =
{
InitialSize = "Vector3";
LODData = "BinaryString";
PhysicsData = "BinaryString";
PhysicalConfigData = "SharedString";
};
Defaults =
{
LODData = "";
PhysicsData = "";
InitialSize = Vector3.new(1, 1, 1);
PhysicalConfigData = "1B2M2Y8AsgTpgAmY7PhCfg==";
};
};
TrussPart =
{
Add = { style = "Enum:Style" };
Redirect = { Style = "style" };
};
ViewportFrame =
{
Add =
{
CameraCFrame = "CFrame";
CameraFieldOfView = "float";
};
Defaults =
{
CameraCFrame = CFrame.new();
CameraFieldOfView = 70;
};
Remove = {"CurrentCamera"};
};
WeldConstraint =
{
Add =
{
Part0Internal = "Class:BasePart";
Part1Internal = "Class:BasePart";
CFrame0 = "CFrame";
CFrame1 = "CFrame";
};
Defaults =
{
CFrame0 = CFrame.new();
CFrame1 = CFrame.new();
Part0 = Instance.new("Part");
Part1 = Instance.new("Part");
};
Redirect =
{
Part0 = "Part0Internal";
Part1 = "Part1Internal";
};
};
Workspace =
{
Add =
{
AutoJointsMode = "Enum:AutoJointsMode";
CollisionGroups = "string";
ExplicitAutoJoints = "bool";
StreamingMinRadius = "int";
StreamingTargetRadius = "int";
StreamingPauseMode = "Enum:StreamingPauseMode";
TerrainWeldsFixed = "bool";
};
Defaults =
{
AutoJointsMode = Enum.AutoJointsMode.Default;
CollisionGroups = "Default^0^1";
ExplicitAutoJoints = true;
StreamingMinRadius = 64;
StreamingTargetRadius = 1024;
StreamingPauseMode = Enum.StreamingPauseMode.Default;
TerrainWeldsFixed = true;
}
}
}

View File

@ -3,9 +3,6 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using RobloxFiles.BinaryFormat;
using RobloxFiles.XmlFormat;
namespace RobloxFiles
{
/// <summary>

View File

@ -71,9 +71,14 @@
<Compile Include="BinaryFormat\Chunks\SSTR.cs" />
<Compile Include="BinaryFormat\IO\BinaryFileReader.cs" />
<Compile Include="BinaryFormat\IO\BinaryFileWriter.cs" />
<Compile Include="DataTypes\Color3uint8.cs" />
<Compile Include="DataTypes\ProtectedString.cs" />
<Compile Include="DataTypes\Content.cs" />
<Compile Include="DataTypes\SharedString.cs" />
<Compile Include="Interfaces\IBinaryFileChunk.cs" />
<Compile Include="Program.cs" />
<Compile Include="Tree\Enums.cs" />
<Compile Include="Generated\Classes.cs" />
<Compile Include="Generated\Enums.cs" />
<Compile Include="Tree\Property.cs" />
<Compile Include="Tree\Instance.cs" />
<Compile Include="RobloxFile.cs" />
@ -89,7 +94,6 @@
<Compile Include="DataTypes\NumberSequenceKeypoint.cs" />
<Compile Include="DataTypes\PhysicalProperties.cs" />
<Compile Include="DataTypes\Ray.cs" />
<Compile Include="DataTypes\Region3int16.cs" />
<Compile Include="Interfaces\IXmlPropertyToken.cs" />
<Compile Include="Utility\BrickColors.cs" />
<Compile Include="DataTypes\Vector3int16.cs" />
@ -100,41 +104,43 @@
<Compile Include="DataTypes\Vector2.cs" />
<Compile Include="DataTypes\Vector3.cs" />
<Compile Include="Utility\Formatting.cs" />
<Compile Include="Utility\FontUtility.cs" />
<Compile Include="Utility\MaterialInfo.cs" />
<Compile Include="Utility\Quaternion.cs" />
<Compile Include="DataTypes\Quaternion.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\SharedString.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Vector3int16.cs" />
<Compile Include="XmlFormat\Tokens\SharedString.cs" />
<Compile Include="XmlFormat\Tokens\ProtectedString.cs" />
<Compile Include="XmlFormat\Tokens\Vector3int16.cs" />
<Compile Include="XmlFormat\IO\XmlFileWriter.cs" />
<Compile Include="XmlFormat\PropertyTokens\XmlPropertyTokens.cs" />
<Compile Include="XmlFormat\XmlPropertyTokens.cs" />
<Compile Include="XmlFormat\IO\XmlFileReader.cs" />
<Compile Include="XmlFormat\XmlRobloxFile.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Axes.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\BinaryString.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Boolean.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\BrickColor.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\CFrame.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Content.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Color3.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Color3uint8.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\ColorSequence.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Double.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Enum.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Faces.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Float.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Int.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Int64.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\NumberRange.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\NumberSequence.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\PhysicalProperties.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Ray.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Rect.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Ref.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\String.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\UDim.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\UDim2.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Vector2.cs" />
<Compile Include="XmlFormat\PropertyTokens\Tokens\Vector3.cs" />
<Compile Include="XmlFormat\Tokens\Axes.cs" />
<Compile Include="XmlFormat\Tokens\BinaryString.cs" />
<Compile Include="XmlFormat\Tokens\Boolean.cs" />
<Compile Include="XmlFormat\Tokens\BrickColor.cs" />
<Compile Include="XmlFormat\Tokens\CFrame.cs" />
<Compile Include="XmlFormat\Tokens\Content.cs" />
<Compile Include="XmlFormat\Tokens\Color3.cs" />
<Compile Include="XmlFormat\Tokens\Color3uint8.cs" />
<Compile Include="XmlFormat\Tokens\ColorSequence.cs" />
<Compile Include="XmlFormat\Tokens\Double.cs" />
<Compile Include="XmlFormat\Tokens\Enum.cs" />
<Compile Include="XmlFormat\Tokens\Faces.cs" />
<Compile Include="XmlFormat\Tokens\Float.cs" />
<Compile Include="XmlFormat\Tokens\Int.cs" />
<Compile Include="XmlFormat\Tokens\Int64.cs" />
<Compile Include="XmlFormat\Tokens\NumberRange.cs" />
<Compile Include="XmlFormat\Tokens\NumberSequence.cs" />
<Compile Include="XmlFormat\Tokens\PhysicalProperties.cs" />
<Compile Include="XmlFormat\Tokens\Ray.cs" />
<Compile Include="XmlFormat\Tokens\Rect.cs" />
<Compile Include="XmlFormat\Tokens\Ref.cs" />
<Compile Include="XmlFormat\Tokens\String.cs" />
<Compile Include="XmlFormat\Tokens\UDim.cs" />
<Compile Include="XmlFormat\Tokens\UDim2.cs" />
<Compile Include="XmlFormat\Tokens\Vector2.cs" />
<Compile Include="XmlFormat\Tokens\Vector3.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2">

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace RobloxFiles
{
@ -11,16 +13,32 @@ namespace RobloxFiles
public class Instance
{
public Instance()
{
Name = ClassName;
}
/// <summary>The ClassName of this Instance.</summary>
public string ClassName;
public string ClassName => GetType().Name;
/// <summary>Internal list of Properties that are under this Instance.</summary>
private Dictionary<string, Property> props = new Dictionary<string, Property>();
/// <summary>A list of properties that are defined under this Instance.</summary>
private Dictionary<string, Property> props = new Dictionary<string, Property>();
public IReadOnlyDictionary<string, Property> Properties => props;
protected List<Instance> Children = new List<Instance>();
private Instance parent;
/// <summary>The raw list of children for this Instance.</summary>
internal List<Instance> Children = new List<Instance>();
/// <summary>Raw value of the Instance's parent.</summary>
private Instance RawParent;
/// <summary>The name of this Instance.</summary>
public string Name;
/// <summary>Indicates whether this Instance should be serialized.</summary>
public bool Archivable = true;
/// <summary>The name of this Instance, if a Name property is defined.</summary>
public override string ToString() => Name;
@ -30,14 +48,56 @@ namespace RobloxFiles
/// <summary>Indicates whether the parent of this object is locked.</summary>
public bool ParentLocked { get; internal set; }
/// <summary>Indicates whether this Instance is marked as a Service in the binary file format.</summary>
/// <summary>Indicates whether this Instance is a Service.</summary>
public bool IsService { get; internal set; }
/// <summary>If this instance is a service, this indicates whether the service should be loaded via GetService when Roblox loads the place file.</summary>
public bool IsRootedService { get; internal set; }
/// <summary>Raw list of CollectionService tags assigned to this Instance.</summary>
private List<string> RawTags = new List<string>();
/// <summary>Indicates whether this object should be serialized.</summary>
public bool Archivable = true;
/// <summary>A list of CollectionService tags assigned to this Instance.</summary>
public List<string> Tags => RawTags;
/// <summary>
/// Internal format of the Instance's CollectionService tags.
/// Property objects will look to this member for serializing the Tags property.
/// </summary>
internal byte[] SerializedTags
{
get
{
string fullString = string.Join("\0", Tags.ToArray());
byte[] buffer = fullString.ToCharArray()
.Select(ch => (byte)ch)
.ToArray();
return buffer;
}
set
{
int length = value.Length;
List<byte> buffer = new List<byte>();
Tags.Clear();
for (int i = 0; i < length; i++)
{
byte id = value[i];
if (id != 0)
buffer.Add(id);
if (id == 0 || i == (length - 1))
{
byte[] data = buffer.ToArray();
buffer.Clear();
string tag = Encoding.UTF8.GetString(data);
Tags.Add(tag);
}
}
}
}
/// <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>
@ -61,26 +121,20 @@ namespace RobloxFiles
return ancestor.IsAncestorOf(this);
}
public string Name
/// <summary>
/// Returns true if the provided instance inherits from the provided instance type.
/// </summary>
public bool IsA<T>() where T : Instance
{
get
{
Property propName = GetProperty("Name");
if (propName == null)
SetProperty("Name", "Instance");
return propName.Value.ToString();
}
set
{
SetProperty("Name", value);
}
Type myType = GetType();
Type classType = typeof(T);
return classType.IsAssignableFrom(myType);
}
/// <summary>
/// The parent of this Instance, or null if the instance is the root of a tree.<para/>
/// Setting the value of this property will throw an exception if:<para/>
/// - The parent is currently locked.<para/>
/// - The value is set to itself.<para/>
/// - The value is a descendant of the Instance.
/// </summary>
@ -88,7 +142,7 @@ namespace RobloxFiles
{
get
{
return parent;
return RawParent;
}
set
{
@ -101,11 +155,10 @@ namespace RobloxFiles
if (Parent == this)
throw new Exception("Attempt to set parent to self.");
if (parent != null)
parent.Children.Remove(this);
RawParent?.Children.Remove(this);
value?.Children.Add(this);
value.Children.Add(this);
parent = value;
RawParent = value;
}
}
@ -143,10 +196,14 @@ namespace RobloxFiles
/// </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)
public T FindFirstChild<T>(string name, bool recursive = false) where T : Instance
{
Instance result = null;
var query = Children.Where((child) => name == child.Name);
T result = null;
var query = Children
.Where(child => child is T)
.Where(child => name == child.Name)
.Cast<T>();
if (query.Count() > 0)
{
@ -156,7 +213,7 @@ namespace RobloxFiles
{
foreach (Instance child in Children)
{
Instance found = child.FindFirstChild(name, true);
T found = child.FindFirstChild<T>(name, true);
if (found != null)
{
@ -169,6 +226,37 @@ namespace RobloxFiles
return result;
}
/// <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)
{
return FindFirstChild<Instance>(name, recursive);
}
/// <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 T FindFirstAncestor<T>(string name) where T : Instance
{
Instance ancestor = Parent;
while (ancestor != null)
{
if (ancestor is T && ancestor.Name == name)
return (T)ancestor;
ancestor = ancestor.Parent;
}
return null;
}
/// <summary>
/// Returns the first ancestor of this Instance whose Name is the provided string name.
/// If the instance is not found, this returns null.
@ -176,17 +264,7 @@ namespace RobloxFiles
/// <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;
return FindFirstAncestor<Instance>(name);
}
/// <summary>
@ -194,18 +272,45 @@ namespace RobloxFiles
/// If the instance is not found, this returns null.
/// </summary>
/// <param name="name">The Name of the Instance to find.</param>
public Instance FindFirstAncestorOfClass(string className)
public T FindFirstAncestorOfClass<T>() where T : Instance
{
Type classType = typeof(T);
string className = classType.Name;
Instance ancestor = Parent;
while (ancestor != null)
{
if (ancestor.ClassName == className)
break;
if (ancestor is T)
return (T)ancestor;
ancestor = ancestor.Parent;
}
return null;
}
/// <summary>
/// Returns the first ancestor of this Instance which derives from the provided type T.
/// If the instance is not found, this returns null.
/// </summary>
/// <param name="name">The Name of the Instance to find.</param>
public T FindFirstAncestorWhichIsA<T>() where T : Instance
{
T ancestor = null;
Instance check = Parent;
while (check != null)
{
if (check.IsA<T>())
{
ancestor = (T)check;
break;
}
check = check.Parent;
}
return ancestor;
}
@ -214,13 +319,65 @@ namespace RobloxFiles
/// 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, bool recursive = false)
public T FindFirstChildOfClass<T>(bool recursive = false) where T : Instance
{
Instance result = null;
var query = Children.Where((child) => className == child.ClassName);
var query = Children
.Where(child => child is T)
.Cast<T>();
T result = null;
if (query.Count() > 0)
{
result = query.First();
}
else if (recursive)
{
foreach (Instance child in Children)
{
T found = child.FindFirstChildOfClass<T>(true);
if (found != null)
{
result = found;
break;
}
}
}
return result;
}
/// <summary>
/// Returns the first child of this Instance which derives from the provided type T.
/// If the instance is not found, this returns null.
/// </summary>
/// <param name="recursive">Whether this should search descendants as well.</param>
public T FindFirstChildWhichIsA<T>(bool recursive = false) where T : Instance
{
var query = Children
.Where(child => child.IsA<T>())
.Cast<T>();
T result = null;
if (query.Count() > 0)
{
result = query.First();
}
else if (recursive)
{
foreach (Instance child in Children)
{
T found = child.FindFirstChildWhichIsA<T>(true);
if (found != null)
{
result = found;
break;
}
}
}
return result;
}
@ -243,123 +400,18 @@ namespace RobloxFiles
}
/// <summary>
/// Returns a Property object if a property with the specified name is defined in this Instance.
/// Returns a Property object whose name is the provided string name.
/// </summary>
public Property GetProperty(string name)
{
Property result = null;
if (Properties.ContainsKey(name))
result = Properties[name];
if (props.ContainsKey(name))
result = props[name];
return result;
}
/// <summary>
/// Finds or creates a property with the specified name, and sets its value to the provided object.
/// Returns the property object that had its value set, if the value is not null.
/// </summary>
public Property SetProperty(string name, object value, PropertyType? preferType = null)
{
Property prop = GetProperty(name) ?? new Property()
{
Type = preferType ?? PropertyType.Unknown,
Name = name
};
if (preferType == null)
{
object oldValue = prop.Value;
Type oldType = oldValue?.GetType();
Type newType = value?.GetType();
if (oldType != newType)
{
if (value == null)
{
RemoveProperty(name);
return prop;
}
string typeName = newType.Name;
if (value is Instance)
typeName = "Ref";
else if (value is int)
typeName = "Int";
else if (value is long)
typeName = "Int64";
Enum.TryParse(typeName, out prop.Type);
}
}
prop.Value = value;
if (prop.Instance == null)
AddProperty(ref prop);
return prop;
}
/// <summary>
/// Looks for a property with the specified property name, and returns its value as an object.
/// <para/>The resulting value may be null if the property is not serialized.
/// <para/>You can use the templated ReadProperty overload to fetch it as a specific type with a default value provided.
/// </summary>
/// <param name="propertyName">The name of the property to be fetched from this Instance.</param>
/// <returns>An object reference to the value of the specified property, if it exists.</returns>
public object ReadProperty(string propertyName)
{
Property property = GetProperty(propertyName);
return property?.Value;
}
/// <summary>
/// Looks for a property with the specified property name, and returns it as the specified type.<para/>
/// If it cannot be converted, the provided nullFallback value will be returned instead.
/// </summary>
/// <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="nullFallback">A fallback value to be returned if casting to T fails, or the property is not found.</param>
/// <returns></returns>
public T ReadProperty<T>(string propertyName, T nullFallback)
{
try
{
object result = ReadProperty(propertyName);
return (T)result;
}
catch
{
return nullFallback;
}
}
/// <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/>
/// 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>
/// <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="outValue">The value to write to if the property can be casted to T correctly.</param>
public bool TryReadProperty<T>(string propertyName, ref T outValue)
{
try
{
object result = ReadProperty(propertyName);
outValue = (T)result;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Adds a property by reference to this Instance's property list.
/// </summary>
@ -368,16 +420,8 @@ namespace RobloxFiles
{
prop.Instance = this;
if (prop.Name == "Name")
{
Property nameProp = GetProperty("Name");
if (nameProp != null)
{
nameProp.Value = prop.Value;
return;
}
}
if (props.ContainsKey(prop.Name))
props.Remove(prop.Name);
props.Add(prop.Name, prop);
}
@ -387,14 +431,69 @@ namespace RobloxFiles
/// </summary>
/// <param name="name">The name of the property to be removed.</param>
/// <returns>True if a property with the provided name was removed.</returns>
public bool RemoveProperty(string name)
internal bool RemoveProperty(string name)
{
Property prop = GetProperty(name);
if (prop != null)
if (props.ContainsKey(name))
{
Property prop = Properties[name];
prop.Instance = null;
}
return props.Remove(name);
}
/// <summary>
/// Ensures that all serializable properties of this Instance have
/// a registered Property object with the correct PropertyType.
/// </summary>
internal IReadOnlyDictionary<string, Property> RefreshProperties()
{
Type instType = GetType();
FieldInfo[] fields = instType.GetFields(Property.BindingFlags);
foreach (FieldInfo field in fields)
{
string fieldName = field.Name;
Type fieldType = field.FieldType;
if (field.GetCustomAttribute<ObsoleteAttribute>() != null)
continue;
if (Property.Types.ContainsKey(fieldType))
{
if (fieldName.EndsWith("_"))
fieldName = instType.Name;
if (!props.ContainsKey(fieldName))
{
Property newProp = new Property()
{
Type = Property.Types[fieldType],
Value = field.GetValue(this),
Name = fieldName,
Instance = this
};
AddProperty(ref newProp);
}
else
{
Property prop = props[fieldName];
prop.Value = field.GetValue(this);
prop.Type = Property.Types[fieldType];
}
}
}
Property tags = GetProperty("Tags");
if (tags == null)
{
tags = new Property("Tags", PropertyType.String);
AddProperty(ref tags);
}
return Properties;
}
}
}

View File

@ -1,8 +1,13 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using RobloxFiles.BinaryFormat;
using RobloxFiles.BinaryFormat.Chunks;
using RobloxFiles.DataTypes;
using RobloxFiles.Utility;
namespace RobloxFiles
{
public enum PropertyType
@ -22,8 +27,7 @@ namespace RobloxFiles
Color3,
Vector2,
Vector3,
Vector2int16,
CFrame,
CFrame = 16,
Quaternion,
Enum,
Ref,
@ -48,11 +52,57 @@ namespace RobloxFiles
public string XmlToken = "";
public byte[] RawBuffer;
internal BinaryRobloxFileWriter CurrentWriter;
internal object RawValue;
internal BinaryRobloxFileWriter CurrentWriter;
internal static BindingFlags BindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase;
public static IReadOnlyDictionary<Type, PropertyType> Types = new Dictionary<Type, PropertyType>()
{
{ typeof(Axes), PropertyType.Axes },
{ typeof(Faces), PropertyType.Faces },
{ typeof(int), PropertyType.Int },
{ typeof(bool), PropertyType.Bool },
{ typeof(long), PropertyType.Int64 },
{ typeof(float), PropertyType.Float },
{ typeof(double), PropertyType.Double },
{ typeof(string), PropertyType.String },
{ typeof(Ray), PropertyType.Ray },
{ typeof(Rect), PropertyType.Rect },
{ typeof(UDim), PropertyType.UDim },
{ typeof(UDim2), PropertyType.UDim2 },
{ typeof(CFrame), PropertyType.CFrame },
{ typeof(Color3), PropertyType.Color3 },
{ typeof(Vector2), PropertyType.Vector2 },
{ typeof(Vector3), PropertyType.Vector3 },
{ typeof(BrickColor), PropertyType.BrickColor },
{ typeof(Quaternion), PropertyType.Quaternion },
{ typeof(NumberRange), PropertyType.NumberRange },
{ typeof(SharedString), PropertyType.SharedString },
{ typeof(Vector3int16), PropertyType.Vector3int16 },
{ typeof(ColorSequence), PropertyType.ColorSequence },
{ typeof(NumberSequence), PropertyType.NumberSequence },
{ typeof(PhysicalProperties), PropertyType.PhysicalProperties },
};
private void ImproviseRawBuffer()
{
if (RawValue is byte[])
{
RawBuffer = RawValue as byte[];
return;
}
else if (RawValue is SharedString)
{
var sharedString = CastValue<SharedString>();
RawBuffer = Convert.FromBase64String(sharedString.MD5_Key);
return;
}
switch (Type)
{
case PropertyType.Int:
@ -70,21 +120,85 @@ namespace RobloxFiles
case PropertyType.Double:
RawBuffer = BitConverter.GetBytes((double)Value);
break;
case PropertyType.SharedString:
RawBuffer = Convert.FromBase64String((string)Value);
break;
//
}
}
private string ImplicitName
{
get
{
if (Instance != null)
{
Type instType = Instance.GetType();
string typeName = instType.Name;
if (typeName == Name)
{
var implicitName = Name + '_';
return implicitName;
}
}
return Name;
}
}
public object Value
{
get
{
if (Instance != null)
{
if (Name == "Tags")
{
byte[] data = Instance.SerializedTags;
RawValue = data;
}
else
{
FieldInfo field = Instance.GetType()
.GetField(ImplicitName, BindingFlags);
if (field != null)
{
object value = field.GetValue(Instance);
RawValue = value;
}
else
{
Console.WriteLine($"RobloxFiles.Property - No defined field for {Instance.ClassName}.{Name}");
}
}
}
return RawValue;
}
set
{
if (Instance != null)
{
if (Name == "Tags" && value is byte[])
{
byte[] data = value as byte[];
Instance.SerializedTags = data;
}
else
{
FieldInfo field = Instance.GetType()
.GetField(ImplicitName, BindingFlags);
try
{
field?.SetValue(Instance, value);
}
catch
{
Console.WriteLine($"RobloxFiles.Property - Failed to cast value {value} into property {Instance.ClassName}.{Name}");
}
}
}
RawValue = value;
RawBuffer = null;
@ -96,11 +210,9 @@ namespace RobloxFiles
{
get
{
// Improvise what the buffer should be if this is a primitive.
if (RawBuffer == null && Value != null)
{
// Improvise what the buffer should be if this is a primitive.
ImproviseRawBuffer();
}
return (RawBuffer != null);
}

66
Utility/FontUtility.cs Normal file
View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RobloxFiles.Enums;
namespace RobloxFiles.Utility
{
public static class FontUtility
{
public static IReadOnlyDictionary<int, FontSize> FontSizes = new Dictionary<int, FontSize>()
{
{ 8, FontSize.Size8 },
{ 9, FontSize.Size9 },
{ 10, FontSize.Size10 },
{ 11, FontSize.Size11 },
{ 12, FontSize.Size12 },
{ 14, FontSize.Size14 },
{ 18, FontSize.Size18 },
{ 24, FontSize.Size24 },
{ 28, FontSize.Size28 },
{ 32, FontSize.Size32 },
{ 36, FontSize.Size36 },
{ 42, FontSize.Size42 },
{ 48, FontSize.Size48 },
{ 60, FontSize.Size60 },
{ 96, FontSize.Size96 },
};
private static Dictionary<int, FontSize> IntToFontSize = new Dictionary<int, FontSize>();
public static FontSize GetFontSize(int fontSize)
{
if (fontSize > 60)
return FontSize.Size96;
if (FontSizes.ContainsKey(fontSize))
return FontSizes[fontSize];
FontSize closest = FontSizes
.Where(pair => pair.Key <= fontSize)
.Select(pair => pair.Value)
.Last();
return closest;
}
public static FontSize GetFontSize(float size)
{
int fontSize = (int)size;
return GetFontSize(fontSize);
}
public static int GetFontSize(FontSize fontSize)
{
int value = FontSizes
.Where(pair => pair.Value == fontSize)
.Select(pair => pair.Key)
.First();
return value;
}
}
}

View File

@ -1,8 +1,5 @@
using System.Globalization;
using System.Linq;
// This global class defines extension methods to numeric types
// where I don't want system globalization to come into play.
using System;
using System.Globalization;
internal static class Formatting
{
@ -105,4 +102,14 @@ internal static class Formatting
{
return int.Parse(s, invariant);
}
public static bool FuzzyEquals(this float a, float b, float epsilon = 10e-5f)
{
return Math.Abs(a - b) < epsilon;
}
public static bool FuzzyEquals(this double a, double b, double epsilon = 10e-5)
{
return Math.Abs(a - b) < epsilon;
}
}

View File

@ -25,6 +25,7 @@ namespace RobloxFiles.Utility
{Material.DiamondPlate, 7.85f},
{Material.Fabric, 0.70f},
{Material.Foil, 2.70f},
{Material.ForceField, 2.40f},
{Material.Glacier, 0.92f},
{Material.Glass, 2.40f},
{Material.Granite, 2.69f},
@ -68,6 +69,7 @@ namespace RobloxFiles.Utility
{Material.DiamondPlate, 0.25f},
{Material.Fabric, 0.05f},
{Material.Foil, 0.25f},
{Material.ForceField, 0.20f},
{Material.Glacier, 0.15f},
{Material.Glass, 0.20f},
{Material.Granite, 0.20f},
@ -111,6 +113,7 @@ namespace RobloxFiles.Utility
{Material.DiamondPlate, 0.35f},
{Material.Fabric, 0.35f},
{Material.Foil, 0.40f},
{Material.ForceField, 0.25f},
{Material.Glacier, 0.05f},
{Material.Glass, 0.25f},
{Material.Granite, 0.40f},

View File

@ -1,6 +1,8 @@
using System;
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat
{
public static class XmlRobloxFileReader
@ -35,11 +37,35 @@ namespace RobloxFiles.XmlFormat
string key = md5Node.InnerText;
string value = sharedString.InnerText.Replace("\n", "");
file.SharedStrings.Add(key, value);
byte[] buffer = Convert.FromBase64String(value);
SharedString record = SharedString.FromBase64(value);
if (record.MD5_Key != key)
throw error("The provided md5 hash did not match with the md5 hash computed for the value!");
file.SharedStrings.Add(key);
}
}
}
public static void ReadMetadata(XmlNode meta, XmlRobloxFile file)
{
var error = createErrorHandler("ReadMetadata");
if (meta.Name != "Meta")
throw error("Provided XmlNode's class should be 'Meta'!");
XmlNode propName = meta.Attributes.GetNamedItem("name");
if (propName == null)
throw error("Got a Meta node without a 'name' attribute!");
string key = propName.InnerText;
string value = meta.InnerText;
file.Metadata[key] = value;
}
public static void ReadProperties(Instance instance, XmlNode propsNode)
{
var error = createErrorHandler("ReadProperties");
@ -53,7 +79,12 @@ namespace RobloxFiles.XmlFormat
XmlNode propName = propNode.Attributes.GetNamedItem("name");
if (propName == null)
{
if (propNode.Name == "Item")
continue;
throw error("Got a property node without a 'name' attribute!");
}
IXmlPropertyToken tokenHandler = XmlPropertyTokens.GetHandler(propType);
@ -90,8 +121,12 @@ namespace RobloxFiles.XmlFormat
if (classToken == null)
throw error("Got an Item without a defined 'class' attribute!");
Instance inst = new Instance() { ClassName = classToken.InnerText };
string className = classToken.InnerText;
Type instType = Type.GetType($"RobloxFiles.{className}") ?? typeof(Instance);
Instance inst = Activator.CreateInstance(instType) as Instance;
// The 'referent' attribute is optional, but should be defined if a Ref property needs to link to this Instance.
XmlNode refToken = instNode.Attributes.GetNamedItem("referent");

View File

@ -1,9 +1,8 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using RobloxFiles.DataTypes;
using RobloxFiles.XmlFormat.PropertyTokens;
namespace RobloxFiles.XmlFormat
@ -40,7 +39,7 @@ namespace RobloxFiles.XmlFormat
foreach (Instance child in inst.GetChildren())
RecordInstances(file, child);
if (inst.Referent.Length < 35)
if (inst.Referent == null || inst.Referent.Length < 35)
inst.Referent = CreateReferent();
file.Instances.Add(inst.Referent, inst);
@ -102,19 +101,8 @@ namespace RobloxFiles.XmlFormat
if (prop.Type == PropertyType.SharedString)
{
string data = prop.Value.ToString();
byte[] buffer = Convert.FromBase64String(data);
using (MD5 md5 = MD5.Create())
{
byte[] hash = md5.ComputeHash(buffer);
string key = Convert.ToBase64String(hash);
if (!file.SharedStrings.ContainsKey(key))
file.SharedStrings.Add(key, data);
propNode.InnerText = key;
}
SharedString value = prop.CastValue<SharedString>();
file.SharedStrings.Add(value.MD5_Key);
}
return propNode;
@ -122,6 +110,9 @@ namespace RobloxFiles.XmlFormat
public static XmlNode WriteInstance(Instance instance, XmlDocument doc, XmlRobloxFile file)
{
if (!instance.Archivable)
return null;
XmlElement instNode = doc.CreateElement("Item");
instNode.SetAttribute("class", instance.ClassName);
instNode.SetAttribute("referent", instance.Referent);
@ -129,7 +120,7 @@ namespace RobloxFiles.XmlFormat
XmlElement propsNode = doc.CreateElement("Properties");
instNode.AppendChild(propsNode);
var props = instance.Properties;
var props = instance.RefreshProperties();
foreach (string propName in props.Keys)
{
@ -140,8 +131,11 @@ namespace RobloxFiles.XmlFormat
foreach (Instance child in instance.GetChildren())
{
XmlNode childNode = WriteInstance(child, doc, file);
instNode.AppendChild(childNode);
if (child.Archivable)
{
XmlNode childNode = WriteInstance(child, doc, file);
instNode.AppendChild(childNode);
}
}
return instNode;
@ -152,18 +146,15 @@ namespace RobloxFiles.XmlFormat
XmlElement sharedStrings = doc.CreateElement("SharedStrings");
var binaryWriter = XmlPropertyTokens.GetHandler<BinaryStringToken>();
var bufferProp = new Property("SharedString", PropertyType.String);
var binaryBuffer = new Property("SharedString", PropertyType.String);
foreach (string md5 in file.SharedStrings.Keys)
foreach (string md5 in file.SharedStrings)
{
XmlElement sharedString = doc.CreateElement("SharedString");
sharedString.SetAttribute("md5", md5);
string data = file.SharedStrings[md5];
byte[] buffer = Convert.FromBase64String(data);
bufferProp.RawBuffer = buffer;
binaryWriter.WriteProperty(bufferProp, doc, sharedString);
binaryBuffer.RawBuffer = SharedString.FindRecord(md5);
binaryWriter.WriteProperty(binaryBuffer, doc, sharedString);
sharedStrings.AppendChild(sharedString);
}

View File

@ -1,19 +0,0 @@
using System.Xml;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
public class EnumToken : IXmlPropertyToken
{
public string Token => "token";
public bool ReadProperty(Property prop, XmlNode token)
{
return XmlPropertyTokens.ReadPropertyGeneric<uint>(prop, PropertyType.Enum, token);
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToString();
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Xml;
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat.PropertyTokens
@ -7,25 +6,20 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public class AxesToken : IXmlPropertyToken
{
public string Token => "Axes";
public bool ReadProperty(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadPropertyGeneric<uint>(prop, PropertyType.Axes, token);
uint value;
if (success)
if (XmlPropertyTokens.ReadPropertyGeneric(token, out value))
{
uint value = (uint)prop.Value;
try
{
Axes axes = (Axes)value;
prop.Value = axes;
}
catch
{
success = false;
}
Axes axes = (Axes)value;
prop.Value = axes;
return true;
}
return success;
return false;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
@ -33,7 +27,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
XmlElement axes = doc.CreateElement("axes");
node.AppendChild(axes);
int value = (int)prop.Value;
int value = prop.CastValue<int>();
axes.InnerText = value.ToInvariantString();
}
}

View File

@ -11,9 +11,9 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
{
// BinaryStrings are encoded in base64
string base64 = token.InnerText.Replace("\n", "");
prop.Value = Convert.FromBase64String(base64);
prop.Type = PropertyType.String;
prop.Value = base64;
byte[] buffer = Convert.FromBase64String(base64);
prop.RawBuffer = buffer;

View File

@ -13,31 +13,23 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public bool ReadProperty(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadPropertyGeneric<int>(prop, PropertyType.BrickColor, token);
int value;
if (success)
if (XmlPropertyTokens.ReadPropertyGeneric(token, out value))
{
int value = (int)prop.Value;
BrickColor brickColor = BrickColor.FromNumber(value);
prop.XmlToken = "BrickColor";
prop.Value = brickColor;
try
{
BrickColor brickColor = BrickColor.FromNumber(value);
prop.XmlToken = "BrickColor";
prop.Value = brickColor;
}
catch
{
// Invalid BrickColor Id?
success = false;
}
return true;
}
return success;
return false;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
BrickColor value = prop.Value as BrickColor;
BrickColor value = prop.CastValue<BrickColor>();
XmlElement brickColor = doc.CreateElement("int");
brickColor.InnerText = value.Number.ToInvariantString();

View File

@ -46,7 +46,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
CFrame cf = prop.Value as CFrame;
CFrame cf = prop.CastValue<CFrame>();
float[] components = cf.GetComponents();
for (int i = 0; i < 12; i++)

View File

@ -51,26 +51,18 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
if (prop.Name == "Color3uint8")
Color3 color = prop.CastValue<Color3>();
float[] rgb = new float[3] { color.R, color.G, color.B };
for (int i = 0; i < 3; i++)
{
var handler = XmlPropertyTokens.GetHandler<Color3uint8Token>();
handler.WriteProperty(prop, doc, node);
}
else
{
Color3 color = prop.Value as Color3;
float[] rgb = new float[3] { color.R, color.G, color.B };
string field = Fields[i];
float value = rgb[i];
for (int i = 0; i < 3; i++)
{
string field = Fields[i];
float value = rgb[i];
XmlElement channel = doc.CreateElement(field);
channel.InnerText = value.ToInvariantString();
XmlElement channel = doc.CreateElement(field);
channel.InnerText = value.ToInvariantString();
node.AppendChild(channel);
}
node.AppendChild(channel);
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Xml;
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat.PropertyTokens
@ -10,29 +9,30 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public bool ReadProperty(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadPropertyGeneric<uint>(prop, PropertyType.Color3, token);
uint value;
if (success)
if (XmlPropertyTokens.ReadPropertyGeneric(token, out value))
{
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);
Color3uint8 result = Color3.FromRGB(r, g, b);
prop.Value = result;
return true;
}
return success;
return false;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
Color3 color = prop.Value as Color3;
Color3uint8 color = prop.CastValue<Color3uint8>();
uint r = (uint)(color.R * 256);
uint g = (uint)(color.G * 256);
uint b = (uint)(color.B * 256);
uint r = color.R,
g = color.G,
b = color.B;
uint rgb = (255u << 24) | (r << 16) | (g << 8) | b;
node.InnerText = rgb.ToString();

View File

@ -47,7 +47,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToString() + ' ';
ColorSequence value = prop.CastValue<ColorSequence>();
node.InnerText = value.ToString() + ' ';
}
}
}

View File

@ -1,6 +1,8 @@
using System;
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
public class ContentToken : IXmlPropertyToken
@ -9,9 +11,9 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public bool ReadProperty(Property prop, XmlNode token)
{
string content = token.InnerText;
string data = token.InnerText;
prop.Value = new Content(data);
prop.Type = PropertyType.String;
prop.Value = content;
if (token.HasChildNodes)
{
@ -23,12 +25,12 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
try
{
// Roblox technically doesn't support this anymore, but load it anyway :P
byte[] buffer = Convert.FromBase64String(content);
byte[] buffer = Convert.FromBase64String(data);
prop.RawBuffer = buffer;
}
catch
{
Console.WriteLine("ContentToken: Got illegal base64 string: {0}", content);
Console.WriteLine("ContentToken: Got illegal base64 string: {0}", data);
}
}
}
@ -38,7 +40,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
string content = prop.Value.ToString();
string content = prop.CastValue<Content>();
string type = "null";
if (prop.HasRawBuffer)

View File

@ -13,7 +13,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToInvariantString();
double value = prop.CastValue<double>();
node.InnerText = value.ToInvariantString();
}
}
}

48
XmlFormat/Tokens/Enum.cs Normal file
View File

@ -0,0 +1,48 @@
using System;
using System.Reflection;
using System.Xml;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
public class EnumToken : IXmlPropertyToken
{
public string Token => "token";
public bool ReadProperty(Property prop, XmlNode token)
{
uint value;
if (XmlPropertyTokens.ReadPropertyGeneric(token, out value))
{
Instance inst = prop.Instance;
Type instType = inst?.GetType();
FieldInfo info = instType.GetField(prop.Name, Property.BindingFlags);
if (info != null)
{
Type enumType = info.FieldType;
string item = value.ToInvariantString();
prop.Type = PropertyType.Enum;
prop.Value = Enum.Parse(enumType, item);
return true;
}
}
return false;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
object rawValue = prop.Value;
Type valueType = rawValue.GetType();
int signed = (int)rawValue;
uint value = (uint)signed;
node.InnerText = value.ToString();
}
}
}

View File

@ -9,23 +9,17 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public bool ReadProperty(Property prop, XmlNode token)
{
bool success = XmlPropertyTokens.ReadPropertyGeneric<uint>(prop, PropertyType.Faces, token);
uint value;
if (success)
if (XmlPropertyTokens.ReadPropertyGeneric(token, out value))
{
uint value = (uint)prop.Value;
try
{
Faces faces = (Faces)value;
prop.Value = faces;
}
catch
{
success = false;
}
Faces faces = (Faces)value;
prop.Value = faces;
return true;
}
return success;
return false;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
@ -33,7 +27,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
XmlElement faces = doc.CreateElement("faces");
node.AppendChild(faces);
int value = (int)prop.Value;
int value = prop.CastValue<int>();
faces.InnerText = value.ToInvariantString();
}
}

View File

@ -13,7 +13,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToInvariantString();
float value = prop.CastValue<float>();
node.InnerText = value.ToInvariantString();
}
}
}

View File

@ -24,7 +24,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToInvariantString();
int value = prop.CastValue<int>();
node.InnerText = value.ToInvariantString();
}
}
}

View File

@ -13,7 +13,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToString();
long value = prop.CastValue<long>();
node.InnerText = value.ToString();
}
}
}

View File

@ -35,7 +35,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToString() + ' ';
NumberRange value = prop.CastValue<NumberRange>();
node.InnerText = value.ToString() + ' ';
}
}
}

View File

@ -44,7 +44,8 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
node.InnerText = prop.Value.ToString() + ' ';
NumberSequence value = prop.CastValue<NumberSequence>();
node.InnerText = value.ToString() + ' ';
}
}
}

View File

@ -61,7 +61,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
if (hasCustomPhysics)
{
var customProps = prop.Value as PhysicalProperties;
var customProps = prop.CastValue<PhysicalProperties>();
var data = new Dictionary<string, float>()
{

View File

@ -0,0 +1,34 @@
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
public class ProtectedStringToken : IXmlPropertyToken
{
public string Token => "ProtectedString";
public bool ReadProperty(Property prop, XmlNode token)
{
ProtectedString contents = token.InnerText;
prop.Type = PropertyType.String;
prop.Value = contents;
return true;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
string value = prop.CastValue<ProtectedString>();
if (value.Contains("\r") || value.Contains("\n"))
{
XmlCDataSection cdata = doc.CreateCDataSection(value);
node.AppendChild(cdata);
}
else
{
node.InnerText = value;
}
}
}
}

View File

@ -39,7 +39,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
Ray ray = prop.Value as Ray;
Ray ray = prop.CastValue<Ray>();
XmlElement origin = doc.CreateElement("origin");
XmlElement direction = doc.CreateElement("direction");

View File

@ -39,7 +39,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
Rect rect = prop.Value as Rect;
Rect rect = prop.CastValue<Rect>();
XmlElement min = doc.CreateElement("min");
Vector2Token.WriteVector2(doc, min, rect.Min);

View File

@ -10,7 +10,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
{
string refId = token.InnerText;
prop.Type = PropertyType.Ref;
prop.Value = refId;
prop.XmlToken = refId;
return true;
}
@ -19,9 +19,9 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
{
string result = "null";
if (prop.Value != null && prop.Value.ToString() != "null")
if (prop.Value != null)
{
Instance inst = prop.Value as Instance;
Instance inst = prop.CastValue<Instance>();
result = inst.Referent;
}

View File

@ -1,6 +1,5 @@
using System;
using System.Text;
using System.Xml;
using System.Xml;
using RobloxFiles.DataTypes;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
@ -10,17 +9,17 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public bool ReadProperty(Property prop, XmlNode token)
{
string contents = token.InnerText;
string md5 = token.InnerText;
prop.Type = PropertyType.SharedString;
prop.Value = contents;
prop.Value = new SharedString(md5);
return true;
}
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
var BinaryStringToken = XmlPropertyTokens.GetHandler<BinaryStringToken>();
BinaryStringToken.WriteProperty(prop, doc, node);
SharedString value = prop.CastValue<SharedString>();
node.InnerText = value.MD5_Key;
}
}
}

View File

@ -1,12 +1,10 @@
using System;
using System.Text;
using System.Xml;
using System.Xml;
namespace RobloxFiles.XmlFormat.PropertyTokens
{
public class StringToken : IXmlPropertyToken
{
public string Token => "string; ProtectedString";
public string Token => "string";
public bool ReadProperty(Property prop, XmlNode token)
{

View File

@ -53,7 +53,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
UDim value = prop.Value as UDim;
UDim value = prop.CastValue<UDim>();
WriteUDim(doc, node, value);
}
}

View File

@ -26,7 +26,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
UDim2 value = prop.Value as UDim2;
UDim2 value = prop.CastValue<UDim2>();
UDim xUDim = value.X;
UDimToken.WriteUDim(doc, node, xUDim, "X");

View File

@ -58,7 +58,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
Vector2 value = prop.Value as Vector2;
Vector2 value = prop.CastValue<Vector2>();
WriteVector2(doc, node, value);
}
}

View File

@ -62,7 +62,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
Vector3 value = prop.Value as Vector3;
Vector3 value = prop.CastValue<Vector3>();
WriteVector3(doc, node, value);
}
}

View File

@ -39,7 +39,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens
public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
{
Vector3int16 value = prop.Value as Vector3int16;
Vector3int16 value = prop.CastValue<Vector3int16>();
XmlElement x = doc.CreateElement("X");
x.InnerText = value.X.ToString();

View File

@ -35,38 +35,54 @@ namespace RobloxFiles.XmlFormat
Handlers = tokenHandlers;
}
public static bool ReadPropertyGeneric<T>(Property prop, PropertyType propType, XmlNode token) where T : struct
public static bool ReadPropertyGeneric<T>(XmlNode token, out T outValue) where T : struct
{
try
{
string value = token.InnerText;
Type type = typeof(T);
if (type == typeof(int))
prop.Value = Formatting.ParseInt(value);
else if (type == typeof(float))
prop.Value = Formatting.ParseFloat(value);
else if (type == typeof(double))
prop.Value = Formatting.ParseDouble(value);
object result = null;
if (prop.Value == null)
if (type == typeof(int))
result = Formatting.ParseInt(value);
else if (type == typeof(float))
result = Formatting.ParseFloat(value);
else if (type == typeof(double))
result = Formatting.ParseDouble(value);
if (result == null)
{
Type resultType = typeof(T);
TypeConverter converter = TypeDescriptor.GetConverter(resultType);
object result = converter.ConvertFromString(token.InnerText);
prop.Value = result;
var converter = TypeDescriptor.GetConverter(resultType);
result = converter.ConvertFromString(token.InnerText);
}
prop.Type = propType;
outValue = (T)result;
return true;
}
catch
{
outValue = default(T);
return false;
}
}
public static bool ReadPropertyGeneric<T>(Property prop, PropertyType propType, XmlNode token) where T : struct
{
T result;
if (ReadPropertyGeneric(token, out result))
{
prop.Type = propType;
prop.Value = result;
return true;
}
return false;
}
public static IXmlPropertyToken GetHandler(string tokenName)
{
IXmlPropertyToken result = null;

View File

@ -6,20 +6,26 @@ using System.Linq;
using System.Text;
using System.Xml;
namespace RobloxFiles.XmlFormat
using RobloxFiles.DataTypes;
using RobloxFiles.XmlFormat;
namespace RobloxFiles
{
public class XmlRobloxFile : RobloxFile
{
// Runtime Specific
public readonly XmlDocument Root = new XmlDocument();
public readonly XmlDocument XmlDocument = new XmlDocument();
internal Dictionary<string, Instance> Instances = new Dictionary<string, Instance>();
internal Dictionary<string, string> SharedStrings = new Dictionary<string, string>();
internal HashSet<string> SharedStrings = new HashSet<string>();
internal XmlRobloxFile()
private Dictionary<string, string> RawMetadata = new Dictionary<string, string>();
public Dictionary<string, string> Metadata => RawMetadata;
public XmlRobloxFile()
{
Name = "XmlRobloxFile";
ParentLocked = true;
Referent = "null";
}
protected override void ReadFile(byte[] buffer)
@ -27,14 +33,14 @@ namespace RobloxFiles.XmlFormat
try
{
string xml = Encoding.UTF8.GetString(buffer);
Root.LoadXml(xml);
XmlDocument.LoadXml(xml);
}
catch
{
throw new Exception("XmlRobloxFile: Could not read provided buffer as XML!");
}
XmlNode roblox = Root.FirstChild;
XmlNode roblox = XmlDocument.FirstChild;
if (roblox != null && roblox.Name == "roblox")
{
@ -59,6 +65,10 @@ namespace RobloxFiles.XmlFormat
{
XmlRobloxFileReader.ReadSharedStrings(child, this);
}
else if (child.Name == "Meta")
{
XmlRobloxFileReader.ReadMetadata(child, this);
}
}
// Query the properties.
@ -71,7 +81,8 @@ namespace RobloxFiles.XmlFormat
foreach (Property refProp in refProps)
{
string refId = refProp.Value as string;
string refId = refProp.XmlToken;
refProp.XmlToken = "Ref";
if (Instances.ContainsKey(refId))
{
@ -86,26 +97,13 @@ namespace RobloxFiles.XmlFormat
}
}
// Resolve shared strings.
// Record shared strings.
var sharedProps = allProps.Where(prop => prop.Type == PropertyType.SharedString);
foreach (Property sharedProp in sharedProps)
{
string md5 = sharedProp.Value as string;
if (SharedStrings.ContainsKey(md5))
{
string value = SharedStrings[md5];
sharedProp.Value = value;
byte[] data = Convert.FromBase64String(value);
sharedProp.RawBuffer = data;
}
else
{
string name = sharedProp.GetFullName();
Console.WriteLine("XmlRobloxFile: Could not resolve shared string for {0}", name);
}
SharedString shared = sharedProp.CastValue<SharedString>();
SharedStrings.Add(shared.MD5_Key);
}
}
else
@ -125,17 +123,32 @@ namespace RobloxFiles.XmlFormat
Instances.Clear();
SharedStrings.Clear();
// First, append the metadata
foreach (string key in Metadata.Keys)
{
string value = Metadata[key];
XmlElement meta = doc.CreateElement("Meta");
meta.SetAttribute("name", key);
meta.InnerText = value;
roblox.AppendChild(meta);
}
Instance[] children = GetChildren();
// First, record all of the instances.
// Record all of the instances.
foreach (Instance inst in children)
XmlRobloxFileWriter.RecordInstances(this, inst);
// Now append them into the document.
foreach (Instance inst in children)
{
XmlNode instNode = XmlRobloxFileWriter.WriteInstance(inst, doc, this);
roblox.AppendChild(instNode);
if (inst.Archivable)
{
XmlNode instNode = XmlRobloxFileWriter.WriteInstance(inst, doc, this);
roblox.AppendChild(instNode);
}
}
// Append the shared strings.