├── .gitattributes
├── .gitignore
├── BinaryFormat
├── BinaryFileChunk.cs
├── BinaryFileReader.cs
├── BinaryFileWriter.cs
├── BinaryRobloxFile.cs
└── Chunks
│ ├── INST.cs
│ ├── META.cs
│ ├── PRNT.cs
│ ├── PROP.cs
│ ├── SIGN.cs
│ └── SSTR.cs
├── DataTypes
├── Axes.cs
├── BrickColor.cs
├── CFrame.cs
├── Color3.cs
├── Color3uint8.cs
├── ColorSequence.cs
├── ColorSequenceKeypoint.cs
├── Content.cs
├── ContentId.cs
├── EulerAngles.cs
├── Faces.cs
├── FontFace.cs
├── NumberRange.cs
├── NumberSequence.cs
├── NumberSequenceKeypoint.cs
├── Optional.cs
├── PhysicalProperties.cs
├── ProtectedString.cs
├── Quaternion.cs
├── Ray.cs
├── Rect.cs
├── Region3.cs
├── Region3int16.cs
├── SecurityCapabilities.cs
├── SharedString.cs
├── UDim.cs
├── UDim2.cs
├── UniqueId.cs
├── Vector2.cs
├── Vector2int16.cs
├── Vector3.cs
└── Vector3int16.cs
├── FodyWeavers.xml
├── Generated
├── Classes.cs
└── Enums.cs
├── Interfaces
├── IAttributeToken.cs
├── IBinaryFileChunk.cs
└── IXmlPropertyToken.cs
├── LICENSE
├── Plugins
├── .vscode
│ └── settings.json
├── GenerateApiDump
│ ├── BrickColors.lua
│ ├── Formatting.lua
│ ├── LegacyFonts.lua
│ ├── LostEnumValues.lua
│ ├── PropertyPatches.lua
│ └── init.server.lua
├── Null.rbxlx
├── aftman.toml
├── default.project.json
├── make
└── sourcemap.json
├── Properties
└── AssemblyInfo.cs
├── README.md
├── RobloxFile.cs
├── RobloxFileFormat.csproj
├── RobloxFileFormat.dll
├── RobloxFileFormat.sln
├── Tokens
├── Axes.cs
├── BinaryString.cs
├── Boolean.cs
├── BrickColor.cs
├── CFrame.cs
├── Color3.cs
├── Color3uint8.cs
├── ColorSequence.cs
├── Content.cs
├── ContentId.cs
├── Double.cs
├── Enum.cs
├── Faces.cs
├── Float.cs
├── Font.cs
├── Int.cs
├── Int64.cs
├── NumberRange.cs
├── NumberSequence.cs
├── OptionalCFrame.cs
├── PhysicalProperties.cs
├── ProtectedString.cs
├── Ray.cs
├── Rect.cs
├── Ref.cs
├── SecurityCapabilities.cs
├── SharedString.cs
├── String.cs
├── UDim.cs
├── UDim2.cs
├── UniqueId.cs
├── Vector2.cs
├── Vector3.cs
└── Vector3int16.cs
├── Tree
├── Attributes.cs
├── Instance.cs
├── Property.cs
├── RbxObject.cs
└── Service.cs
├── UnitTest
├── Files
│ ├── Binary.rbxl
│ └── Xml.rbxlx
├── Program.cs
└── RobloxFileFormat.UnitTest.csproj
├── Utility
├── BrickColors.cs
├── DefaultProperty.cs
├── FontUtility.cs
├── Formatting.cs
├── ImplicitMember.cs
├── LostEnumValue.cs
├── MaterialInfo.cs
└── Specials.cs
├── XmlFormat
├── XmlFileReader.cs
├── XmlFileWriter.cs
├── XmlPropertyTokens.cs
└── XmlRobloxFile.cs
├── app.config
└── packages.config
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.rbxmx linguist-language=XML
2 | *.rbxlx linguist-language=XML
3 |
--------------------------------------------------------------------------------
/BinaryFormat/BinaryFileChunk.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using System.IO.Compression;
5 |
6 | using LZ4;
7 | using ZstdSharp;
8 |
9 | namespace RobloxFiles.BinaryFormat
10 | {
11 | ///
12 | /// BinaryRobloxFileChunk represents a generic LZ4-compressed chunk
13 | /// of data in Roblox's Binary File Format.
14 | ///
15 | public class BinaryRobloxFileChunk
16 | {
17 | public readonly string ChunkType;
18 | public readonly int Reserved;
19 |
20 | public readonly int CompressedSize;
21 | public readonly int Size;
22 |
23 | public readonly byte[] CompressedData;
24 | public readonly byte[] Data;
25 |
26 | public bool HasCompressedData => (CompressedSize > 0);
27 | public IBinaryFileChunk Handler { get; internal set; }
28 |
29 | public bool HasWriteBuffer { get; private set; }
30 | public byte[] WriteBuffer { get; private set; }
31 |
32 | public override string ToString()
33 | {
34 | string chunkType = ChunkType.Replace('\0', ' ');
35 | return $"'{chunkType}' Chunk ({Size} bytes) [{Handler?.ToString()}]";
36 | }
37 |
38 | public BinaryRobloxFileChunk(BinaryRobloxFileReader reader)
39 | {
40 | byte[] rawChunkType = reader.ReadBytes(4);
41 | ChunkType = Encoding.ASCII.GetString(rawChunkType);
42 |
43 | CompressedSize = reader.ReadInt32();
44 | Size = reader.ReadInt32();
45 | Reserved = reader.ReadInt32();
46 |
47 | if (HasCompressedData)
48 | {
49 | CompressedData = reader.ReadBytes(CompressedSize);
50 | Data = new byte[Size];
51 |
52 | using (var compStream = new MemoryStream(CompressedData))
53 | {
54 | Stream decompStream = null;
55 |
56 |
57 | if (CompressedData[0] == 0x78 || CompressedData[0] == 0x58)
58 | {
59 | // Probably zlib
60 | decompStream = new DeflateStream(compStream, CompressionMode.Decompress);
61 | }
62 | else if (BitConverter.ToString(CompressedData, 1, 3) == "B5-2F-FD")
63 | {
64 | // Probably zstd
65 | decompStream = new DecompressionStream(compStream);
66 | }
67 | else
68 | {
69 | // Probably LZ4
70 | var decomp = LZ4Codec.Decode(CompressedData, 0, CompressedSize, Size);
71 | decompStream = new MemoryStream(decomp);
72 | }
73 |
74 | if (decompStream == null)
75 | throw new Exception("Unsupported compression scheme!");
76 |
77 | decompStream.Read(Data, 0, Size);
78 | decompStream.Dispose();
79 | }
80 | }
81 | else
82 | {
83 | Data = reader.ReadBytes(Size);
84 | }
85 | }
86 |
87 | public BinaryRobloxFileChunk(BinaryRobloxFileWriter writer, bool compress = true)
88 | {
89 | if (!writer.WritingChunk)
90 | throw new Exception("BinaryRobloxFileChunk: Supplied writer must have WritingChunk set to true.");
91 |
92 | Stream stream = writer.BaseStream;
93 |
94 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, true))
95 | {
96 | long length = (stream.Position - writer.ChunkStart);
97 | stream.Position = writer.ChunkStart;
98 |
99 | Size = (int)length;
100 | Data = reader.ReadBytes(Size);
101 | }
102 |
103 | CompressedData = LZ4Codec.Encode(Data, 0, Size);
104 | CompressedSize = CompressedData.Length;
105 |
106 | if (!compress || CompressedSize > Size)
107 | {
108 | CompressedSize = 0;
109 | CompressedData = Array.Empty();
110 | }
111 |
112 | ChunkType = writer.ChunkType;
113 | Reserved = 0;
114 | }
115 |
116 | public void WriteChunk(BinaryRobloxFileWriter writer)
117 | {
118 | // Record where we are when we start writing.
119 | var stream = writer.BaseStream;
120 | long startPos = stream.Position;
121 |
122 | // Write the chunk's data.
123 | writer.WriteString(ChunkType, true);
124 |
125 | writer.Write(CompressedSize);
126 | writer.Write(Size);
127 |
128 | writer.Write(Reserved);
129 |
130 | if (CompressedSize > 0)
131 | writer.Write(CompressedData);
132 | else
133 | writer.Write(Data);
134 |
135 | // Capture the data we wrote into a byte[] array.
136 | long endPos = stream.Position;
137 | int length = (int)(endPos - startPos);
138 |
139 | using (MemoryStream buffer = new MemoryStream())
140 | {
141 | stream.Position = startPos;
142 | stream.CopyTo(buffer, length);
143 |
144 | WriteBuffer = buffer.ToArray();
145 | HasWriteBuffer = true;
146 | }
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/BinaryFormat/BinaryFileReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Collections.Generic;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 |
8 | namespace RobloxFiles.BinaryFormat
9 | {
10 | public class BinaryRobloxFileReader : BinaryReader
11 | {
12 | public readonly BinaryRobloxFile File;
13 | private byte[] lastStringBuffer = Array.Empty();
14 |
15 | public BinaryRobloxFileReader(BinaryRobloxFile file, Stream stream) : base(stream)
16 | {
17 | File = file;
18 | }
19 |
20 | // Reads 'count * sizeof(T)' interleaved bytes
21 | public T[] ReadInterleaved(int count, Func transform) where T : struct
22 | {
23 | int sizeof_T = Marshal.SizeOf();
24 | int blobSize = count * sizeof_T;
25 |
26 | var blob = ReadBytes(blobSize);
27 | var work = new byte[sizeof_T];
28 | var values = new T[count];
29 |
30 | for (int offset = 0; offset < count; offset++)
31 | {
32 | for (int i = 0; i < sizeof_T; i++)
33 | {
34 | int index = (i * count) + offset;
35 | work[sizeof_T - i - 1] = blob[index];
36 | }
37 |
38 | values[offset] = transform(work, 0);
39 | }
40 |
41 | return values;
42 | }
43 |
44 | // Rotates the sign bit of an int32 buffer.
45 | public int RotateInt32(byte[] buffer, int startIndex)
46 | {
47 | int value = BitConverter.ToInt32(buffer, startIndex);
48 | return (int)((uint)value >> 1) ^ (-(value & 1));
49 | }
50 |
51 | // Rotates the sign bit of an int64 buffer.
52 | public long RotateInt64(byte[] buffer, int startIndex)
53 | {
54 | long value = BitConverter.ToInt64(buffer, startIndex);
55 | return (long)((ulong)value >> 1) ^ (-(value & 1));
56 | }
57 |
58 | // Rotates the sign bit of a float buffer.
59 | public float RotateFloat(byte[] buffer, int startIndex)
60 | {
61 | uint u = BitConverter.ToUInt32(buffer, startIndex);
62 | uint i = (u >> 1) | (u << 31);
63 |
64 | byte[] b = BitConverter.GetBytes(i);
65 | return BitConverter.ToSingle(b, 0);
66 | }
67 |
68 | // Reads and accumulates an interleaved int32 buffer.
69 | public List ReadObjectIds(int count)
70 | {
71 | int[] values = ReadInterleaved(count, RotateInt32);
72 |
73 | for (int i = 1; i < count; ++i)
74 | values[i] += values[i - 1];
75 |
76 | return values.ToList();
77 | }
78 |
79 | public override string ReadString()
80 | {
81 | int length = ReadInt32();
82 | byte[] buffer = ReadBytes(length);
83 |
84 | lastStringBuffer = buffer;
85 | return Encoding.UTF8.GetString(buffer);
86 | }
87 |
88 | public float ReadFloat()
89 | {
90 | return ReadSingle();
91 | }
92 |
93 | public byte[] GetLastStringBuffer()
94 | {
95 | return lastStringBuffer;
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/BinaryFormat/Chunks/INST.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace RobloxFiles.BinaryFormat.Chunks
6 | {
7 | public class INST : IBinaryFileChunk
8 | {
9 | public int ClassIndex { get; internal set; }
10 | public string ClassName { get; internal set; }
11 |
12 | public bool IsService { get; internal set; }
13 | public List RootedServices { get; internal set; }
14 |
15 | public int NumObjects { get; internal set; }
16 | public List ObjectIds { get; internal set; }
17 |
18 | public override string ToString() => ClassName;
19 |
20 | public void Load(BinaryRobloxFileReader reader)
21 | {
22 | BinaryRobloxFile file = reader.File;
23 |
24 | ClassIndex = reader.ReadInt32();
25 | ClassName = reader.ReadString();
26 | IsService = reader.ReadBoolean();
27 |
28 | NumObjects = reader.ReadInt32();
29 | ObjectIds = reader.ReadObjectIds(NumObjects);
30 |
31 | Type instType = Type.GetType($"RobloxFiles.{ClassName}");
32 | file.Classes[ClassIndex] = this;
33 |
34 | if (instType == null)
35 | {
36 | RobloxFile.LogError($"INST - Unknown class: {ClassName} while reading INST chunk.");
37 | return;
38 | }
39 |
40 | if (IsService)
41 | {
42 | RootedServices = new List();
43 |
44 | for (int i = 0; i < NumObjects; i++)
45 | {
46 | bool isRooted = reader.ReadBoolean();
47 | RootedServices.Add(isRooted);
48 | }
49 | }
50 |
51 | for (int i = 0; i < NumObjects; i++)
52 | {
53 | int objId = ObjectIds[i];
54 |
55 | var obj = Activator.CreateInstance(instType) as RbxObject;
56 | obj.Referent = objId.ToString();
57 |
58 | if (obj is Instance inst)
59 | {
60 | if (IsService && inst.IsService)
61 | {
62 | var serviceInfo = Attribute.GetCustomAttribute(instType, typeof(RbxService)) as RbxService;
63 | bool isRooted = RootedServices[i];
64 |
65 | if (!isRooted && serviceInfo.IsRooted)
66 | // Service MUST be a child of the DataModel.
67 | isRooted = true;
68 |
69 | inst.Parent = (isRooted ? file : null);
70 | }
71 | }
72 |
73 | file.Objects[objId] = obj;
74 | }
75 | }
76 |
77 | public void Save(BinaryRobloxFileWriter writer)
78 | {
79 | writer.Write(ClassIndex);
80 | writer.WriteString(ClassName);
81 |
82 | writer.Write(IsService);
83 | writer.Write(NumObjects);
84 | writer.WriteObjectIds(ObjectIds);
85 |
86 | if (IsService)
87 | {
88 | var file = writer.File;
89 |
90 | foreach (int objId in ObjectIds)
91 | {
92 | RbxObject obj = file.Objects[objId];
93 |
94 | if (obj is Instance service)
95 | {
96 | writer.Write(service.Parent == file);
97 | continue;
98 | }
99 |
100 | writer.Write(false);
101 | }
102 | }
103 | }
104 |
105 | public void WriteInfo(StringBuilder builder)
106 | {
107 | builder.AppendLine($"- ClassIndex: {ClassIndex}");
108 | builder.AppendLine($"- ClassName: {ClassName}");
109 | builder.AppendLine($"- IsService: {IsService}");
110 |
111 | if (IsService && RootedServices != null)
112 | builder.AppendLine($"- RootedServices: `{string.Join(", ", RootedServices)}`");
113 |
114 | builder.AppendLine($"- NumObjects: {NumObjects}");
115 | builder.AppendLine($"- ObjectIds: `{string.Join(", ", ObjectIds)}`");
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/BinaryFormat/Chunks/META.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text;
3 |
4 | namespace RobloxFiles.BinaryFormat.Chunks
5 | {
6 | public class META : IBinaryFileChunk
7 | {
8 | public Dictionary Data = new Dictionary();
9 |
10 | public void Load(BinaryRobloxFileReader reader)
11 | {
12 | BinaryRobloxFile file = reader.File;
13 | int numEntries = reader.ReadInt32();
14 |
15 | for (int i = 0; i < numEntries; i++)
16 | {
17 | string key = reader.ReadString();
18 | string value = reader.ReadString();
19 | Data.Add(key, value);
20 | }
21 |
22 | file.META = this;
23 | }
24 |
25 | public void Save(BinaryRobloxFileWriter writer)
26 | {
27 | writer.Write(Data.Count);
28 |
29 | foreach (var pair in Data)
30 | {
31 | writer.WriteString(pair.Key);
32 | writer.WriteString(pair.Value);
33 | }
34 | }
35 |
36 | public void WriteInfo(StringBuilder builder)
37 | {
38 | builder.AppendLine($"- NumEntries: {Data.Count}");
39 |
40 | foreach (var pair in Data)
41 | {
42 | string key = pair.Key,
43 | value = pair.Value;
44 |
45 | builder.AppendLine($" - {key}: {value}");
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/BinaryFormat/Chunks/PRNT.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace RobloxFiles.BinaryFormat.Chunks
6 | {
7 | public class PRNT : IBinaryFileChunk
8 | {
9 | private const byte FORMAT = 0;
10 | private BinaryRobloxFile File;
11 |
12 | public void Load(BinaryRobloxFileReader reader)
13 | {
14 | BinaryRobloxFile file = reader.File;
15 | File = file;
16 |
17 | byte format = reader.ReadByte();
18 | int idCount = reader.ReadInt32();
19 |
20 | if (format != FORMAT)
21 | throw new Exception($"Unexpected PRNT format: {format} (expected {FORMAT}!)");
22 |
23 | var childIds = reader.ReadObjectIds(idCount);
24 | var parentIds = reader.ReadObjectIds(idCount);
25 |
26 | for (int i = 0; i < idCount; i++)
27 | {
28 | int childId = childIds[i];
29 | int parentId = parentIds[i];
30 |
31 | var child = file.Objects[childId] as Instance;
32 | var parent = (parentId >= 0 ? file.Objects[parentId] : file) as Instance;
33 |
34 | if (child == null)
35 | {
36 | RobloxFile.LogError($"PRNT: could not parent {childId} to {parentId} because child {childId} was null or not an Instance.");
37 | continue;
38 | }
39 |
40 | if (parentId >= 0 && parent == null)
41 | {
42 | RobloxFile.LogError($"PRNT: could not parent {childId} to {parentId} because parent {parentId} was null or not an Instance.");
43 | continue;
44 | }
45 |
46 | child.Parent = parent;
47 | }
48 | }
49 |
50 | public void Save(BinaryRobloxFileWriter writer)
51 | {
52 | var file = writer.File;
53 | File = file;
54 |
55 | var postInstances = writer.PostInstances;
56 | var idCount = postInstances.Count;
57 |
58 | var childIds = new List();
59 | var parentIds = new List();
60 |
61 | foreach (Instance inst in writer.PostInstances)
62 | {
63 | Instance parent = inst.Parent;
64 |
65 | int childId = int.Parse(inst.Referent);
66 | int parentId = -1;
67 |
68 | if (parent != null)
69 | parentId = int.Parse(parent.Referent);
70 |
71 | childIds.Add(childId);
72 | parentIds.Add(parentId);
73 | }
74 |
75 | writer.Write(FORMAT);
76 | writer.Write(idCount);
77 |
78 | writer.WriteObjectIds(childIds);
79 | writer.WriteObjectIds(parentIds);
80 | }
81 |
82 | public void WriteInfo(StringBuilder builder)
83 | {
84 | var childIds = new List();
85 | var parentIds = new List();
86 |
87 | foreach (Instance inst in File.GetDescendants())
88 | {
89 | Instance parent = inst.Parent;
90 |
91 | int childId = int.Parse(inst.Referent);
92 | int parentId = -1;
93 |
94 | if (parent != null)
95 | parentId = int.Parse(parent.Referent);
96 |
97 | childIds.Add(childId);
98 | parentIds.Add(parentId);
99 | }
100 |
101 | builder.AppendLine($"- Format: {FORMAT}");
102 | builder.AppendLine($"- ChildIds: {string.Join(", ", childIds)}");
103 | builder.AppendLine($"- ParentIds: {string.Join(", ", parentIds)}");
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/BinaryFormat/Chunks/SIGN.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace RobloxFiles.BinaryFormat.Chunks
5 | {
6 | public enum RbxSignatureType
7 | {
8 | Ed25519
9 | }
10 |
11 | public struct RbxSignature
12 | {
13 | public RbxSignatureType SignatureType;
14 | public long PublicKeyId;
15 | public byte[] Value;
16 | }
17 |
18 | public class SIGN : IBinaryFileChunk
19 | {
20 | public RbxSignature[] Signatures;
21 |
22 | public void Load(BinaryRobloxFileReader reader)
23 | {
24 | int numSignatures = reader.ReadInt32();
25 | Signatures = new RbxSignature[numSignatures];
26 |
27 | for (int i = 0; i < numSignatures; i++)
28 | {
29 | var signature = new RbxSignature
30 | {
31 | SignatureType = (RbxSignatureType)reader.ReadInt32(),
32 | PublicKeyId = reader.ReadInt64(),
33 | };
34 |
35 | var length = reader.ReadInt32();
36 | signature.Value = reader.ReadBytes(length);
37 | Signatures[i] = signature;
38 | }
39 |
40 | var file = reader.File;
41 | file.SIGN = this;
42 | }
43 |
44 | public void Save(BinaryRobloxFileWriter writer)
45 | {
46 | writer.Write(Signatures.Length);
47 |
48 | for (int i = 0; i < Signatures.Length; i++)
49 | {
50 | var signature = Signatures[i];
51 |
52 | writer.Write((int)signature.SignatureType);
53 | writer.Write(signature.PublicKeyId);
54 |
55 | writer.Write(signature.Value.Length);
56 | writer.Write(signature.Value);
57 | }
58 | }
59 |
60 | public void WriteInfo(StringBuilder builder)
61 | {
62 | int numSignatures = Signatures.Length;
63 | builder.AppendLine($"NumSignatures: {numSignatures}");
64 |
65 | for (int i = 0; i < numSignatures; i++)
66 | {
67 | var signature = Signatures[i];
68 | builder.AppendLine($"## Signature {i}");
69 |
70 | var version = Enum.GetName(typeof(RbxSignatureType), signature.SignatureType);
71 | builder.AppendLine($"- SignatureType: {version}");
72 |
73 | var publicKeyId = signature.PublicKeyId;
74 | builder.AppendLine($"- PublicKeyId: {publicKeyId}");
75 |
76 | var value = Convert.ToBase64String(signature.Value);
77 | builder.AppendLine($"- Value: {value}");
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/BinaryFormat/Chunks/SSTR.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | using RobloxFiles.DataTypes;
6 |
7 | namespace RobloxFiles.BinaryFormat.Chunks
8 | {
9 | public class SSTR : IBinaryFileChunk
10 | {
11 | private const int FORMAT = 0;
12 |
13 | internal Dictionary Lookup = new Dictionary();
14 | internal Dictionary Strings = new Dictionary();
15 |
16 | public void Load(BinaryRobloxFileReader reader)
17 | {
18 | BinaryRobloxFile file = reader.File;
19 |
20 | int format = reader.ReadInt32();
21 | int numHashes = reader.ReadInt32();
22 |
23 | if (format != FORMAT)
24 | throw new Exception($"Unexpected SSTR format: {format} (expected {FORMAT}!)");
25 |
26 | for (uint id = 0; id < numHashes; id++)
27 | {
28 | byte[] hash = reader.ReadBytes(16);
29 | string key = Convert.ToBase64String(hash);
30 |
31 | byte[] data = reader.ReadBuffer();
32 | SharedString value = SharedString.FromBuffer(data);
33 |
34 | Lookup[key] = id;
35 | Strings[id] = value;
36 | }
37 |
38 | file.SSTR = this;
39 | }
40 |
41 | public void Save(BinaryRobloxFileWriter writer)
42 | {
43 | writer.Write(FORMAT);
44 | writer.Write(Lookup.Count);
45 |
46 | foreach (var pair in Lookup)
47 | {
48 | string key = pair.Key;
49 |
50 | byte[] hash = Convert.FromBase64String(key);
51 | writer.Write(hash);
52 |
53 | SharedString value = Strings[pair.Value];
54 | byte[] buffer = SharedString.Find(value.Key);
55 |
56 | writer.Write(buffer.Length);
57 | writer.Write(buffer);
58 | }
59 | }
60 |
61 | public void WriteInfo(StringBuilder builder)
62 | {
63 | builder.AppendLine($"Format: {FORMAT}");
64 | builder.AppendLine($"NumStrings: {Lookup.Count}");
65 |
66 | builder.AppendLine($"## Keys");
67 |
68 | foreach (var pair in Lookup)
69 | {
70 | string key = pair.Key;
71 | builder.AppendLine($"- `{key}`");
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/DataTypes/Axes.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using RobloxFiles.Enums;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | [Flags]
7 | public enum Axes
8 | {
9 | X = 1 << Axis.X,
10 | Y = 1 << Axis.Y,
11 | Z = 1 << Axis.Z,
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/DataTypes/Color3.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public class Color3
6 | {
7 | public readonly float R, G, B;
8 | public override string ToString() => $"{R}, {G}, {B}";
9 |
10 | public Color3(float r = 0, float g = 0, float b = 0)
11 | {
12 | R = r;
13 | G = g;
14 | B = b;
15 | }
16 |
17 | public override int GetHashCode()
18 | {
19 | int r = R.GetHashCode(),
20 | g = G.GetHashCode(),
21 | b = B.GetHashCode();
22 |
23 | return r ^ g ^ b;
24 | }
25 |
26 | public override bool Equals(object obj)
27 | {
28 | if (!(obj is Color3 other))
29 | return false;
30 |
31 | if (!R.Equals(other.R))
32 | return false;
33 |
34 | if (!G.Equals(other.G))
35 | return false;
36 |
37 | if (!B.Equals(other.B))
38 | return false;
39 |
40 | return true;
41 | }
42 |
43 | public static Color3 FromRGB(uint r = 0, uint g = 0, uint b = 0)
44 | {
45 | return new Color3(r / 255f, g / 255f, b / 255f);
46 | }
47 |
48 | public static Color3 FromHSV(float h = 0, float s = 0, float v = 0)
49 | {
50 | int i = (int)Math.Min(5, Math.Floor(6.0 * h));
51 | float f = 6.0f * h - i;
52 |
53 | float m = v * (1.0f - (s));
54 | float n = v * (1.0f - (s * f));
55 | float k = v * (1.0f - (s * (1 - f)));
56 |
57 | switch (i)
58 | {
59 | case 0 : return new Color3(v, k, m);
60 | case 1 : return new Color3(n, v, m);
61 | case 2 : return new Color3(m, v, k);
62 | case 3 : return new Color3(m, n, v);
63 | case 4 : return new Color3(k, m, v);
64 | case 5 : return new Color3(v, m, n);
65 | default : return new Color3(0, 0, 0);
66 | }
67 | }
68 |
69 | public static float[] ToHSV(Color3 color)
70 | {
71 | float val = Math.Max(Math.Max(color.R, color.G), color.B);
72 |
73 | if (Math.Abs(val) < 0.001f)
74 | return new float[3] { 0, 0, 0 };
75 |
76 | float hue = Math.Min(Math.Min(color.R, color.G), color.B);
77 | float sat = (val - hue) / val;
78 |
79 | if (Math.Abs(sat) >= 0.001f)
80 | {
81 | Vector3 rgbN = val - new Vector3(color.R, color.G, color.B);
82 | rgbN /= (val - hue);
83 |
84 | if (color.R == val)
85 | hue = (color.G == hue) ? 5.0f + rgbN.Z : 1.0f - rgbN.Y;
86 | else if (color.G == val)
87 | hue = (color.B == hue) ? 1.0f + rgbN.X : 3.0f - rgbN.Z;
88 | else
89 | hue = (color.R == hue) ? 3.0f + rgbN.Y : 5.0f - rgbN.Z;
90 |
91 | hue /= 6.0f;
92 | }
93 |
94 | return new float[3] { hue, sat, val };
95 | }
96 |
97 | public Color3 Lerp(Color3 other, float alpha)
98 | {
99 | float r = (R + (other.R - R) * alpha);
100 | float g = (G + (other.G - G) * alpha);
101 | float b = (B + (other.B - B) * alpha);
102 |
103 | return new Color3(r, g, b);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/DataTypes/Color3uint8.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | ///
4 | /// Color3uint8 functions as an interconvertible storage medium for Color3 types.
5 | /// It is used by property types that want their Color3 value encoded with bytes instead of floats.
6 | ///
7 | public class Color3uint8
8 | {
9 | public readonly byte R, G, B;
10 | public override string ToString() => $"{R}, {G}, {B}";
11 |
12 | public Color3uint8(byte r = 0, byte g = 0, byte b = 0)
13 | {
14 | R = r;
15 | G = g;
16 | B = b;
17 | }
18 |
19 | public override int GetHashCode()
20 | {
21 | return (R << 16) | (G << 8) | B;
22 | }
23 |
24 | public override bool Equals(object obj)
25 | {
26 | if (!(obj is Color3uint8))
27 | return false;
28 |
29 | int rgb0 = GetHashCode(),
30 | rgb1 = obj.GetHashCode();
31 |
32 | return rgb0.Equals(rgb1);
33 | }
34 |
35 | public static implicit operator Color3(Color3uint8 color)
36 | {
37 | float r = color.R / 255f;
38 | float g = color.G / 255f;
39 | float b = color.B / 255f;
40 |
41 | return new Color3(r, g, b);
42 | }
43 |
44 | public static implicit operator Color3uint8(Color3 color)
45 | {
46 | byte r = (byte)(color.R * 255);
47 | byte g = (byte)(color.G * 255);
48 | byte b = (byte)(color.B * 255);
49 |
50 | return new Color3uint8(r, g, b);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/DataTypes/ColorSequence.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public class ColorSequence
6 | {
7 | public readonly ColorSequenceKeypoint[] Keypoints;
8 |
9 | public override string ToString()
10 | {
11 | return string.Join(" ", Keypoints);
12 | }
13 |
14 | public ColorSequence(float r, float g, float b) : this(new Color3(r, g, b))
15 | {
16 | }
17 |
18 | public ColorSequence(Color3 c) : this(c, c)
19 | {
20 | }
21 |
22 | public ColorSequence(Color3 c0, Color3 c1)
23 | {
24 | Keypoints = new ColorSequenceKeypoint[2]
25 | {
26 | new ColorSequenceKeypoint(0, c0),
27 | new ColorSequenceKeypoint(1, c1)
28 | };
29 | }
30 |
31 | public override int GetHashCode()
32 | {
33 | int hash = 0;
34 |
35 | foreach (var keypoint in Keypoints)
36 | hash ^= keypoint.GetHashCode();
37 |
38 | return hash;
39 | }
40 |
41 | public override bool Equals(object obj)
42 | {
43 | if (!(obj is ColorSequence colorSeq))
44 | return false;
45 |
46 | var otherKeys = colorSeq.Keypoints;
47 |
48 | if (Keypoints.Length != otherKeys.Length)
49 | return false;
50 |
51 | for (int i = 0; i < Keypoints.Length; i++)
52 | {
53 | var keyA = Keypoints[i];
54 | var keyB = otherKeys[i];
55 |
56 | if (keyA.Equals(keyB))
57 | continue;
58 |
59 | return false;
60 | }
61 |
62 | return true;
63 | }
64 |
65 | public ColorSequence(ColorSequenceKeypoint[] keypoints)
66 | {
67 | int numKeys = keypoints.Length;
68 |
69 | if (numKeys < 2)
70 | throw new Exception("ColorSequence: requires at least 2 keypoints");
71 | else if (numKeys > 20)
72 | throw new Exception("ColorSequence: table is too long.");
73 |
74 | for (int key = 1; key < numKeys; key++)
75 | if (keypoints[key - 1].Time > keypoints[key].Time)
76 | throw new Exception("ColorSequence: all keypoints must be ordered by time");
77 |
78 | var first = keypoints[0];
79 | var last = keypoints[numKeys - 1];
80 |
81 | if (!first.Time.FuzzyEquals(0))
82 | throw new Exception("ColorSequence must start at time=0.0");
83 |
84 | if (!last.Time.FuzzyEquals(1))
85 | throw new Exception("ColorSequence must end at time=1.0");
86 |
87 | Keypoints = keypoints;
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/DataTypes/ColorSequenceKeypoint.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class ColorSequenceKeypoint
4 | {
5 | public readonly float Time;
6 | public readonly Color3uint8 Value;
7 | public readonly int Envelope;
8 |
9 | public override string ToString()
10 | {
11 | Color3 Color = Value;
12 | return $"{Time} {Color.R} {Color.G} {Color.B} {Envelope}";
13 | }
14 |
15 | public ColorSequenceKeypoint(float time, Color3 value, int envelope = 0)
16 | {
17 | Time = time;
18 | Value = value;
19 | Envelope = envelope;
20 | }
21 |
22 | public override int GetHashCode()
23 | {
24 | int hash = Time.GetHashCode()
25 | ^ Value.GetHashCode()
26 | ^ Envelope.GetHashCode();
27 |
28 | return hash;
29 | }
30 |
31 | public override bool Equals(object obj)
32 | {
33 | if (!(obj is ColorSequenceKeypoint otherKey))
34 | return false;
35 |
36 | if (!Time.Equals(otherKey.Time))
37 | return false;
38 |
39 | if (!Value.Equals(otherKey.Value))
40 | return false;
41 |
42 | if (!Envelope.Equals(otherKey.Envelope))
43 | return false;
44 |
45 | return true;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/DataTypes/Content.cs:
--------------------------------------------------------------------------------
1 | using RobloxFiles.Enums;
2 | using System;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | public class Content
7 | {
8 | public readonly string Uri;
9 | public readonly ContentSourceType SourceType;
10 | public static readonly Content None = new Content();
11 |
12 | public RbxObject Object { get; internal set; }
13 | internal readonly string RefId = "";
14 |
15 | public Content()
16 | {
17 | SourceType = ContentSourceType.None;
18 | }
19 |
20 | public Content(string uri)
21 | {
22 | Uri = uri;
23 | SourceType = ContentSourceType.Uri;
24 | }
25 |
26 | public Content(RbxObject obj)
27 | {
28 | if (obj is Instance)
29 | {
30 | SourceType = ContentSourceType.None;
31 | }
32 | else
33 | {
34 | Object = obj;
35 | SourceType = ContentSourceType.Object;
36 | }
37 | }
38 |
39 | internal Content(RobloxFile file, string refId)
40 | {
41 | SourceType = ContentSourceType.Object;
42 | RefId = refId;
43 | }
44 |
45 | public override bool Equals(object obj)
46 | {
47 | if (!(obj is Content content))
48 | return false;
49 |
50 | if (SourceType != content.SourceType)
51 | return false;
52 | else if (SourceType == ContentSourceType.None)
53 | return true;
54 | else if (SourceType == ContentSourceType.Uri)
55 | return Uri?.Equals(content.Uri) ?? false;
56 |
57 | return Object?.Equals(content.Object) ?? false;
58 | }
59 |
60 | public override int GetHashCode()
61 | {
62 | if (SourceType == ContentSourceType.None)
63 | return 0;
64 | else if (SourceType == ContentSourceType.Uri)
65 | return Uri.GetHashCode();
66 |
67 | return Object.GetHashCode();
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/DataTypes/ContentId.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | // ContentId represents legacy Content properties which don't support object bindings.
4 | public class ContentId
5 | {
6 | public readonly string Url;
7 |
8 | public ContentId(string url)
9 | {
10 | Url = url;
11 | }
12 |
13 | public override int GetHashCode()
14 | {
15 | return Url.GetHashCode();
16 | }
17 |
18 | public override string ToString()
19 | {
20 | return Url;
21 | }
22 |
23 | public static implicit operator string(ContentId id)
24 | {
25 | return id.Url;
26 | }
27 |
28 | public static implicit operator ContentId(string url)
29 | {
30 | return new ContentId(url);
31 | }
32 |
33 | public static implicit operator Content(ContentId id)
34 | {
35 | return new Content(id.Url);
36 | }
37 |
38 | public static implicit operator ContentId(Content content)
39 | {
40 | if (content.SourceType == Enums.ContentSourceType.Uri)
41 | return content.Uri;
42 |
43 | return "";
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/DataTypes/EulerAngles.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public struct EulerAngles
4 | {
5 | public float Yaw;
6 | public float Pitch;
7 | public float Roll;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/DataTypes/Faces.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using RobloxFiles.Enums;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | [Flags]
7 | public enum Faces
8 | {
9 | Right = 1 << NormalId.Right,
10 | Top = 1 << NormalId.Top,
11 | Back = 1 << NormalId.Back,
12 | Left = 1 << NormalId.Left,
13 | Bottom = 1 << NormalId.Bottom,
14 | Front = 1 << NormalId.Front,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/DataTypes/FontFace.cs:
--------------------------------------------------------------------------------
1 | using RobloxFiles.Enums;
2 | using RobloxFiles.Utility;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | // Implementation of Roblox's FontFace datatype.
7 | // In Luau this type is named Font, but we avoid that name
8 | // to avoid ambiguity with System.Font and Roblox's Font enum.
9 |
10 | public class FontFace
11 | {
12 | public readonly ContentId Family = "rbxasset://fonts/families/LegacyArial.json";
13 | public readonly FontWeight Weight = FontWeight.Regular;
14 | public readonly FontStyle Style = FontStyle.Normal;
15 |
16 | // Roblox caches the asset of the font's face to make it
17 | // load faster. At runtime both the Family and the CachedFaceId
18 | // are loaded in parallel. If the CachedFaceId doesn't match with
19 | // the family file's face asset, then the correct one will be loaded late.
20 | // Setting this is not required, it's just a throughput optimization.
21 | public ContentId CachedFaceId { get; set; } = "";
22 |
23 | public FontFace(ContentId family, FontWeight weight = FontWeight.Regular, FontStyle style = FontStyle.Normal, string cachedFaceId = "")
24 | {
25 | CachedFaceId = cachedFaceId;
26 | Family = family;
27 | Weight = weight;
28 | Style = style;
29 | }
30 |
31 | public static FontFace FromEnum(Font font)
32 | {
33 | return FontUtility.FontFaces[font];
34 | }
35 |
36 | public static FontFace FromName(string name, FontWeight weight = FontWeight.Regular, FontStyle style = FontStyle.Normal)
37 | {
38 | ContentId url = $"rbxasset://fonts/families/{name}.json";
39 | return new FontFace(url, weight, style);
40 | }
41 |
42 | public static FontFace FromId(ulong id, FontWeight weight = FontWeight.Regular, FontStyle style = FontStyle.Normal)
43 | {
44 | ContentId url = $"rbxassetid://{id}";
45 | return new FontFace(url, weight, style);
46 | }
47 |
48 | public override string ToString()
49 | {
50 | return $"Font {{ Family = {Family}, Weight = {Weight}, Style = {Style}}}";
51 | }
52 |
53 | public override int GetHashCode()
54 | {
55 | int hash = Family.GetHashCode()
56 | ^ Weight.GetHashCode()
57 | ^ Style.GetHashCode();
58 |
59 | return hash;
60 | }
61 |
62 | public override bool Equals(object obj)
63 | {
64 | if (!(obj is FontFace font))
65 | return false;
66 |
67 | if (Family != font.Family)
68 | return false;
69 |
70 | if (Weight != font.Weight)
71 | return false;
72 |
73 | if (Style != font.Style)
74 | return false;
75 |
76 | return true;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/DataTypes/NumberRange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.Contracts;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | public class NumberRange
7 | {
8 | public readonly float Min;
9 | public readonly float Max;
10 |
11 | public override string ToString() => $"{Min} {Max}";
12 |
13 | public NumberRange(float num)
14 | {
15 | Min = num;
16 | Max = num;
17 | }
18 |
19 | public NumberRange(float min = 0, float max = 0)
20 | {
21 | Contract.Requires(max - min >= 0, "Max must be greater than min.");
22 | Contract.EndContractBlock();
23 |
24 | Min = min;
25 | Max = max;
26 | }
27 |
28 | public override int GetHashCode()
29 | {
30 | return Min.GetHashCode() ^ Max.GetHashCode();
31 | }
32 |
33 | public override bool Equals(object obj)
34 | {
35 | if (!(obj is NumberRange other))
36 | return false;
37 |
38 | if (!Min.Equals(other.Min))
39 | return false;
40 |
41 | if (!Max.Equals(other.Max))
42 | return false;
43 |
44 | return true;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/DataTypes/NumberSequence.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public class NumberSequence
6 | {
7 | public readonly NumberSequenceKeypoint[] Keypoints;
8 |
9 | public override string ToString()
10 | {
11 | return string.Join(" ", Keypoints);
12 | }
13 |
14 | public NumberSequence(float n)
15 | {
16 | NumberSequenceKeypoint a = new NumberSequenceKeypoint(0, n);
17 | NumberSequenceKeypoint b = new NumberSequenceKeypoint(1, n);
18 |
19 | Keypoints = new NumberSequenceKeypoint[2] { a, b };
20 | }
21 |
22 | public NumberSequence(float n0, float n1)
23 | {
24 | NumberSequenceKeypoint a = new NumberSequenceKeypoint(0, n0);
25 | NumberSequenceKeypoint b = new NumberSequenceKeypoint(1, n1);
26 |
27 | Keypoints = new NumberSequenceKeypoint[2] { a, b };
28 | }
29 |
30 | public NumberSequence(NumberSequenceKeypoint[] keypoints)
31 | {
32 | int numKeys = keypoints.Length;
33 |
34 | if (numKeys < 2)
35 | throw new Exception("NumberSequence: requires at least 2 keypoints");
36 | else if (numKeys > 20)
37 | throw new Exception("NumberSequence: table is too long.");
38 |
39 | for (int key = 1; key < numKeys; key++)
40 | if (keypoints[key - 1].Time > keypoints[key].Time)
41 | throw new Exception("NumberSequence: all keypoints must be ordered by time");
42 |
43 | var first = keypoints[0];
44 | var last = keypoints[numKeys - 1];
45 |
46 | if (!first.Time.FuzzyEquals(0))
47 | throw new Exception("NumberSequence must start at time=0.0");
48 |
49 | if (!last.Time.FuzzyEquals(1))
50 | throw new Exception("NumberSequence must end at time=1.0");
51 |
52 | Keypoints = keypoints;
53 | }
54 |
55 | public override int GetHashCode()
56 | {
57 | int hash = 0;
58 |
59 | foreach (var keypoint in Keypoints)
60 | hash ^= keypoint.GetHashCode();
61 |
62 | return hash;
63 | }
64 |
65 | public override bool Equals(object obj)
66 | {
67 | if (!(obj is NumberSequence numberSeq))
68 | return false;
69 |
70 | var otherKeys = numberSeq.Keypoints;
71 |
72 | if (Keypoints.Length != otherKeys.Length)
73 | return false;
74 |
75 | for (int i = 0; i < Keypoints.Length; i++)
76 | {
77 | var keyA = Keypoints[i];
78 | var keyB = otherKeys[i];
79 |
80 | if (keyA.Equals(keyB))
81 | continue;
82 |
83 | return false;
84 | }
85 |
86 | return true;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/DataTypes/NumberSequenceKeypoint.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class NumberSequenceKeypoint
4 | {
5 | public readonly float Time;
6 | public readonly float Value;
7 | public readonly float Envelope;
8 |
9 | public override string ToString()
10 | {
11 | return $"{Time} {Value} {Envelope}";
12 | }
13 |
14 | public NumberSequenceKeypoint(float time, float value, float envelope = 0)
15 | {
16 | Time = time;
17 | Value = value;
18 | Envelope = envelope;
19 | }
20 |
21 | public override int GetHashCode()
22 | {
23 | int hash = Time.GetHashCode()
24 | ^ Value.GetHashCode()
25 | ^ Envelope.GetHashCode();
26 |
27 | return hash;
28 | }
29 |
30 | public override bool Equals(object obj)
31 | {
32 | if (!(obj is NumberSequenceKeypoint otherKey))
33 | return false;
34 |
35 | if (!Time.Equals(otherKey.Time))
36 | return false;
37 |
38 | if (!Value.Equals(otherKey.Value))
39 | return false;
40 |
41 | if (!Envelope.Equals(otherKey.Envelope))
42 | return false;
43 |
44 | return true;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/DataTypes/Optional.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | // Optional represents a value that can be explicitly
6 | // marked as an optional variant to a specified type.
7 | // In practice this is used for OptionalCFrame.
8 |
9 | public struct Optional
10 | {
11 | public T Value;
12 | public bool HasValue => (Value != null);
13 |
14 | public Optional(T value)
15 | {
16 | Value = value;
17 | }
18 |
19 | public override string ToString()
20 | {
21 | return Value?.ToString() ?? "null";
22 | }
23 |
24 | public override int GetHashCode()
25 | {
26 | if (HasValue)
27 | return Value.GetHashCode();
28 |
29 | var T = typeof(T);
30 | return T.GetHashCode();
31 | }
32 |
33 | public override bool Equals(object obj)
34 | {
35 | if (!(obj is Optional optional))
36 | return false;
37 |
38 | if (HasValue != optional.HasValue)
39 | return false;
40 |
41 | if (HasValue)
42 | return Value.Equals(optional.Value);
43 |
44 | return true; // Both have no value.
45 | }
46 |
47 | public static implicit operator T(Optional optional)
48 | {
49 | if (optional.HasValue)
50 | return optional.Value;
51 |
52 | return default(T);
53 | }
54 |
55 | public static implicit operator Optional(T value)
56 | {
57 | return new Optional(value);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/DataTypes/PhysicalProperties.cs:
--------------------------------------------------------------------------------
1 | using RobloxFiles.Enums;
2 | using RobloxFiles.Utility;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | public class PhysicalProperties
7 | {
8 | public readonly float Density = 1.0f;
9 | public readonly float Friction = 1.0f;
10 | public readonly float Elasticity = 0.5f;
11 |
12 | public readonly float FrictionWeight = 1.0f;
13 | public readonly float ElasticityWeight = 1.0f;
14 |
15 | public override string ToString()
16 | {
17 | return $"{Density}, {Friction}, {Elasticity}, {FrictionWeight}, {ElasticityWeight}";
18 | }
19 |
20 | public PhysicalProperties(Material material)
21 | {
22 | var info = PhysicalPropertyData.Materials[material];
23 | ElasticityWeight = info.ElasticityWeight;
24 | FrictionWeight = info.FrictionWeight;
25 | Elasticity = info.Elasticity;
26 | Friction = info.Friction;
27 | Density = info.Density;
28 | }
29 |
30 | public PhysicalProperties(float density, float friction, float elasticity, float frictionWeight = 1f, float elasticityWeight = 1f)
31 | {
32 | Density = density;
33 | Friction = friction;
34 | Elasticity = elasticity;
35 | FrictionWeight = frictionWeight;
36 | ElasticityWeight = elasticityWeight;
37 | }
38 |
39 | public override int GetHashCode()
40 | {
41 | int hash = Density.GetHashCode()
42 | ^ Friction.GetHashCode()
43 | ^ Elasticity.GetHashCode()
44 | ^ FrictionWeight.GetHashCode()
45 | ^ ElasticityWeight.GetHashCode();
46 |
47 | return hash;
48 | }
49 |
50 | public override bool Equals(object obj)
51 | {
52 | if (!(obj is PhysicalProperties other))
53 | return false;
54 |
55 | if (!Density.Equals(other.Density))
56 | return false;
57 |
58 | if (!Friction.Equals(other.Friction))
59 | return false;
60 |
61 | if (!Elasticity.Equals(other.Elasticity))
62 | return false;
63 |
64 | if (!FrictionWeight.Equals(other.FrictionWeight))
65 | return false;
66 |
67 | if (!ElasticityWeight.Equals(other.ElasticityWeight))
68 | return false;
69 |
70 | return true;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/DataTypes/ProtectedString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | ///
7 | /// ProtectedString is a type used by the Source property of scripts.
8 | /// If constructed as an array of bytes, it's assumed to be compiled byte-code.
9 | ///
10 | public class ProtectedString
11 | {
12 | public readonly bool IsCompiled;
13 | public readonly byte[] RawBuffer;
14 |
15 | public override string ToString()
16 | {
17 | if (IsCompiled)
18 | return $"byte[{RawBuffer.Length}]";
19 |
20 | return Encoding.UTF8.GetString(RawBuffer);
21 | }
22 |
23 | public ProtectedString(string value)
24 | {
25 | IsCompiled = false;
26 | RawBuffer = Encoding.UTF8.GetBytes(value);
27 | }
28 |
29 | public ProtectedString(byte[] compiled)
30 | {
31 | // This'll break in the future if Luau ever has more than 32 VM versions.
32 | // Feels pretty unlikely this'll happen anytime soon, if ever.
33 |
34 | IsCompiled = true;
35 |
36 | if (compiled.Length > 0)
37 | if (compiled[0] >= 32)
38 | IsCompiled = false;
39 |
40 | RawBuffer = compiled;
41 | }
42 |
43 | public static implicit operator string(ProtectedString protectedString)
44 | {
45 | return Encoding.UTF8.GetString(protectedString.RawBuffer);
46 | }
47 |
48 | public static implicit operator ProtectedString(string value)
49 | {
50 | return new ProtectedString(value);
51 | }
52 |
53 | public static implicit operator byte[](ProtectedString protectedString)
54 | {
55 | return protectedString.RawBuffer;
56 | }
57 |
58 | public static implicit operator ProtectedString(byte[] value)
59 | {
60 | return new ProtectedString(value);
61 | }
62 |
63 | public override bool Equals(object obj)
64 | {
65 | if (!(obj is ProtectedString other))
66 | return false;
67 |
68 | var otherBuffer = other.RawBuffer;
69 |
70 | if (RawBuffer.Length != otherBuffer.Length)
71 | return false;
72 |
73 | for (int i = 0; i < RawBuffer.Length; i++)
74 | if (RawBuffer[i] != otherBuffer[i])
75 | return false;
76 |
77 | return true;
78 | }
79 |
80 | public override int GetHashCode()
81 | {
82 | var str = Convert.ToBase64String(RawBuffer);
83 | return str.GetHashCode();
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/DataTypes/Ray.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class Ray
4 | {
5 | public readonly Vector3 Origin;
6 | public readonly Vector3 Direction;
7 |
8 | public override string ToString() => $"{{{Origin}}}, {{{Direction}}}";
9 |
10 | public Ray Unit
11 | {
12 | get
13 | {
14 | Ray unit;
15 |
16 | if (Direction.Magnitude == 1.0f)
17 | unit = this;
18 | else
19 | unit = new Ray(Origin, Direction.Unit);
20 |
21 | return unit;
22 | }
23 | }
24 |
25 | public Ray(Vector3 origin = null, Vector3 direction = null)
26 | {
27 | Origin = origin ?? new Vector3();
28 | Direction = direction ?? new Vector3();
29 | }
30 |
31 | public Vector3 ClosestPoint(Vector3 point)
32 | {
33 | Vector3 result = Origin;
34 | float dist = Direction.Dot(point - result);
35 |
36 | if (dist >= 0)
37 | result += (Direction * dist);
38 |
39 | return result;
40 | }
41 |
42 | public float Distance(Vector3 point)
43 | {
44 | Vector3 closestPoint = ClosestPoint(point);
45 | return (point - closestPoint).Magnitude;
46 | }
47 |
48 | public override bool Equals(object obj)
49 | {
50 | if (!(obj is Ray other))
51 | return false;
52 |
53 | if (!Origin.Equals(other.Origin))
54 | return false;
55 |
56 | if (!Direction.Equals(other.Direction))
57 | return false;
58 |
59 | return true;
60 | }
61 |
62 | public override int GetHashCode()
63 | {
64 | int hash = Origin.GetHashCode()
65 | ^ Direction.GetHashCode();
66 |
67 | return hash;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/DataTypes/Rect.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class Rect
4 | {
5 | public readonly Vector2 Min;
6 | public readonly Vector2 Max;
7 |
8 | public float Width => (Max - Min).X;
9 | public float Height => (Max - Min).Y;
10 |
11 | public override string ToString() => $"{Min}, {Max}";
12 |
13 | public Rect(Vector2 min = null, Vector2 max = null)
14 | {
15 | Min = min ?? Vector2.zero;
16 | Max = max ?? Vector2.zero;
17 | }
18 |
19 | public Rect(float minX, float minY, float maxX, float maxY)
20 | {
21 | Min = new Vector2(minX, minY);
22 | Max = new Vector2(maxX, maxY);
23 | }
24 |
25 | public override int GetHashCode()
26 | {
27 | int hash = Min.GetHashCode()
28 | ^ Max.GetHashCode();
29 |
30 | return hash;
31 | }
32 |
33 | public override bool Equals(object obj)
34 | {
35 | if (!(obj is Rect other))
36 | return false;
37 |
38 | if (!Min.Equals(other.Min))
39 | return false;
40 |
41 | if (!Max.Equals(other.Max))
42 | return false;
43 |
44 | return true;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/DataTypes/Region3.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public class Region3
6 | {
7 | public readonly Vector3 Min, Max;
8 |
9 | public Vector3 Size => (Max - Min);
10 | public CFrame CFrame => new CFrame((Min + Max) / 2);
11 |
12 | public override string ToString() => $"{CFrame}; {Size}";
13 |
14 | public Region3(Vector3 min, Vector3 max)
15 | {
16 | Min = min;
17 | Max = max;
18 | }
19 |
20 | public Region3 ExpandToGrid(float resolution)
21 | {
22 | Vector3 emin = new Vector3
23 | (
24 | (float)Math.Floor(Min.X) * resolution,
25 | (float)Math.Floor(Min.Y) * resolution,
26 | (float)Math.Floor(Min.Z) * resolution
27 | );
28 |
29 | Vector3 emax = new Vector3
30 | (
31 | (float)Math.Floor(Max.X) * resolution,
32 | (float)Math.Floor(Max.Y) * resolution,
33 | (float)Math.Floor(Max.Z) * resolution
34 | );
35 |
36 | return new Region3(emin, emax);
37 | }
38 |
39 | public override int GetHashCode()
40 | {
41 | int hash = Min.GetHashCode()
42 | ^ Max.GetHashCode();
43 |
44 | return hash;
45 | }
46 |
47 | public override bool Equals(object obj)
48 | {
49 | if (!(obj is Region3 other))
50 | return false;
51 |
52 | if (!Min.Equals(other.Min))
53 | return false;
54 |
55 | if (!Max.Equals(other.Max))
56 | return false;
57 |
58 | return true;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/DataTypes/Region3int16.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class Region3int16
4 | {
5 | public readonly Vector3int16 Min, Max;
6 | public override string ToString() => $"{Min}; {Max}";
7 |
8 | public Region3int16(Vector3int16 min = null, Vector3int16 max = null)
9 | {
10 | Min = min ?? new Vector3int16();
11 | Max = max ?? new Vector3int16();
12 | }
13 |
14 | public override int GetHashCode()
15 | {
16 | int hash = Min.GetHashCode()
17 | ^ Max.GetHashCode();
18 |
19 | return hash;
20 | }
21 |
22 | public override bool Equals(object obj)
23 | {
24 | if (!(obj is Region3int16 other))
25 | return false;
26 |
27 | if (!Min.Equals(other.Min))
28 | return false;
29 |
30 | if (!Max.Equals(other.Max))
31 | return false;
32 |
33 | return true;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/DataTypes/SecurityCapabilities.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using RobloxFiles.Enums;
3 |
4 | namespace RobloxFiles
5 | {
6 | [Flags]
7 | public enum SecurityCapabilities : ulong
8 | {
9 | RunClientScript
10 | = 1 << SecurityCapability.RunClientScript,
11 |
12 | RunServerScript
13 | = 1 << SecurityCapability.RunServerScript,
14 |
15 | AccessOutsideWrite
16 | = 1 << SecurityCapability.AccessOutsideWrite,
17 |
18 | AssetRequire
19 | = 1 << SecurityCapability.AssetRequire,
20 |
21 | LoadString
22 | = 1 << SecurityCapability.LoadString,
23 |
24 | ScriptGlobals
25 | = 1 << SecurityCapability.ScriptGlobals,
26 |
27 | CreateInstances
28 | = 1 << SecurityCapability.CreateInstances,
29 |
30 | Basic
31 | = 1 << SecurityCapability.Basic,
32 |
33 | Audio
34 | = 1 << SecurityCapability.Audio,
35 |
36 | DataStore
37 | = 1 << SecurityCapability.DataStore,
38 |
39 | Network
40 | = 1 << SecurityCapability.Network,
41 |
42 | Physics
43 | = 1 << SecurityCapability.Physics,
44 |
45 | UI
46 | = 1 << SecurityCapability.UI,
47 |
48 | CSG
49 | = 1 << SecurityCapability.CSG,
50 |
51 | Chat
52 | = 1 << SecurityCapability.Chat,
53 |
54 | Animation
55 | = 1 << SecurityCapability.Animation,
56 |
57 | Avatar
58 | = 1 << SecurityCapability.Avatar,
59 |
60 | Input
61 | = 1 << SecurityCapability.Input,
62 |
63 | Environment
64 | = 1 << SecurityCapability.Environment,
65 |
66 | RemoteEvent
67 | = 1 << SecurityCapability.RemoteEvent,
68 |
69 | LegacySound
70 | = 1 << SecurityCapability.LegacySound,
71 |
72 | Players
73 | = 1 << SecurityCapability.Players,
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/DataTypes/SharedString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Collections.Generic;
4 | using System.Collections.Concurrent;
5 | using Konscious.Security.Cryptography;
6 |
7 | namespace RobloxFiles.DataTypes
8 | {
9 | // SharedString is a datatype that takes a sequence of bytes and stores it in a
10 | // lookup table that is shared by the entire file. It originally used MD5 for the
11 | // hashing, but Roblox now uses Blake2B to avoid the obvious problems with using MD5.
12 |
13 | // In practice the value of a SharedString does not have to match the hash of the
14 | // data it represents, it just needs to be distinct and MUST be 16 bytes long.
15 | // The XML format still uses 'md5' as its attribute key to the lookup table.
16 |
17 | public class SharedString
18 | {
19 | private static readonly ConcurrentDictionary Lookup = new ConcurrentDictionary();
20 | public string Key { get; internal set; }
21 | public string ComputedKey { get; internal set; }
22 |
23 | public byte[] SharedValue => Find(ComputedKey ?? Key);
24 | public override string ToString() => $"Key: {ComputedKey ?? Key}";
25 |
26 | ///
27 | /// Base64 encoding of: cae66941d9efbd404e4d88758ea67670
... which is
28 | /// the hex output of Blake2B when passing in an empty string.
29 | ///
30 | public static SharedString None => FromBase64("yuZpQdnvvUBOTYh1jqZ2cA==");
31 |
32 | public override int GetHashCode()
33 | {
34 | return Key.GetHashCode();
35 | }
36 |
37 | public override bool Equals(object obj)
38 | {
39 | if (!(obj is SharedString other))
40 | return false;
41 |
42 | return Key.Equals(other.Key);
43 | }
44 |
45 | internal SharedString(string key)
46 | {
47 | Key = key;
48 | }
49 |
50 | internal static void Register(string key, byte[] buffer)
51 | {
52 | if (Lookup.ContainsKey(key))
53 | return;
54 |
55 | Lookup.TryAdd(key, buffer);
56 | }
57 |
58 | private SharedString(byte[] buffer)
59 | {
60 | using (var blake2B = new HMACBlake2B(16 * 8))
61 | {
62 | byte[] hash = blake2B.ComputeHash(buffer);
63 | ComputedKey = Convert.ToBase64String(hash);
64 | Key = ComputedKey;
65 | }
66 |
67 | if (Lookup.ContainsKey(ComputedKey))
68 | return;
69 |
70 | Register(ComputedKey, buffer);
71 | }
72 |
73 | public static byte[] Find(string key)
74 | {
75 | byte[] result = null;
76 |
77 | if (Lookup.ContainsKey(key))
78 | result = Lookup[key];
79 |
80 | return result;
81 | }
82 |
83 | public static SharedString FromBuffer(byte[] buffer)
84 | {
85 | return new SharedString(buffer ?? Array.Empty());
86 | }
87 |
88 | public static SharedString FromString(string value)
89 | {
90 | byte[] buffer = Encoding.UTF8.GetBytes(value);
91 | return new SharedString(buffer);
92 | }
93 |
94 | public static SharedString FromBase64(string base64)
95 | {
96 | byte[] buffer = Convert.FromBase64String(base64);
97 | return new SharedString(buffer);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/DataTypes/UDim.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class UDim
4 | {
5 | public readonly float Scale;
6 | public readonly int Offset;
7 |
8 | public override string ToString() => $"{Scale}, {Offset}";
9 |
10 | public UDim(float scale = 0, int offset = 0)
11 | {
12 | Scale = scale;
13 | Offset = offset;
14 | }
15 |
16 | public static UDim operator+(UDim a, UDim b)
17 | {
18 | return new UDim(a.Scale + b.Scale, a.Offset + b.Offset);
19 | }
20 |
21 | public static UDim operator-(UDim a, UDim b)
22 | {
23 | return new UDim(a.Scale - b.Scale, a.Offset - b.Offset);
24 | }
25 |
26 | public override int GetHashCode()
27 | {
28 | int hash = Scale.GetHashCode()
29 | ^ Offset.GetHashCode();
30 |
31 | return hash;
32 | }
33 |
34 | public override bool Equals(object obj)
35 | {
36 | if (!(obj is UDim other))
37 | return false;
38 |
39 | if (!Scale.Equals(other.Scale))
40 | return false;
41 |
42 | if (!Offset.Equals(other.Offset))
43 | return false;
44 |
45 | return true;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/DataTypes/UDim2.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles.DataTypes
2 | {
3 | public class UDim2
4 | {
5 | public readonly UDim X, Y;
6 | public override string ToString() => $"{{{X}}},{{{Y}}}";
7 |
8 | public UDim Width => X;
9 | public UDim Height => Y;
10 |
11 | public UDim2(float scaleX = 0, int offsetX = 0, float scaleY = 0, int offsetY = 0)
12 | {
13 | X = new UDim(scaleX, offsetX);
14 | Y = new UDim(scaleY, offsetY);
15 | }
16 |
17 | public UDim2(UDim x, UDim y)
18 | {
19 | X = x;
20 | Y = y;
21 | }
22 |
23 | public UDim2 Lerp(UDim2 other, float alpha)
24 | {
25 | float scaleX = X.Scale + ((other.X.Scale - X.Scale) * alpha);
26 | int offsetX = X.Offset + (int)((other.X.Offset - X.Offset) * alpha);
27 |
28 | float scaleY = Y.Scale + ((other.Y.Scale - Y.Scale) * alpha);
29 | int offsetY = Y.Offset + (int)((other.Y.Offset - Y.Offset) * alpha);
30 |
31 | return new UDim2(scaleX, offsetX, scaleY, offsetY);
32 | }
33 |
34 | public override int GetHashCode()
35 | {
36 | int hash = X.GetHashCode()
37 | ^ Y.GetHashCode();
38 |
39 | return hash;
40 | }
41 |
42 | public override bool Equals(object obj)
43 | {
44 | if (!(obj is UDim2 other))
45 | return false;
46 |
47 | if (!X.Equals(other.X))
48 | return false;
49 |
50 | if (!Y.Equals(other.Y))
51 | return false;
52 |
53 | return true;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/DataTypes/UniqueId.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public struct UniqueId
6 | {
7 | public readonly uint Time;
8 | public readonly uint Index;
9 | public readonly long Random;
10 |
11 | public UniqueId(long random, uint time, uint index)
12 | {
13 | Time = time;
14 | Index = index;
15 | Random = random;
16 | }
17 |
18 | public override string ToString()
19 | {
20 | string random = Random.ToString("x2").PadLeft(16, '0');
21 | string index = Index.ToString("x2").PadLeft(8, '0');
22 | string time = Time.ToString("x2").PadLeft(8, '0');
23 |
24 | return $"{random}{time}{index}";
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/DataTypes/Vector2.cs:
--------------------------------------------------------------------------------
1 | #pragma warning disable IDE1006 // Naming Styles
2 | using System;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 |
7 | public class Vector2
8 | {
9 | public readonly float X, Y;
10 | public override string ToString() => $"{X}, {Y}";
11 |
12 | public Vector2(float x = 0, float y = 0)
13 | {
14 | X = x;
15 | Y = y;
16 | }
17 |
18 | public Vector2(params float[] coords)
19 | {
20 | X = coords.Length > 0 ? coords[0] : 0;
21 | Y = coords.Length > 1 ? coords[1] : 0;
22 | }
23 |
24 | public float Magnitude => (float)Math.Sqrt(X*X + Y*Y);
25 | public Vector2 Unit => this / Magnitude;
26 |
27 | private delegate Vector2 Operator(Vector2 a, Vector2 b);
28 |
29 | private static Vector2 UpcastFloatOp(Vector2 vec, float num, Operator upcast)
30 | {
31 | var numVec = new Vector2(num, num);
32 | return upcast(vec, numVec);
33 | }
34 |
35 | private static Vector2 UpcastFloatOp(float num, Vector2 vec, Operator upcast)
36 | {
37 | var numVec = new Vector2(num, num);
38 | return upcast(numVec, vec);
39 | }
40 |
41 | private static readonly Operator add = new Operator((a, b) => new Vector2(a.X + b.X, a.Y + b.Y));
42 | private static readonly Operator sub = new Operator((a, b) => new Vector2(a.X - b.X, a.Y - b.Y));
43 | private static readonly Operator mul = new Operator((a, b) => new Vector2(a.X * b.X, a.Y * b.Y));
44 | private static readonly Operator div = new Operator((a, b) => new Vector2(a.X / b.X, a.Y / b.Y));
45 |
46 | public static Vector2 operator +(Vector2 a, Vector2 b) => add(a, b);
47 | public static Vector2 operator +(Vector2 v, float n) => UpcastFloatOp(v, n, add);
48 | public static Vector2 operator +(float n, Vector2 v) => UpcastFloatOp(n, v, add);
49 |
50 | public static Vector2 operator -(Vector2 a, Vector2 b) => sub(a, b);
51 | public static Vector2 operator -(Vector2 v, float n) => UpcastFloatOp(v, n, sub);
52 | public static Vector2 operator -(float n, Vector2 v) => UpcastFloatOp(n, v, sub);
53 |
54 | public static Vector2 operator *(Vector2 a, Vector2 b) => mul(a, b);
55 | public static Vector2 operator *(Vector2 v, float n) => UpcastFloatOp(v, n, mul);
56 | public static Vector2 operator *(float n, Vector2 v) => UpcastFloatOp(n, v, mul);
57 |
58 | public static Vector2 operator /(Vector2 a, Vector2 b) => div(a, b);
59 | public static Vector2 operator /(Vector2 v, float n) => UpcastFloatOp(v, n, div);
60 | public static Vector2 operator /(float n, Vector2 v) => UpcastFloatOp(n, v, div);
61 |
62 | public static Vector2 operator -(Vector2 v) => new Vector2(-v.X, -v.Y);
63 |
64 | public static Vector2 zero => new Vector2(0, 0);
65 | public static Vector2 one => new Vector2(1, 1);
66 |
67 | public static Vector2 xAxis => new Vector2(1, 0);
68 | public static Vector2 yAxis => new Vector2(0, 1);
69 |
70 | public float Dot(Vector2 other) => (X * other.X) + (Y * other.Y);
71 | public Vector2 Cross(Vector2 other) => new Vector2(X * other.Y, Y * other.X);
72 |
73 | public Vector2 Lerp(Vector2 other, float t)
74 | {
75 | return this + (other - this) * t;
76 | }
77 |
78 | public override int GetHashCode()
79 | {
80 | int hash = X.GetHashCode()
81 | ^ Y.GetHashCode();
82 |
83 | return hash;
84 | }
85 |
86 | public override bool Equals(object obj)
87 | {
88 | if (!(obj is Vector2 other))
89 | return false;
90 |
91 | if (!X.Equals(other.X))
92 | return false;
93 |
94 | if (!Y.Equals(other.Y))
95 | return false;
96 |
97 | return true;
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/DataTypes/Vector2int16.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public class Vector2int16
6 | {
7 | public readonly short X, Y;
8 | public override string ToString() => $"{X}, {Y}";
9 |
10 | public Vector2int16(short x = 0, short y = 0)
11 | {
12 | X = x;
13 | Y = y;
14 | }
15 |
16 | public Vector2int16(int x = 0, int y = 0)
17 | {
18 | X = (short)x;
19 | Y = (short)y;
20 | }
21 |
22 | private delegate Vector2int16 Operator(Vector2int16 a, Vector2int16 b);
23 |
24 | private static Vector2int16 upcastShortOp(Vector2int16 vec, short num, Operator upcast)
25 | {
26 | Vector2int16 numVec = new Vector2int16(num, num);
27 | return upcast(vec, numVec);
28 | }
29 |
30 | private static Vector2int16 upcastShortOp(short num, Vector2int16 vec, Operator upcast)
31 | {
32 | Vector2int16 numVec = new Vector2int16(num, num);
33 | return upcast(numVec, vec);
34 | }
35 |
36 | private static readonly Operator add = new Operator((a, b) => new Vector2int16(a.X + b.X, a.Y + b.Y));
37 | private static readonly Operator sub = new Operator((a, b) => new Vector2int16(a.X - b.X, a.Y - b.Y));
38 | private static readonly Operator mul = new Operator((a, b) => new Vector2int16(a.X * b.X, a.Y * b.Y));
39 | private static readonly Operator div = new Operator((a, b) =>
40 | {
41 | if (b.X == 0 || b.Y == 0)
42 | throw new DivideByZeroException();
43 |
44 | return new Vector2int16(a.X / b.X, a.Y / b.Y);
45 | });
46 |
47 | public static Vector2int16 operator +(Vector2int16 a, Vector2int16 b) => add(a, b);
48 | public static Vector2int16 operator +(Vector2int16 v, short n) => upcastShortOp(v, n, add);
49 | public static Vector2int16 operator +(short n, Vector2int16 v) => upcastShortOp(n, v, add);
50 |
51 | public static Vector2int16 operator -(Vector2int16 a, Vector2int16 b) => sub(a, b);
52 | public static Vector2int16 operator -(Vector2int16 v, short n) => upcastShortOp(v, n, sub);
53 | public static Vector2int16 operator -(short n, Vector2int16 v) => upcastShortOp(n, v, sub);
54 |
55 | public static Vector2int16 operator *(Vector2int16 a, Vector2int16 b) => mul(a, b);
56 | public static Vector2int16 operator *(Vector2int16 v, short n) => upcastShortOp(v, n, mul);
57 | public static Vector2int16 operator *(short n, Vector2int16 v) => upcastShortOp(n, v, mul);
58 |
59 | public static Vector2int16 operator /(Vector2int16 a, Vector2int16 b) => div(a, b);
60 | public static Vector2int16 operator /(Vector2int16 v, short n) => upcastShortOp(v, n, div);
61 | public static Vector2int16 operator /(short n, Vector2int16 v) => upcastShortOp(n, v, div);
62 |
63 | public override int GetHashCode()
64 | {
65 | int hash = X.GetHashCode()
66 | ^ Y.GetHashCode();
67 |
68 | return hash;
69 | }
70 |
71 | public override bool Equals(object obj)
72 | {
73 | if (!(obj is Vector2int16 other))
74 | return false;
75 |
76 | if (!X.Equals(other.X))
77 | return false;
78 |
79 | if (!Y.Equals(other.Y))
80 | return false;
81 |
82 | return true;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/DataTypes/Vector3.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using RobloxFiles.Enums;
3 |
4 | namespace RobloxFiles.DataTypes
5 | {
6 | public class Vector3
7 | {
8 | public readonly float X, Y, Z;
9 | public override string ToString() => $"{X}, {Y}, {Z}";
10 |
11 | public float Magnitude
12 | {
13 | get
14 | {
15 | float product = Dot(this);
16 | double magnitude = Math.Sqrt(product);
17 |
18 | return (float)magnitude;
19 | }
20 | }
21 |
22 | public Vector3 Unit
23 | {
24 | get { return this / Magnitude; }
25 | }
26 |
27 | public Vector3(float x = 0, float y = 0, float z = 0)
28 | {
29 | X = x;
30 | Y = y;
31 | Z = z;
32 | }
33 |
34 | public Vector3(params float[] coords)
35 | {
36 | X = coords.Length > 0 ? coords[0] : 0;
37 | Y = coords.Length > 1 ? coords[1] : 0;
38 | Z = coords.Length > 2 ? coords[2] : 0;
39 | }
40 |
41 | public static Vector3 FromAxis(Axis axis)
42 | {
43 | float[] coords = new float[3] { 0f, 0f, 0f };
44 |
45 | int index = (int)axis;
46 | coords[index] = 1f;
47 |
48 | return new Vector3(coords);
49 | }
50 |
51 | public static Vector3 FromNormalId(NormalId normalId)
52 | {
53 | float[] coords = new float[3] { 0f, 0f, 0f };
54 |
55 | int index = (int)normalId;
56 | coords[index % 3] = (index > 2 ? -1f : 1f);
57 |
58 | return new Vector3(coords);
59 | }
60 |
61 | private delegate Vector3 Operator(Vector3 a, Vector3 b);
62 |
63 | private static Vector3 UpcastFloatOp(Vector3 vec, float num, Operator upcast)
64 | {
65 | Vector3 numVec = new Vector3(num, num, num);
66 | return upcast(vec, numVec);
67 | }
68 |
69 | private static Vector3 UpcastFloatOp(float num, Vector3 vec, Operator upcast)
70 | {
71 | Vector3 numVec = new Vector3(num, num, num);
72 | return upcast(numVec, vec);
73 | }
74 |
75 | private static readonly Operator add = (a, b) => new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
76 | private static readonly Operator sub = (a, b) => new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
77 | private static readonly Operator mul = (a, b) => new Vector3(a.X * b.X, a.Y * b.Y, a.Z * b.Z);
78 | private static readonly Operator div = (a, b) => new Vector3(a.X / b.X, a.Y / b.Y, a.Z / b.Z);
79 |
80 | public static Vector3 operator +(Vector3 a, Vector3 b) => add(a, b);
81 | public static Vector3 operator +(Vector3 v, float n) => UpcastFloatOp(v, n, add);
82 | public static Vector3 operator +(float n, Vector3 v) => UpcastFloatOp(n, v, add);
83 |
84 | public static Vector3 operator -(Vector3 a, Vector3 b) => sub(a, b);
85 | public static Vector3 operator -(Vector3 v, float n) => UpcastFloatOp(v, n, sub);
86 | public static Vector3 operator -(float n, Vector3 v) => UpcastFloatOp(n, v, sub);
87 |
88 | public static Vector3 operator *(Vector3 a, Vector3 b) => mul(a, b);
89 | public static Vector3 operator *(Vector3 v, float n) => UpcastFloatOp(v, n, mul);
90 | public static Vector3 operator *(float n, Vector3 v) => UpcastFloatOp(n, v, mul);
91 |
92 | public static Vector3 operator /(Vector3 a, Vector3 b) => div(a, b);
93 | public static Vector3 operator /(Vector3 v, float n) => UpcastFloatOp(v, n, div);
94 | public static Vector3 operator /(float n, Vector3 v) => UpcastFloatOp(n, v, div);
95 |
96 | public static Vector3 operator -(Vector3 v) => new Vector3(-v.X, -v.Y, -v.Z);
97 |
98 | public static readonly Vector3 zero = new Vector3(0, 0, 0);
99 | public static readonly Vector3 one = new Vector3(1, 1, 1);
100 |
101 | public static readonly Vector3 xAxis = new Vector3(1, 0, 0);
102 | public static readonly Vector3 yAxis = new Vector3(0, 1, 0);
103 | public static readonly Vector3 zAxis = new Vector3(0, 0, 1);
104 |
105 | public static Vector3 Right => xAxis;
106 | public static Vector3 Up => yAxis;
107 | public static Vector3 Back => zAxis;
108 |
109 | public float Dot(Vector3 other)
110 | {
111 | float dotX = X * other.X;
112 | float dotY = Y * other.Y;
113 | float dotZ = Z * other.Z;
114 |
115 | return dotX + dotY + dotZ;
116 | }
117 |
118 | public Vector3 Cross(Vector3 other)
119 | {
120 | float crossX = Y * other.Z - other.Y * Z;
121 | float crossY = Z * other.X - other.Z * X;
122 | float crossZ = X * other.Y - other.X * Y;
123 |
124 | return new Vector3(crossX, crossY, crossZ);
125 | }
126 |
127 | public Vector3 Lerp(Vector3 other, float t)
128 | {
129 | return this + (other - this) * t;
130 | }
131 |
132 | public bool IsClose(Vector3 other, float epsilon = 0.0f)
133 | {
134 | return (other - this).Magnitude <= Math.Abs(epsilon);
135 | }
136 |
137 | public int ToNormalId()
138 | {
139 | int result = -1;
140 |
141 | for (int i = 0; i < 6; i++)
142 | {
143 | NormalId normalId = (NormalId)i;
144 | Vector3 normal = FromNormalId(normalId);
145 |
146 | float dotProd = normal.Dot(this);
147 |
148 | if (dotProd.FuzzyEquals(1))
149 | {
150 | result = i;
151 | break;
152 | }
153 | }
154 |
155 | return result;
156 | }
157 |
158 | public override int GetHashCode()
159 | {
160 | int hash = X.GetHashCode()
161 | ^ Y.GetHashCode()
162 | ^ Z.GetHashCode();
163 |
164 | return hash;
165 | }
166 |
167 | public override bool Equals(object obj)
168 | {
169 | if (!(obj is Vector3 other))
170 | return false;
171 |
172 | if (!X.Equals(other.X))
173 | return false;
174 |
175 | if (!Y.Equals(other.Y))
176 | return false;
177 |
178 | if (!Z.Equals(other.Z))
179 | return false;
180 |
181 | return true;
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/DataTypes/Vector3int16.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles.DataTypes
4 | {
5 | public class Vector3int16
6 | {
7 | public readonly short X, Y, Z;
8 | public override string ToString() => $"{X}, {Y}, {Z}";
9 |
10 | public Vector3int16() : this(0, 0, 0)
11 | {
12 | }
13 |
14 | public Vector3int16(short x = 0, short y = 0, short z = 0)
15 | {
16 | X = x;
17 | Y = y;
18 | Z = z;
19 | }
20 |
21 | public Vector3int16(int x = 0, int y = 0, int z = 0)
22 | {
23 | X = (short)x;
24 | Y = (short)y;
25 | Z = (short)z;
26 | }
27 |
28 | private delegate Vector3int16 Operator(Vector3int16 a, Vector3int16 b);
29 |
30 | private static Vector3int16 upcastShortOp(Vector3int16 vec, short num, Operator upcast)
31 | {
32 | Vector3int16 numVec = new Vector3int16(num, num, num);
33 | return upcast(vec, numVec);
34 | }
35 |
36 | private static Vector3int16 upcastShortOp(short num, Vector3int16 vec, Operator upcast)
37 | {
38 | Vector3int16 numVec = new Vector3int16(num, num, num);
39 | return upcast(numVec, vec);
40 | }
41 |
42 | private static readonly Operator add = new Operator((a, b) => new Vector3int16(a.X + b.X, a.Y + b.Y, a.Z + b.Z));
43 | private static readonly Operator sub = new Operator((a, b) => new Vector3int16(a.X - b.X, a.Y - b.Y, a.Z - b.Z));
44 | private static readonly Operator mul = new Operator((a, b) => new Vector3int16(a.X * b.X, a.Y * b.Y, a.Z * b.Z));
45 | private static readonly Operator div = new Operator((a, b) =>
46 | {
47 | if (b.X == 0 || b.Y == 0 || b.Z == 0)
48 | throw new DivideByZeroException();
49 |
50 | return new Vector3int16(a.X / b.X, a.Y / b.Y, a.Z / b.Z);
51 | });
52 |
53 | public static Vector3int16 operator +(Vector3int16 a, Vector3int16 b) => add(a, b);
54 | public static Vector3int16 operator +(Vector3int16 v, short n) => upcastShortOp(v, n, add);
55 | public static Vector3int16 operator +(short n, Vector3int16 v) => upcastShortOp(n, v, add);
56 |
57 | public static Vector3int16 operator -(Vector3int16 a, Vector3int16 b) => sub(a, b);
58 | public static Vector3int16 operator -(Vector3int16 v, short n) => upcastShortOp(v, n, sub);
59 | public static Vector3int16 operator -(short n, Vector3int16 v) => upcastShortOp(n, v, sub);
60 |
61 | public static Vector3int16 operator *(Vector3int16 a, Vector3int16 b) => mul(a, b);
62 | public static Vector3int16 operator *(Vector3int16 v, short n) => upcastShortOp(v, n, mul);
63 | public static Vector3int16 operator *(short n, Vector3int16 v) => upcastShortOp(n, v, mul);
64 |
65 | public static Vector3int16 operator /(Vector3int16 a, Vector3int16 b) => div(a, b);
66 | public static Vector3int16 operator /(Vector3int16 v, short n) => upcastShortOp(v, n, div);
67 | public static Vector3int16 operator /(short n, Vector3int16 v) => upcastShortOp(n, v, div);
68 |
69 | public override int GetHashCode()
70 | {
71 | int hash = X.GetHashCode()
72 | ^ Y.GetHashCode()
73 | ^ Z.GetHashCode();
74 |
75 | return hash;
76 | }
77 |
78 | public override bool Equals(object obj)
79 | {
80 | if (!(obj is Vector3int16 other))
81 | return false;
82 |
83 | if (!X.Equals(other.X))
84 | return false;
85 |
86 | if (!Y.Equals(other.Y))
87 | return false;
88 |
89 | if (!Z.Equals(other.Z))
90 | return false;
91 |
92 | return true;
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Interfaces/IAttributeToken.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxFiles
2 | {
3 | public interface IAttributeToken
4 | {
5 | AttributeType AttributeType { get; }
6 |
7 | T ReadAttribute(RbxAttribute attribute);
8 | void WriteAttribute(RbxAttribute attribute, T value);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Interfaces/IBinaryFileChunk.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Text;
3 |
4 | namespace RobloxFiles.BinaryFormat
5 | {
6 | public interface IBinaryFileChunk
7 | {
8 | void Load(BinaryRobloxFileReader reader);
9 | void Save(BinaryRobloxFileWriter writer);
10 | void WriteInfo(StringBuilder builder);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Interfaces/IXmlPropertyToken.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 |
3 | namespace RobloxFiles.Tokens
4 | {
5 | public interface IXmlPropertyToken
6 | {
7 | string XmlPropertyToken { get; }
8 |
9 | bool ReadProperty(Property prop, XmlNode node);
10 | void WriteProperty(Property prop, XmlDocument doc, XmlNode node);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Max G. (CloneTrooper1019)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Plugins/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[lua]": {
3 | "editor.defaultFormatter": "JohnnyMorganz.stylua",
4 | "editor.formatOnSave": true
5 | }
6 | }
--------------------------------------------------------------------------------
/Plugins/GenerateApiDump/BrickColors.lua:
--------------------------------------------------------------------------------
1 | local BrickColors = {
2 | ById = {} :: {
3 | [number]: string
4 | },
5 |
6 | ByName = {} :: {
7 | [string]: string
8 | },
9 | }
10 |
11 | local lastValidId = -1
12 | local palette = false
13 | local dump = false
14 | local full = false
15 |
16 | local mapped = {} :: {
17 | [string]: true
18 | }
19 |
20 | for i = 1, 1032 do
21 | local color = BrickColor.new(i)
22 |
23 | if color.Number ~= i then
24 | continue
25 | end
26 |
27 | local name = color.Name
28 |
29 | local enum = name
30 | :gsub("[^A-z ]+", "")
31 | :gsub(" ", "_")
32 | :gsub("^_%l", string.upper)
33 |
34 | -- There are only 4 duo-pairs of colors with the same name,
35 | -- so we don't need to worry about incrementing a counter.
36 |
37 | if mapped[enum] then
38 | enum ..= "_2"
39 | end
40 |
41 | if dump then
42 | if full then
43 | print("{")
44 | print(`\tBrickColorId.{enum},`)
45 | print(`\tnew BrickColorInfo("{name}", 0x{color.Color:ToHex():upper()})`)
46 | print("},")
47 | else
48 | if i == lastValidId + 1 then
49 | print(`{enum},`)
50 | else
51 | print(`{enum} = {i},`)
52 | end
53 |
54 | lastValidId = i
55 | end
56 | end
57 |
58 | mapped[enum] = true
59 | BrickColors.ById[i] = enum
60 | BrickColors.ByName[name] = enum
61 | end
62 |
63 | if palette then
64 | for i = 0, 127 do
65 | local color = BrickColor.palette(i)
66 | local name = BrickColors.ById[color.Number]
67 | print(`BrickColorId.{name},`)
68 | end
69 | end
70 |
71 | return BrickColors
--------------------------------------------------------------------------------
/Plugins/GenerateApiDump/LegacyFonts.lua:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | local LegacyFonts = {} :: {
4 | [string]: Enum.Font,
5 | }
6 |
7 | for i, font: Enum.Font in Enum.Font:GetEnumItems() do
8 | if font ~= Enum.Font.Unknown then
9 | local fontFace = Font.fromEnum(font)
10 | LegacyFonts[tostring(fontFace)] = font
11 | end
12 | end
13 |
14 | return table.freeze(LegacyFonts)
15 |
--------------------------------------------------------------------------------
/Plugins/GenerateApiDump/LostEnumValues.lua:
--------------------------------------------------------------------------------
1 | type LostEnumValues = {
2 | [string]: {
3 | [string]: number
4 | }
5 | }
6 |
7 | return {
8 | BinType = {
9 | Slingshot = 5,
10 | Rocket = 6,
11 | Laser = 7,
12 | },
13 |
14 | InputType = {
15 | LeftTread = 1,
16 | RightTread = 2,
17 | Steer = 3,
18 | Throttle = 4,
19 | UpDown = 6,
20 | Action1 = 7,
21 | Action2 = 8,
22 | Action3 = 9,
23 | Action4 = 10,
24 | Action5 = 11,
25 | },
26 |
27 | SurfaceType = {
28 | Unjoinable = 9,
29 | },
30 | } :: LostEnumValues
31 |
--------------------------------------------------------------------------------
/Plugins/Null.rbxlx:
--------------------------------------------------------------------------------
1 |
2 | -
3 |
4 | DumpFolder
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Plugins/aftman.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | rojo = "rojo-rbx/rojo@7.3.0"
--------------------------------------------------------------------------------
/Plugins/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GenerateApiDump",
3 |
4 | "tree":
5 | {
6 | "$path": "GenerateApiDump"
7 | }
8 | }
--------------------------------------------------------------------------------
/Plugins/make:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rojo build --output $LOCALAPPDATA/Roblox/Plugins/GenerateApiDump.rbxm
--------------------------------------------------------------------------------
/Plugins/sourcemap.json:
--------------------------------------------------------------------------------
1 | {"name":"GenerateApiDump","className":"Script","filePaths":["GenerateApiDump\\init.server.lua","default.project.json"],"children":[{"name":"BrickColors","className":"ModuleScript","filePaths":["GenerateApiDump\\BrickColors.lua"]},{"name":"Formatting","className":"ModuleScript","filePaths":["GenerateApiDump\\Formatting.lua"]},{"name":"LegacyFonts","className":"ModuleScript","filePaths":["GenerateApiDump\\LegacyFonts.lua"]},{"name":"LostEnumValues","className":"ModuleScript","filePaths":["GenerateApiDump\\LostEnumValues.lua"]},{"name":"PropertyPatches","className":"ModuleScript","filePaths":["GenerateApiDump\\PropertyPatches.lua"]}]}
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("Roblox File Format")]
9 | [assembly: AssemblyDescription("Implementation of Roblox's File Format in C# for .NET 4.7.2")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("Written by MaximumADHD")]
12 | [assembly: AssemblyProduct("Roblox File Format")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Setting ComVisible to false makes the types in this assembly not visible
17 | // to COM components. If you need to access a type in this assembly from
18 | // COM, set the ComVisible attribute to true on that type.
19 | [assembly: ComVisible(false)]
20 |
21 | // The following GUID is for the ID of the typelib if this project is exposed to COM
22 | [assembly: Guid("cf50c0e2-23a7-4dc1-b4b2-e60cde716253")]
23 |
24 | // Version information for an assembly consists of the following four values:
25 | //
26 | // Major Version
27 | // Minor Version
28 | // Build Number
29 | // Revision
30 | //
31 | // You can specify all the values or you can default the Build and Revision Numbers
32 | // by using the '*' as shown below:
33 | // [assembly: AssemblyVersion("1.0.*")]
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Roblox-File-Format
2 | A C# library designed to make it easy to create and manipulate files in Roblox's model/place file format!
3 |
4 | # Usage
5 | The `RobloxFile` class is the main entry point for opening and saving files.
6 | You can provide one of three possible inputs to `RobloxFile.Open`:
7 |
8 | - A `string` containing the path to some `*.rbxl/.rbxlx` or `*.rbxm/*.rbxmx` to read from.
9 | - A `Stream` or `byte[]` to be read from directly.
10 |
11 | ```cs
12 | RobloxFile file = RobloxFile.Open(@"A:\Path\To\Some\File.rbxm");
13 |
14 | // Make some changes...
15 |
16 | file.Save(@"A:\Path\To\Some\NewFile.rbxm");
17 | ```
18 |
19 | Depending on the format being used by the file, it will return either a `BinaryRobloxFile` or an `XmlRobloxFile`, both of which derive from the `RobloxFile` class.
20 | At this time converting between Binary and XML is not directly supported, but in theory it shouldn't cause too many problems.
21 |
22 | ```cs
23 | if (file is BinaryRobloxFile)
24 | Console.WriteLine("This file used Roblox's binary format!");
25 | else
26 | Console.WriteLine("This file used Roblox's xml format!");
27 | ```
28 |
29 | This library contains a full implementation of Roblox's DOM, meaning that you can directly iterate over the Instance tree of the file as if you were writing code in Lua for Roblox!
30 | The `RobloxFile` class inherits from the provided `Instance` class in this library, serving as the root entry point to the contents of the file:
31 |
32 | ```cs
33 | foreach (Instance descendant in file.GetDescendants())
34 | Console.WriteLine(descendant.GetFullName());
35 | ```
36 |
37 | You can use type casting to read the properties specific to a derived class of Instance.
38 | Full type coverage is provided for all of Roblox's built-in types under the `RobloxFiles.DataTypes` namespace.
39 | Additionally, all of Roblox's enums are defined under the `RobloxFiles.Enums` namespace.
40 |
41 | ```cs
42 | Workspace workspace = file.FindFirstChildWhichIsA();
43 |
44 | if (workspace != null)
45 | {
46 | BasePart primary = workspace.PrimaryPart;
47 |
48 | if (primary != null)
49 | {
50 | primary.CFrame = new CFrame(1, 2, 3);
51 | primary.Size = new Vector3(4, 5, 6);
52 | }
53 |
54 | workspace.StreamingPauseMode = StreamingPauseMode.ClientPhysicsPause;
55 | Console.WriteLine($"Workspace.FilteringEnabled: {workspace.FilteringEnabled}");
56 | }
57 | ```
58 |
59 | Property values are populated upon opening a file through `Property` binding objects.
60 | The read-only dictionary `Instance.Properties` provides a lookup for these bindings, thus allowing for generic iteration over the properties of an Instance!
61 | For example, this function will count all distinct `Content` urls in the `Workspace` a given file:
62 | ```cs
63 | static void CountAssets(string path)
64 | {
65 | Console.WriteLine("Opening file...");
66 | RobloxFile target = RobloxFile.Open(path);
67 |
68 | var workspace = target.FindFirstChildOfClass();
69 | var assets = new HashSet();
70 |
71 | if (workspace == null)
72 | {
73 | Console.WriteLine("No workspace found!");
74 | Debugger.Break();
75 |
76 | return;
77 | }
78 |
79 | foreach (Instance inst in workspace.GetDescendants())
80 | {
81 | var instPath = inst.GetFullName();
82 | var props = inst.Properties;
83 |
84 | foreach (var prop in props)
85 | {
86 | Property binding = prop.Value;
87 | ContentId content = binding.CastValue();
88 |
89 | if (content != null)
90 | {
91 | string propName = prop.Key;
92 | string url = content.Url.Trim();
93 |
94 | var id = Regex
95 | .Match(url, pattern)?
96 | .Value;
97 |
98 | if (id != null && id.Length > 5)
99 | url = "rbxassetid://" + id;
100 |
101 | if (url.Length > 0 && !assets.Contains(url))
102 | {
103 | Console.WriteLine($"[{url}] at {instPath}.{propName}");
104 | assets.Add(url);
105 | }
106 | }
107 | }
108 | }
109 |
110 | Console.WriteLine("Done! Press any key to continue...");
111 | Console.Read();
112 | }
113 | ```
114 |
--------------------------------------------------------------------------------
/RobloxFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 |
6 | namespace RobloxFiles
7 | {
8 | ///
9 | /// Represents a loaded Roblox place/model file.
10 | /// RobloxFile is an Instance and its children are the contents of the file.
11 | ///
12 | public abstract class RobloxFile : Instance
13 | {
14 | public static bool LogErrors = false;
15 |
16 | protected abstract void ReadFile(byte[] buffer);
17 |
18 | ///
19 | /// Saves this RobloxFile to the provided stream.
20 | ///
21 | /// The stream to save to.
22 | public abstract void Save(Stream stream);
23 |
24 | ///
25 | /// Opens a RobloxFile using the provided buffer.
26 | ///
27 | /// The opened RobloxFile.
28 | public static RobloxFile Open(byte[] buffer)
29 | {
30 | if (buffer.Length > 14)
31 | {
32 | string header = Encoding.ASCII.GetString(buffer, 0, 8);
33 | RobloxFile file = null;
34 |
35 | if (header == "
51 | /// Opens a Roblox file by reading from a provided Stream.
52 | ///
53 | /// The stream to read the Roblox file from.
54 | /// The opened RobloxFile.
55 | public static RobloxFile Open(Stream stream)
56 | {
57 | byte[] buffer;
58 |
59 | using (MemoryStream memoryStream = new MemoryStream())
60 | {
61 | stream.CopyTo(memoryStream);
62 | buffer = memoryStream.ToArray();
63 | }
64 |
65 | return Open(buffer);
66 | }
67 |
68 | ///
69 | /// Opens a Roblox file from a provided file path.
70 | ///
71 | /// A path to a Roblox file to be opened.
72 | /// The opened RobloxFile.
73 | public static RobloxFile Open(string filePath)
74 | {
75 | byte[] buffer = File.ReadAllBytes(filePath);
76 | return Open(buffer);
77 | }
78 |
79 | ///
80 | /// Creates and runs a Task to open a Roblox file from a byte sequence that represents the file.
81 | ///
82 | /// A byte sequence that represents the file.
83 | /// A task which will complete once the file is opened with the resulting RobloxFile.
84 | public static Task OpenAsync(byte[] buffer)
85 | {
86 | return Task.Run(() => Open(buffer));
87 | }
88 |
89 | ///
90 | /// Creates and runs a Task to open a Roblox file using a provided Stream.
91 | ///
92 | /// The stream to read the Roblox file from.
93 | /// A task which will complete once the file is opened with the resulting RobloxFile.
94 | public static Task OpenAsync(Stream stream)
95 | {
96 | return Task.Run(() => Open(stream));
97 | }
98 |
99 | ///
100 | /// Opens a Roblox file from a provided file path.
101 | ///
102 | /// A path to a Roblox file to be opened.
103 | /// A task which will complete once the file is opened with the resulting RobloxFile.
104 | public static Task OpenAsync(string filePath)
105 | {
106 | return Task.Run(() => Open(filePath));
107 | }
108 |
109 | ///
110 | /// Saves this RobloxFile to the provided file path.
111 | ///
112 | /// A path to where the file should be saved.
113 | public void Save(string filePath)
114 | {
115 | using (FileStream stream = File.OpenWrite(filePath))
116 | {
117 | Save(stream);
118 | }
119 | }
120 |
121 | ///
122 | /// Asynchronously saves this RobloxFile to the provided stream.
123 | ///
124 | /// The stream to save to.
125 | /// A task which will complete upon the save's completion.
126 | public Task SaveAsync(Stream stream)
127 | {
128 | return Task.Run(() => Save(stream));
129 | }
130 |
131 | ///
132 | /// Asynchronously saves this RobloxFile to the provided file path.
133 | ///
134 | /// A path to where the file should be saved.
135 | /// A task which will complete upon the save's completion.
136 | public Task SaveAsync(string filePath)
137 | {
138 | return Task.Run(() => Save(filePath));
139 | }
140 |
141 | ///
142 | /// Logs an error that occurred while opening a RobloxFile if logs are enabled.
143 | ///
144 | ///
145 | internal static void LogError(string message)
146 | {
147 | if (!LogErrors)
148 | return;
149 |
150 | Console.Error.WriteLine(message);
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/RobloxFileFormat.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximumADHD/Roblox-File-Format/556fd674c5331e83f3b4e4be08fb7c53a1976c34/RobloxFileFormat.dll
--------------------------------------------------------------------------------
/RobloxFileFormat.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32929.385
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobloxFileFormat", "RobloxFileFormat.csproj", "{CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RobloxFileFormat.UnitTest", "UnitTest\RobloxFileFormat.UnitTest.csproj", "{E9FF1680-6FB9-41CD-9A73-7D072CB91118}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x64 = Debug|x64
14 | Release|Any CPU = Release|Any CPU
15 | Release|x64 = Release|x64
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Debug|x64.ActiveCfg = Debug|x64
21 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Debug|x64.Build.0 = Debug|x64
22 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Release|x64.ActiveCfg = Release|x64
25 | {CF50C0E2-23A7-4DC1-B4B2-E60CDE716253}.Release|x64.Build.0 = Release|x64
26 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Debug|x64.ActiveCfg = Debug|x64
29 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Debug|x64.Build.0 = Debug|x64
30 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Release|x64.ActiveCfg = Release|x64
33 | {E9FF1680-6FB9-41CD-9A73-7D072CB91118}.Release|x64.Build.0 = Release|x64
34 | EndGlobalSection
35 | GlobalSection(SolutionProperties) = preSolution
36 | HideSolutionNode = FALSE
37 | EndGlobalSection
38 | GlobalSection(ExtensibilityGlobals) = postSolution
39 | SolutionGuid = {D3301D5B-7D5A-429E-A2FC-AA83B2414EE3}
40 | EndGlobalSection
41 | EndGlobal
42 |
--------------------------------------------------------------------------------
/Tokens/Axes.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 |
3 | using RobloxFiles.DataTypes;
4 | using RobloxFiles.XmlFormat;
5 |
6 | namespace RobloxFiles.Tokens
7 | {
8 | public class AxesToken : IXmlPropertyToken
9 | {
10 | public string XmlPropertyToken => "Axes";
11 |
12 | public bool ReadProperty(Property prop, XmlNode token)
13 | {
14 | if (XmlPropertyTokens.ReadPropertyGeneric(token, out uint value))
15 | {
16 | Axes axes = (Axes)value;
17 | prop.Value = axes;
18 |
19 | return true;
20 | }
21 |
22 | return false;
23 | }
24 |
25 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
26 | {
27 | XmlElement axes = doc.CreateElement("axes");
28 | node.AppendChild(axes);
29 |
30 | int value = prop.CastValue();
31 | axes.InnerText = value.ToInvariantString();
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tokens/BinaryString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class BinaryStringToken : IXmlPropertyToken
7 | {
8 | public string XmlPropertyToken => "BinaryString";
9 |
10 | public bool ReadProperty(Property prop, XmlNode token)
11 | {
12 | // BinaryStrings are encoded in base64
13 | string base64 = token.InnerText.Replace("\n", "");
14 | byte[] buffer = Convert.FromBase64String(base64);
15 |
16 | prop.Value = buffer;
17 | prop.RawBuffer = buffer;
18 | prop.Type = PropertyType.String;
19 |
20 | return true;
21 | }
22 |
23 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
24 | {
25 | if (!prop.HasRawBuffer)
26 | return;
27 |
28 | byte[] data = prop.RawBuffer;
29 | string value = Convert.ToBase64String(data);
30 |
31 | if (value.Length > 72)
32 | {
33 | string buffer = "";
34 |
35 | while (value.Length > 72)
36 | {
37 | string chunk = value.Substring(0, 72);
38 | value = value.Substring(72);
39 | buffer += chunk + '\n';
40 | }
41 |
42 | value = buffer + value;
43 | }
44 |
45 | XmlCDataSection cdata = doc.CreateCDataSection(value);
46 | node.AppendChild(cdata);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tokens/Boolean.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.XmlFormat;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class BoolToken : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "bool";
9 | public AttributeType AttributeType => AttributeType.Bool;
10 |
11 | public bool ReadAttribute(RbxAttribute attr) => attr.ReadBool();
12 | public void WriteAttribute(RbxAttribute attr, bool value) => attr.WriteBool(value);
13 |
14 | public bool ReadProperty(Property prop, XmlNode token)
15 | {
16 | return XmlPropertyTokens.ReadPropertyGeneric(prop, PropertyType.Bool, token);
17 | }
18 |
19 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
20 | {
21 | string boolString = prop.Value
22 | .ToString()
23 | .ToLower();
24 |
25 | node.InnerText = boolString;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tokens/BrickColor.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 | using RobloxFiles.XmlFormat;
4 |
5 | namespace RobloxFiles.Tokens
6 | {
7 | public class BrickColorToken : IXmlPropertyToken, IAttributeToken
8 | {
9 | // This is a lie: The token is actually int, but that would cause a name collision.
10 | // Since BrickColors are written as ints, the IntToken class will try to redirect
11 | // to this handler if it believes that its representing a BrickColor.
12 | public string XmlPropertyToken => "BrickColor";
13 | public AttributeType AttributeType => AttributeType.BrickColor;
14 |
15 | public BrickColor ReadAttribute(RbxAttribute attr) => (BrickColorId)attr.ReadInt();
16 | public void WriteAttribute(RbxAttribute attr, BrickColor value) => attr.WriteInt(value.Number);
17 |
18 | public bool ReadProperty(Property prop, XmlNode token)
19 | {
20 | if (XmlPropertyTokens.ReadPropertyGeneric(token, out int value))
21 | {
22 | BrickColor brickColor = (BrickColorId)value;
23 | prop.XmlToken = "BrickColor";
24 | prop.Value = brickColor;
25 |
26 | return true;
27 | }
28 |
29 | return false;
30 | }
31 |
32 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
33 | {
34 | BrickColor value = prop.CastValue();
35 |
36 | XmlElement brickColor = doc.CreateElement("int");
37 | brickColor.InnerText = value.Number.ToInvariantString();
38 |
39 | brickColor.SetAttribute("name", prop.Name);
40 | brickColor.AppendChild(node);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tokens/CFrame.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Remoting.Messaging;
3 | using System.Xml;
4 | using RobloxFiles.DataTypes;
5 | using RobloxFiles.Enums;
6 |
7 | namespace RobloxFiles.Tokens
8 | {
9 | public class CFrameToken : IXmlPropertyToken, IAttributeToken
10 | {
11 | public string XmlPropertyToken => "CoordinateFrame; CFrame";
12 | public AttributeType AttributeType => AttributeType.CFrame;
13 |
14 | private static readonly string[] Coords = new string[12]
15 | {
16 | "X", "Y", "Z",
17 | "R00", "R01", "R02",
18 | "R10", "R11", "R12",
19 | "R20", "R21", "R22"
20 | };
21 |
22 | public static CFrame ReadCFrame(XmlNode token)
23 | {
24 | float[] components = new float[12];
25 |
26 | for (int i = 0; i < 12; i++)
27 | {
28 | string key = Coords[i];
29 |
30 | try
31 | {
32 | var coord = token[key];
33 | components[i] = Formatting.ParseFloat(coord.InnerText);
34 | }
35 | catch
36 | {
37 | return null;
38 | }
39 | }
40 |
41 | return new CFrame(components);
42 | }
43 |
44 | public static void WriteCFrame(CFrame cf, XmlDocument doc, XmlNode node)
45 | {
46 | float[] components = cf.GetComponents();
47 |
48 | for (int i = 0; i < 12; i++)
49 | {
50 | string coordName = Coords[i];
51 | float coordValue = components[i];
52 |
53 | XmlElement coord = doc.CreateElement(coordName);
54 | coord.InnerText = coordValue.ToInvariantString();
55 |
56 | node.AppendChild(coord);
57 | }
58 | }
59 |
60 | public bool ReadProperty(Property prop, XmlNode token)
61 | {
62 | CFrame result = ReadCFrame(token);
63 | bool success = (result != null);
64 |
65 | if (success)
66 | {
67 | prop.Type = PropertyType.CFrame;
68 | prop.Value = result;
69 | }
70 |
71 | return success;
72 | }
73 |
74 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
75 | {
76 | CFrame value = prop.Value as CFrame;
77 | WriteCFrame(value, doc, node);
78 | }
79 |
80 | public CFrame ReadAttribute(RbxAttribute attribute)
81 | {
82 | float x = attribute.ReadFloat(),
83 | y = attribute.ReadFloat(),
84 | z = attribute.ReadFloat();
85 |
86 | byte orientId = attribute.ReadByte();
87 | var pos = new Vector3(x, y, z);
88 |
89 | if (orientId > 0)
90 | {
91 | return CFrame.FromOrientId(orientId - 1) + pos;
92 | }
93 | else
94 | {
95 | float[] matrix = new float[12];
96 |
97 | for (int i = 3; i < 12; i++)
98 | matrix[i] = attribute.ReadFloat();
99 |
100 | return new CFrame(matrix) + pos;
101 | }
102 | }
103 |
104 | public void WriteAttribute(RbxAttribute attribute, CFrame value)
105 | {
106 | Vector3 pos = value.Position;
107 | Vector3Token.WriteVector3(attribute, pos);
108 |
109 | int orientId = value.GetOrientId();
110 | attribute.WriteByte((byte)(orientId + 1));
111 |
112 | if (orientId == -1)
113 | {
114 | float[] components = value.GetComponents();
115 |
116 | for (int i = 3; i < 12; i++)
117 | {
118 | float component = components[i];
119 | attribute.WriteFloat(component);
120 | }
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Tokens/Color3.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 | using RobloxFiles.XmlFormat;
4 |
5 | namespace RobloxFiles.Tokens
6 | {
7 | public class Color3Token : IXmlPropertyToken, IAttributeToken
8 | {
9 | public string XmlPropertyToken => "Color3";
10 | private readonly string[] XmlFields = new string[3] { "R", "G", "B" };
11 |
12 | public AttributeType AttributeType => AttributeType.Color3;
13 | public Color3 ReadAttribute(RbxAttribute attr) => ReadColor3(attr);
14 | public void WriteAttribute(RbxAttribute attr, Color3 value) => WriteColor3(attr, value);
15 |
16 | public static Color3 ReadColor3(RbxAttribute attr)
17 | {
18 | float r = attr.ReadFloat(),
19 | g = attr.ReadFloat(),
20 | b = attr.ReadFloat();
21 |
22 | return new Color3(r, g, b);
23 | }
24 |
25 | public static void WriteColor3(RbxAttribute attr, Color3 value)
26 | {
27 | attr.WriteFloat(value.R);
28 | attr.WriteFloat(value.G);
29 | attr.WriteFloat(value.B);
30 | }
31 |
32 | public bool ReadProperty(Property prop, XmlNode token)
33 | {
34 | bool success = true;
35 | float[] fields = new float[XmlFields.Length];
36 |
37 | for (int i = 0; i < fields.Length; i++)
38 | {
39 | string key = XmlFields[i];
40 |
41 | try
42 | {
43 | var coord = token[key];
44 | string text = coord?.InnerText;
45 |
46 | if (text == null)
47 | {
48 | text = "0";
49 | success = false;
50 | }
51 |
52 | fields[i] = Formatting.ParseFloat(text);
53 | }
54 | catch
55 | {
56 | success = false;
57 | break;
58 | }
59 | }
60 |
61 | if (success)
62 | {
63 | float r = fields[0],
64 | g = fields[1],
65 | b = fields[2];
66 |
67 | prop.Type = PropertyType.Color3;
68 | prop.Value = new Color3(r, g, b);
69 | }
70 | else
71 | {
72 | // Try falling back to the Color3uint8 technique...
73 | var color3uint8 = XmlPropertyTokens.GetHandler();
74 | success = color3uint8.ReadProperty(prop, token);
75 | }
76 |
77 | return success;
78 | }
79 |
80 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
81 | {
82 | Color3 color = prop.CastValue();
83 | float[] rgb = new float[3] { color.R, color.G, color.B };
84 |
85 | for (int i = 0; i < 3; i++)
86 | {
87 | string field = XmlFields[i];
88 | float value = rgb[i];
89 |
90 | XmlElement channel = doc.CreateElement(field);
91 | channel.InnerText = value.ToInvariantString();
92 |
93 | node.AppendChild(channel);
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Tokens/Color3uint8.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 | using RobloxFiles.XmlFormat;
4 | using System.Diagnostics.Contracts;
5 |
6 | namespace RobloxFiles.Tokens
7 | {
8 | public class Color3uint8Token : IXmlPropertyToken
9 | {
10 | public string XmlPropertyToken => "Color3uint8";
11 |
12 | public bool ReadProperty(Property prop, XmlNode token)
13 | {
14 | if (XmlPropertyTokens.ReadPropertyGeneric(token, out uint value))
15 | {
16 | uint r = (value >> 16) & 0xFF;
17 | uint g = (value >> 8) & 0xFF;
18 | uint b = value & 0xFF;
19 |
20 | Color3uint8 result = Color3.FromRGB(r, g, b);
21 | prop.Value = result;
22 |
23 | return true;
24 | }
25 |
26 | return false;
27 | }
28 |
29 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
30 | {
31 | Color3uint8 color = prop?.CastValue();
32 | Contract.Requires(node != null);
33 |
34 |
35 | uint r = color.R,
36 | g = color.G,
37 | b = color.B;
38 |
39 | uint rgb = (255u << 24) | (r << 16) | (g << 8) | b;
40 | node.InnerText = rgb.ToInvariantString();
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tokens/ColorSequence.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class ColorSequenceToken : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "ColorSequence";
9 | public AttributeType AttributeType => AttributeType.ColorSequence;
10 |
11 | public bool ReadProperty(Property prop, XmlNode token)
12 | {
13 | string contents = token.InnerText.Trim();
14 | string[] buffer = contents.Split(' ');
15 |
16 | int length = buffer.Length;
17 | bool valid = (length % 5 == 0);
18 |
19 | if (valid)
20 | {
21 | try
22 | {
23 | var keypoints = new ColorSequenceKeypoint[length / 5];
24 |
25 | for (int i = 0; i < length; i += 5)
26 | {
27 | float Time = Formatting.ParseFloat(buffer[i]);
28 |
29 | float R = Formatting.ParseFloat(buffer[i + 1]);
30 | float G = Formatting.ParseFloat(buffer[i + 2]);
31 | float B = Formatting.ParseFloat(buffer[i + 3]);
32 |
33 | Color3 Value = new Color3(R, G, B);
34 | keypoints[i / 5] = new ColorSequenceKeypoint(Time, Value);
35 | }
36 |
37 | prop.Type = PropertyType.ColorSequence;
38 | prop.Value = new ColorSequence(keypoints);
39 | }
40 | catch
41 | {
42 | valid = false;
43 | }
44 | }
45 |
46 | return valid;
47 | }
48 |
49 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
50 | {
51 | ColorSequence value = prop.CastValue();
52 | node.InnerText = value.ToString() + ' ';
53 | }
54 |
55 | public ColorSequence ReadAttribute(RbxAttribute attr)
56 | {
57 | int numKeys = attr.ReadInt();
58 | var keypoints = new ColorSequenceKeypoint[numKeys];
59 |
60 | for (int i = 0; i < numKeys; i++)
61 | {
62 | int envelope = attr.ReadInt();
63 | float time = attr.ReadFloat();
64 |
65 | Color3 value = Color3Token.ReadColor3(attr);
66 | keypoints[i] = new ColorSequenceKeypoint(time, value, envelope);
67 | }
68 |
69 | return new ColorSequence(keypoints);
70 | }
71 |
72 | public void WriteAttribute(RbxAttribute attr, ColorSequence value)
73 | {
74 | attr.WriteInt(value.Keypoints.Length);
75 |
76 | foreach (var keypoint in value.Keypoints)
77 | {
78 | attr.WriteInt(keypoint.Envelope);
79 | attr.WriteFloat(keypoint.Time);
80 |
81 | Color3Token.WriteColor3(attr, keypoint.Value);
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Tokens/Content.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 |
4 | using RobloxFiles.Enums;
5 | using RobloxFiles.DataTypes;
6 | using RobloxFiles.XmlFormat;
7 | using Newtonsoft.Json.Linq;
8 |
9 | namespace RobloxFiles.Tokens
10 | {
11 | public class ContentToken : IXmlPropertyToken
12 | {
13 | public string XmlPropertyToken => "Content";
14 |
15 | public bool ReadProperty(Property prop, XmlNode token)
16 | {
17 | var obj = prop.Object;
18 | var objType = obj.GetType();
19 | var objField = objType.GetField(prop.Name);
20 |
21 | if (objField != null && objField.FieldType.Name == "ContentId")
22 | {
23 | var contentIdToken = XmlPropertyTokens.GetHandler();
24 | return contentIdToken.ReadProperty(prop, token);
25 | }
26 | else
27 | {
28 | XmlNode childNode = token.FirstChild;
29 | string contentType = childNode.Name;
30 |
31 | if (contentType == "uri")
32 | prop.Value = new Content(token.InnerText);
33 | else if (contentType == "Ref")
34 | prop.Value = new Content(prop.File, token.InnerText);
35 | else
36 | prop.Value = Content.None;
37 |
38 | prop.Type = PropertyType.Content;
39 | return true;
40 | }
41 | }
42 |
43 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
44 | {
45 | var obj = prop.Object;
46 | var objType = obj.GetType();
47 | var objField = objType.GetField(prop.Name);
48 |
49 | if (objField != null && objField.FieldType.Name == "ContentId")
50 | {
51 | var contentIdToken = XmlPropertyTokens.GetHandler();
52 | contentIdToken.WriteProperty(prop, doc, node);
53 | }
54 | else
55 | {
56 | var content = prop.CastValue();
57 | string type = "null";
58 |
59 | if (content.SourceType == ContentSourceType.None)
60 | type = "null";
61 | else if (content.SourceType == ContentSourceType.Uri)
62 | type = "uri";
63 | else if (content.SourceType == ContentSourceType.Object)
64 | type = "Ref";
65 |
66 | XmlElement contentType = doc.CreateElement(type);
67 |
68 | if (content.SourceType == ContentSourceType.Uri)
69 | contentType.InnerText = content.Uri;
70 | else if (content.SourceType == ContentSourceType.Object)
71 | contentType.InnerText = content.Object.Referent;
72 |
73 | node.AppendChild(contentType);
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Tokens/ContentId.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 |
4 | using RobloxFiles.DataTypes;
5 |
6 | namespace RobloxFiles.Tokens
7 | {
8 | public class ContentIdToken : IXmlPropertyToken
9 | {
10 | // This is a lie, the token is "Content". A name collision between Content and ContentId.
11 | // The Content token will redirect here appropriately as needed.
12 | public string XmlPropertyToken => "ContentId";
13 |
14 | public bool ReadProperty(Property prop, XmlNode token)
15 | {
16 | string data = token.InnerText;
17 | prop.Value = new ContentId(data);
18 | prop.Type = PropertyType.String;
19 |
20 | if (token.HasChildNodes)
21 | {
22 | XmlNode childNode = token.FirstChild;
23 | string contentType = childNode.Name;
24 |
25 | if (contentType.StartsWith("binary") || contentType == "hash")
26 | {
27 | try
28 | {
29 | // Roblox technically doesn't support this anymore, but load it anyway :P
30 | byte[] buffer = Convert.FromBase64String(data);
31 | prop.RawBuffer = buffer;
32 | }
33 | catch
34 | {
35 | RobloxFile.LogError($"ContentToken: Got illegal base64 string: {data}");
36 | }
37 | }
38 | }
39 |
40 | return true;
41 | }
42 |
43 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
44 | {
45 | string content = prop.CastValue();
46 | string type = "null";
47 |
48 | if (prop.HasRawBuffer)
49 | type = "binary";
50 | else if (content.Length > 0)
51 | type = "url";
52 |
53 | XmlElement contentType = doc.CreateElement(type);
54 |
55 | if (type == "binary")
56 | {
57 | XmlCDataSection cdata = doc.CreateCDataSection(content);
58 | contentType.AppendChild(cdata);
59 | }
60 | else
61 | {
62 | contentType.InnerText = content;
63 | }
64 |
65 | XmlElement contentId = doc.CreateElement("Content");
66 | contentId.SetAttribute("name", prop.Name);
67 | contentId.AppendChild(contentType);
68 | contentId.AppendChild(node);
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/Tokens/Double.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.XmlFormat;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class DoubleToken : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "double";
9 | public AttributeType AttributeType => AttributeType.Double;
10 |
11 | public double ReadAttribute(RbxAttribute attr) => attr.ReadDouble();
12 | public void WriteAttribute(RbxAttribute attr, double value) => attr.WriteDouble(value);
13 |
14 | public bool ReadProperty(Property prop, XmlNode token)
15 | {
16 | return XmlPropertyTokens.ReadPropertyGeneric(prop, PropertyType.Double, token);
17 | }
18 |
19 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
20 | {
21 | double value = prop.CastValue();
22 | node.InnerText = value.ToInvariantString();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tokens/Enum.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.Contracts;
3 | using System.Xml;
4 |
5 | using RobloxFiles.Utility;
6 | using RobloxFiles.XmlFormat;
7 |
8 | namespace RobloxFiles.Tokens
9 | {
10 | public class EnumToken : IXmlPropertyToken, IAttributeToken
11 | {
12 | public string XmlPropertyToken => "token";
13 | public AttributeType AttributeType => AttributeType.Enum;
14 |
15 | public bool ReadProperty(Property prop, XmlNode token)
16 | {
17 | Contract.Requires(prop != null);
18 |
19 | if (XmlPropertyTokens.ReadPropertyGeneric(token, out uint value))
20 | {
21 | RbxObject obj = prop.Object;
22 | Type instType = obj?.GetType();
23 | var info = ImplicitMember.Get(instType, prop.Name);
24 |
25 | if (info != null)
26 | {
27 | Type enumType = info.MemberType;
28 | string item = value.ToInvariantString();
29 |
30 | prop.Type = PropertyType.Enum;
31 | prop.Value = Enum.Parse(enumType, item);
32 |
33 | return true;
34 | }
35 | }
36 |
37 | return false;
38 | }
39 |
40 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
41 | {
42 | Contract.Requires(prop != null && node != null);
43 | object rawValue = prop.Value;
44 |
45 | if (!(rawValue is uint value))
46 | {
47 | Type type = rawValue.GetType();
48 |
49 | if (type.IsEnum)
50 | {
51 | var signed = (int)rawValue;
52 | value = (uint)signed;
53 | }
54 | else
55 | {
56 | value = 0;
57 | RobloxFile.LogError($"Raw value for enum property {prop} could not be casted to uint!");
58 | }
59 | }
60 |
61 | node.InnerText = value.ToInvariantString();
62 | }
63 |
64 | Enum IAttributeToken.ReadAttribute(RbxAttribute attribute)
65 | {
66 | var enumName = attribute.ReadString();
67 | var enumValue = attribute.ReadUInt();
68 |
69 | var enumType = Type.GetType($"RobloxFiles.Enums.{enumName}");
70 | var isValid = enumType?.IsEnum;
71 |
72 | if (isValid ?? false)
73 | {
74 | var value = Enum.ToObject(enumType, enumValue);
75 | return (Enum)value;
76 | }
77 |
78 | // ... not sure what to do with this.
79 | return null;
80 | }
81 |
82 | void IAttributeToken.WriteAttribute(RbxAttribute attribute, Enum value)
83 | {
84 | var enumType = value?.GetType();
85 | attribute.WriteString(enumType?.Name ?? "");
86 |
87 | var valueInt = Convert.ToUInt32(value);
88 | attribute.WriteUInt(valueInt);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Tokens/Faces.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.Contracts;
2 | using System.Xml;
3 |
4 | using RobloxFiles.DataTypes;
5 | using RobloxFiles.XmlFormat;
6 |
7 | namespace RobloxFiles.Tokens
8 | {
9 | public class FacesToken : IXmlPropertyToken
10 | {
11 | public string XmlPropertyToken => "Faces";
12 |
13 | public bool ReadProperty(Property prop, XmlNode token)
14 | {
15 | Contract.Requires(prop != null);
16 |
17 | if (XmlPropertyTokens.ReadPropertyGeneric(token, out uint value))
18 | {
19 | Faces faces = (Faces)value;
20 | prop.Value = faces;
21 |
22 | return true;
23 | }
24 |
25 | return false;
26 | }
27 |
28 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
29 | {
30 | Contract.Requires(prop != null && doc != null && node != null);
31 |
32 | XmlElement faces = doc.CreateElement("faces");
33 | node.AppendChild(faces);
34 |
35 | int value = prop.CastValue();
36 | faces.InnerText = value.ToInvariantString();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tokens/Float.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.XmlFormat;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class FloatToken : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "float";
9 | public AttributeType AttributeType => AttributeType.Float;
10 |
11 | public float ReadAttribute(RbxAttribute attr) => attr.ReadFloat();
12 | public void WriteAttribute(RbxAttribute attr, float value) => attr.WriteFloat(value);
13 |
14 | public bool ReadProperty(Property prop, XmlNode token)
15 | {
16 | return XmlPropertyTokens.ReadPropertyGeneric(prop, PropertyType.Float, token);
17 | }
18 |
19 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
20 | {
21 | float value = prop.CastValue();
22 | node.InnerText = value.ToInvariantString();
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Tokens/Font.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 |
4 | using RobloxFiles.Enums;
5 | using RobloxFiles.DataTypes;
6 |
7 | namespace RobloxFiles.Tokens
8 | {
9 | public class FontToken : IXmlPropertyToken, IAttributeToken
10 | {
11 | public string XmlPropertyToken => "Font";
12 | public AttributeType AttributeType => AttributeType.FontFace;
13 |
14 | public bool ReadProperty(Property prop, XmlNode node)
15 | {
16 | try
17 | {
18 | var familyNode = node["Family"];
19 | var family = familyNode.InnerText;
20 |
21 | var weightNode = node["Weight"];
22 | var weight = (FontWeight)uint.Parse(weightNode.InnerText);
23 |
24 | var styleNode = node["Style"];
25 | Enum.TryParse(styleNode.InnerText, out FontStyle style);
26 |
27 | var cachedFaceNode = node["CachedFaceId"];
28 | var cachedFaceId = cachedFaceNode?.InnerText;
29 |
30 | prop.Value = new FontFace(family, weight, style, cachedFaceId);
31 | return true;
32 | }
33 | catch
34 | {
35 | return false;
36 | }
37 | }
38 |
39 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
40 | {
41 | FontFace font = prop.CastValue();
42 | var weight = (uint)font.Weight;
43 |
44 | string family = font.Family;
45 | string familyType = "null";
46 |
47 | string cached = font.CachedFaceId;
48 | string cachedType = "null";
49 |
50 | if (family.Length > 0)
51 | familyType = "url";
52 |
53 | if (cached.Length > 0)
54 | cachedType = "url";
55 |
56 | var familyNode = doc.CreateElement("Family");
57 | var familyTypeNode = doc.CreateElement(familyType);
58 |
59 | familyTypeNode.InnerText = family;
60 | familyNode.AppendChild(familyTypeNode);
61 | node.AppendChild(familyNode);
62 |
63 | var weightNode = doc.CreateElement("Weight");
64 | weightNode.InnerText = $"{weight}";
65 | node.AppendChild(weightNode);
66 |
67 | var styleNode = doc.CreateElement("Style");
68 | styleNode.InnerText = $"{font.Style}";
69 | node.AppendChild(styleNode);
70 |
71 | var cachedNode = doc.CreateElement("CachedFaceId");
72 | var cachedTypeNode = doc.CreateElement(cachedType);
73 |
74 | cachedTypeNode.InnerText = cached;
75 | cachedNode.AppendChild(cachedTypeNode);
76 | node.AppendChild(cachedNode);
77 | }
78 |
79 | public FontFace ReadAttribute(RbxAttribute attribute)
80 | {
81 | var weight = (FontStyle)attribute.ReadShort();
82 | var style = (FontWeight)attribute.ReadByte();
83 |
84 | var family = attribute.ReadString();
85 | var cachedFaceId = attribute.ReadString();
86 |
87 | return new FontFace(family, style, weight, cachedFaceId);
88 | }
89 |
90 | public void WriteAttribute(RbxAttribute attribute, FontFace value)
91 | {
92 | var weight = (short)value.Weight;
93 | var style = (byte)value.Style;
94 |
95 | var family = value.Family;
96 | var cachedFaceId = value.CachedFaceId;
97 |
98 | var writer = attribute.Writer;
99 | writer.Write(weight);
100 | writer.Write(style);
101 |
102 | attribute.WriteString(family);
103 | attribute.WriteString(cachedFaceId);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Tokens/Int.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.XmlFormat;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class IntToken : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "int";
9 | public AttributeType AttributeType => AttributeType.Int;
10 |
11 | public int ReadAttribute(RbxAttribute attr) => attr.ReadInt();
12 | public void WriteAttribute(RbxAttribute attr, int value) => attr.WriteInt(value);
13 |
14 | public bool ReadProperty(Property prop, XmlNode token)
15 | {
16 | var obj = prop.Object;
17 | var type = obj.GetType();
18 | var field = type.GetField(prop.Name);
19 |
20 | if (field != null && field.FieldType.Name == "BrickColor")
21 | {
22 | var brickColorToken = XmlPropertyTokens.GetHandler();
23 | return brickColorToken.ReadProperty(prop, token);
24 | }
25 | else
26 | {
27 | return XmlPropertyTokens.ReadPropertyGeneric(prop, PropertyType.Int, token);
28 | }
29 | }
30 |
31 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
32 | {
33 | int value = prop.CastValue();
34 | node.InnerText = value.ToInvariantString();
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tokens/Int64.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.XmlFormat;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class Int64Token : IXmlPropertyToken
7 | {
8 | public string XmlPropertyToken => "int64";
9 |
10 | public bool ReadProperty(Property prop, XmlNode token)
11 | {
12 | return XmlPropertyTokens.ReadPropertyGeneric(prop, PropertyType.Int64, token);
13 | }
14 |
15 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
16 | {
17 | long value = prop.CastValue();
18 | node.InnerText = value.ToString();
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/Tokens/NumberRange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 | using RobloxFiles.DataTypes;
4 |
5 | namespace RobloxFiles.Tokens
6 | {
7 | public class NumberRangeToken : IXmlPropertyToken, IAttributeToken
8 | {
9 | public string XmlPropertyToken => "NumberRange";
10 | public AttributeType AttributeType => AttributeType.NumberRange;
11 |
12 | public bool ReadProperty(Property prop, XmlNode token)
13 | {
14 | string contents = token.InnerText.Trim();
15 | string[] buffer = contents.Split(' ');
16 | bool valid = (buffer.Length == 2);
17 |
18 | if (valid)
19 | {
20 | try
21 | {
22 | float min = Formatting.ParseFloat(buffer[0]);
23 | float max = Formatting.ParseFloat(buffer[1]);
24 |
25 | prop.Type = PropertyType.NumberRange;
26 | prop.Value = new NumberRange(min, max);
27 | }
28 | catch
29 | {
30 | valid = false;
31 | }
32 | }
33 |
34 | return valid;
35 | }
36 |
37 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
38 | {
39 | NumberRange value = prop.CastValue();
40 | node.InnerText = value.ToString() + ' ';
41 | }
42 |
43 | public NumberRange ReadAttribute(RbxAttribute attr)
44 | {
45 | float min = attr.ReadFloat();
46 | float max = attr.ReadFloat();
47 |
48 | return new NumberRange(min, max);
49 | }
50 |
51 | public void WriteAttribute(RbxAttribute attr, NumberRange value)
52 | {
53 | attr.WriteFloat(value.Min);
54 | attr.WriteFloat(value.Max);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Tokens/NumberSequence.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class NumberSequenceToken : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "NumberSequence";
9 | public AttributeType AttributeType => AttributeType.NumberSequence;
10 |
11 | public bool ReadProperty(Property prop, XmlNode token)
12 | {
13 | string contents = token.InnerText.Trim();
14 | string[] buffer = contents.Split(' ');
15 |
16 | int length = buffer.Length;
17 | bool valid = (length % 3 == 0);
18 |
19 | if (valid)
20 | {
21 | try
22 | {
23 | NumberSequenceKeypoint[] keypoints = new NumberSequenceKeypoint[length / 3];
24 |
25 | for (int i = 0; i < length; i += 3)
26 | {
27 | float Time = Formatting.ParseFloat(buffer[ i ]);
28 | float Value = Formatting.ParseFloat(buffer[i + 1]);
29 | float Envelope = Formatting.ParseFloat(buffer[i + 2]);
30 |
31 | keypoints[i / 3] = new NumberSequenceKeypoint(Time, Value, Envelope);
32 | }
33 |
34 | prop.Type = PropertyType.NumberSequence;
35 | prop.Value = new NumberSequence(keypoints);
36 | }
37 | catch
38 | {
39 | valid = false;
40 | }
41 | }
42 |
43 | return valid;
44 | }
45 |
46 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
47 | {
48 | NumberSequence value = prop.CastValue();
49 | node.InnerText = value.ToString() + ' ';
50 | }
51 |
52 | public NumberSequence ReadAttribute(RbxAttribute attr)
53 | {
54 | int numKeys = attr.ReadInt();
55 | var keypoints = new NumberSequenceKeypoint[numKeys];
56 |
57 | for (int i = 0; i < numKeys; i++)
58 | {
59 | float envelope = attr.ReadInt(),
60 | time = attr.ReadFloat(),
61 | value = attr.ReadFloat();
62 |
63 | keypoints[i] = new NumberSequenceKeypoint(time, value, envelope);
64 | }
65 |
66 | return new NumberSequence(keypoints);
67 | }
68 |
69 | public void WriteAttribute(RbxAttribute attr, NumberSequence value)
70 | {
71 | attr.WriteInt(value.Keypoints.Length);
72 |
73 | foreach (var keypoint in value.Keypoints)
74 | {
75 | attr.WriteFloat(keypoint.Envelope);
76 | attr.WriteFloat(keypoint.Time);
77 | attr.WriteFloat(keypoint.Value);
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Tokens/OptionalCFrame.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class OptionalCFrameToken : IXmlPropertyToken
7 | {
8 | public string XmlPropertyToken => "OptionalCoordinateFrame";
9 |
10 | public bool ReadProperty(Property prop, XmlNode token)
11 | {
12 | XmlNode first = token.FirstChild;
13 | CFrame value = null;
14 |
15 | if (first?.Name == "CFrame")
16 | value = CFrameToken.ReadCFrame(first);
17 |
18 | prop.Value = new Optional(value);
19 | prop.Type = PropertyType.OptionalCFrame;
20 |
21 | return true;
22 | }
23 |
24 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
25 | {
26 | if (prop.Value is Optional optional)
27 | {
28 | if (optional.HasValue)
29 | {
30 | CFrame value = optional.Value;
31 | XmlElement cfNode = doc.CreateElement("CFrame");
32 |
33 | CFrameToken.WriteCFrame(value, doc, cfNode);
34 | node.AppendChild(cfNode);
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tokens/PhysicalProperties.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Xml;
4 | using RobloxFiles.DataTypes;
5 |
6 | namespace RobloxFiles.Tokens
7 | {
8 | public class PhysicalPropertiesToken : IXmlPropertyToken
9 | {
10 | public string XmlPropertyToken => "PhysicalProperties";
11 |
12 | private static Func CreateReader(Func parse, XmlNode token) where T : struct
13 | {
14 | return new Func(key =>
15 | {
16 | XmlElement node = token[key];
17 | return parse(node.InnerText);
18 | });
19 | }
20 |
21 | public bool ReadProperty(Property prop, XmlNode token)
22 | {
23 | var readBool = CreateReader(bool.Parse, token);
24 | var readFloat = CreateReader(Formatting.ParseFloat, token);
25 |
26 | try
27 | {
28 | bool custom = readBool("CustomPhysics");
29 | prop.Type = PropertyType.PhysicalProperties;
30 |
31 | if (custom)
32 | {
33 | prop.Value = new PhysicalProperties
34 | (
35 | readFloat("Density"),
36 | readFloat("Friction"),
37 | readFloat("Elasticity"),
38 | readFloat("FrictionWeight"),
39 | readFloat("ElasticityWeight")
40 | );
41 | }
42 |
43 | return true;
44 | }
45 | catch
46 | {
47 | return false;
48 | }
49 | }
50 |
51 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
52 | {
53 | bool hasCustomPhysics = (prop.Value != null);
54 |
55 | XmlElement customPhysics = doc.CreateElement("CustomPhysics");
56 | customPhysics.InnerText = hasCustomPhysics
57 | .ToString()
58 | .ToLower();
59 |
60 | node.AppendChild(customPhysics);
61 |
62 | if (hasCustomPhysics)
63 | {
64 | var customProps = prop.CastValue();
65 |
66 | var data = new Dictionary()
67 | {
68 | { "Density", customProps.Density },
69 | { "Friction", customProps.Friction },
70 | { "Elasticity", customProps.Elasticity },
71 | { "FrictionWeight", customProps.FrictionWeight },
72 | { "ElasticityWeight", customProps.ElasticityWeight }
73 | };
74 |
75 | foreach (string elementType in data.Keys)
76 | {
77 | float value = data[elementType];
78 |
79 | XmlElement element = doc.CreateElement(elementType);
80 | element.InnerText = value.ToInvariantString();
81 |
82 | node.AppendChild(element);
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Tokens/ProtectedString.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Xml;
3 |
4 | using RobloxFiles.DataTypes;
5 | using RobloxFiles.XmlFormat;
6 |
7 | namespace RobloxFiles.Tokens
8 | {
9 | public class ProtectedStringToken : IXmlPropertyToken
10 | {
11 | public string XmlPropertyToken => "ProtectedString";
12 |
13 | public bool ReadProperty(Property prop, XmlNode token)
14 | {
15 | ProtectedString contents = token.InnerText;
16 | prop.Type = PropertyType.String;
17 | prop.Value = contents.ToString();
18 |
19 | return true;
20 | }
21 |
22 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
23 | {
24 | ProtectedString value = prop.CastValue();
25 |
26 | if (value.IsCompiled)
27 | {
28 | var binary = XmlPropertyTokens.GetHandler();
29 | binary.WriteProperty(prop, doc, node);
30 | }
31 | else
32 | {
33 | string contents = Encoding.UTF8.GetString(value.RawBuffer);
34 |
35 | if (contents.Contains("\r") || contents.Contains("\n"))
36 | {
37 | XmlCDataSection cdata = doc.CreateCDataSection(contents);
38 | node.AppendChild(cdata);
39 | }
40 | else
41 | {
42 | node.InnerText = contents;
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tokens/Ray.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 | using RobloxFiles.DataTypes;
4 |
5 | namespace RobloxFiles.Tokens
6 | {
7 | public class RayToken : IXmlPropertyToken
8 | {
9 | public string XmlPropertyToken => "Ray";
10 | private static readonly string[] Fields = new string[2] { "origin", "direction" };
11 |
12 | public bool ReadProperty(Property prop, XmlNode token)
13 | {
14 | try
15 | {
16 | Vector3[] read = new Vector3[Fields.Length];
17 |
18 | for (int i = 0; i < read.Length; i++)
19 | {
20 | string field = Fields[i];
21 | var fieldToken = token[field];
22 | read[i] = Vector3Token.ReadVector3(fieldToken);
23 | }
24 |
25 | Vector3 origin = read[0],
26 | direction = read[1];
27 |
28 | Ray ray = new Ray(origin, direction);
29 | prop.Type = PropertyType.Ray;
30 | prop.Value = ray;
31 |
32 | return true;
33 | }
34 | catch
35 | {
36 | return false;
37 | }
38 | }
39 |
40 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
41 | {
42 | Ray ray = prop.CastValue();
43 |
44 | XmlElement origin = doc.CreateElement("origin");
45 | XmlElement direction = doc.CreateElement("direction");
46 |
47 | Vector3Token.WriteVector3(doc, origin, ray.Origin);
48 | Vector3Token.WriteVector3(doc, direction, ray.Direction);
49 |
50 | node.AppendChild(origin);
51 | node.AppendChild(direction);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tokens/Rect.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 | using RobloxFiles.DataTypes;
4 |
5 | namespace RobloxFiles.Tokens
6 | {
7 | public class RectToken : IXmlPropertyToken, IAttributeToken
8 | {
9 | public string XmlPropertyToken => "Rect2D";
10 | public AttributeType AttributeType => AttributeType.Rect;
11 | private static readonly string[] XmlFields = new string[2] { "min", "max" };
12 |
13 | public bool ReadProperty(Property prop, XmlNode token)
14 | {
15 | try
16 | {
17 | Vector2[] read = new Vector2[XmlFields.Length];
18 |
19 | for (int i = 0; i < read.Length; i++)
20 | {
21 | string field = XmlFields[i];
22 | var fieldToken = token[field];
23 | read[i] = Vector2Token.ReadVector2(fieldToken);
24 | }
25 |
26 | Vector2 min = read[0],
27 | max = read[1];
28 |
29 | Rect rect = new Rect(min, max);
30 | prop.Type = PropertyType.Rect;
31 | prop.Value = rect;
32 |
33 | return true;
34 | }
35 | catch
36 | {
37 | return false;
38 | }
39 | }
40 |
41 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
42 | {
43 | Rect rect = prop.CastValue();
44 |
45 | XmlElement min = doc.CreateElement("min");
46 | Vector2Token.WriteVector2(doc, min, rect.Min);
47 | node.AppendChild(min);
48 |
49 | XmlElement max = doc.CreateElement("max");
50 | Vector2Token.WriteVector2(doc, max, rect.Max);
51 | node.AppendChild(max);
52 | }
53 |
54 | public Rect ReadAttribute(RbxAttribute attr)
55 | {
56 | Vector2 min = Vector2Token.ReadVector2(attr);
57 | Vector2 max = Vector2Token.ReadVector2(attr);
58 |
59 | return new Rect(min, max);
60 | }
61 |
62 | public void WriteAttribute(RbxAttribute attr, Rect value)
63 | {
64 | Vector2Token.WriteVector2(attr, value.Min);
65 | Vector2Token.WriteVector2(attr, value.Max);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Tokens/Ref.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 |
3 | namespace RobloxFiles.Tokens
4 | {
5 | public class RefToken : IXmlPropertyToken
6 | {
7 | public string XmlPropertyToken => "Ref";
8 |
9 | public bool ReadProperty(Property prop, XmlNode token)
10 | {
11 | string refId = token.InnerText;
12 | prop.Type = PropertyType.Ref;
13 | prop.XmlToken = refId;
14 |
15 | return true;
16 | }
17 |
18 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
19 | {
20 | string result = "null";
21 |
22 | if (prop.Value != null)
23 | {
24 | Instance inst = prop.CastValue();
25 | result = inst.Referent;
26 | }
27 |
28 | node.InnerText = result;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tokens/SecurityCapabilities.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 |
3 | namespace RobloxFiles.Tokens
4 | {
5 | public class SecurityCapabilitiesToken : IXmlPropertyToken
6 | {
7 | public string XmlPropertyToken => "SecurityCapabilities";
8 |
9 | public bool ReadProperty(Property prop, XmlNode node)
10 | {
11 | if (ulong.TryParse(node.InnerText, out var value))
12 | {
13 | prop.Value = (SecurityCapabilities)value;
14 | return true;
15 | }
16 |
17 | return false;
18 | }
19 |
20 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
21 | {
22 | var value = prop.CastValue();
23 | node.InnerText = value.ToString();
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tokens/SharedString.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class SharedStringToken : IXmlPropertyToken
7 | {
8 | public string XmlPropertyToken => "SharedString";
9 |
10 | public bool ReadProperty(Property prop, XmlNode token)
11 | {
12 | string key = token.InnerText;
13 | prop.Type = PropertyType.SharedString;
14 | prop.Value = new SharedString(key);
15 |
16 | return true;
17 | }
18 |
19 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
20 | {
21 | if (prop.Value is SharedString value)
22 | {
23 | string key = value.Key;
24 |
25 | if (value.ComputedKey == null)
26 | {
27 | var newShared = SharedString.FromBuffer(value.SharedValue);
28 | key = newShared.ComputedKey;
29 | }
30 |
31 | node.InnerText = key;
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tokens/String.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 |
3 | namespace RobloxFiles.Tokens
4 | {
5 | public class StringToken : IXmlPropertyToken, IAttributeToken
6 | {
7 | public string XmlPropertyToken => "string";
8 | public AttributeType AttributeType => AttributeType.String;
9 |
10 | public string ReadAttribute(RbxAttribute attr) => attr.ReadString();
11 | public void WriteAttribute(RbxAttribute attr, string value) => attr.WriteString(value);
12 |
13 | public bool ReadProperty(Property prop, XmlNode token)
14 | {
15 | string contents = token.InnerText;
16 | prop.Type = PropertyType.String;
17 | prop.Value = contents;
18 |
19 | return true;
20 | }
21 |
22 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
23 | {
24 | string value = prop.Value.ToInvariantString();
25 |
26 | if (value.Contains("\r") || value.Contains("\n"))
27 | {
28 | XmlCDataSection cdata = doc.CreateCDataSection(value);
29 | node.AppendChild(cdata);
30 | }
31 | else
32 | {
33 | node.InnerText = value;
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tokens/UDim.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 | using RobloxFiles.DataTypes;
4 |
5 | namespace RobloxFiles.Tokens
6 | {
7 | public class UDimToken : IXmlPropertyToken, IAttributeToken
8 | {
9 | public string XmlPropertyToken => "UDim";
10 | public AttributeType AttributeType => AttributeType.UDim;
11 |
12 | public UDim ReadAttribute(RbxAttribute attr) => ReadUDim(attr);
13 | public void WriteAttribute(RbxAttribute attr, UDim value) => WriteUDim(attr, value);
14 |
15 | public static UDim ReadUDim(XmlNode token, string prefix = "")
16 | {
17 | try
18 | {
19 | XmlElement scaleToken = token[prefix + 'S'];
20 | float scale = Formatting.ParseFloat(scaleToken.InnerText);
21 |
22 | XmlElement offsetToken = token[prefix + 'O'];
23 | int offset = int.Parse(offsetToken.InnerText);
24 |
25 | return new UDim(scale, offset);
26 | }
27 | catch
28 | {
29 | return null;
30 | }
31 | }
32 |
33 | public static void WriteUDim(XmlDocument doc, XmlNode node, UDim value, string prefix = "")
34 | {
35 | XmlElement scale = doc.CreateElement(prefix + 'S');
36 | scale.InnerText = value.Scale.ToInvariantString();
37 | node.AppendChild(scale);
38 |
39 | XmlElement offset = doc.CreateElement(prefix + 'O');
40 | offset.InnerText = value.Offset.ToInvariantString();
41 | node.AppendChild(offset);
42 | }
43 |
44 | public static UDim ReadUDim(RbxAttribute attr)
45 | {
46 | float scale = attr.ReadFloat();
47 | int offset = attr.ReadInt();
48 |
49 | return new UDim(scale, offset);
50 | }
51 |
52 | public static void WriteUDim(RbxAttribute attr, UDim value)
53 | {
54 | float scale = value.Scale;
55 | attr.WriteFloat(scale);
56 |
57 | int offset = value.Offset;
58 | attr.WriteInt(offset);
59 | }
60 |
61 | public bool ReadProperty(Property property, XmlNode token)
62 | {
63 | UDim result = ReadUDim(token);
64 | bool success = (result != null);
65 |
66 | if (success)
67 | {
68 | property.Type = PropertyType.UDim;
69 | property.Value = result;
70 | }
71 |
72 | return success;
73 | }
74 |
75 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
76 | {
77 | UDim value = prop.CastValue();
78 | WriteUDim(doc, node, value);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Tokens/UDim2.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class UDim2Token : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "UDim2";
9 | public AttributeType AttributeType => AttributeType.UDim2;
10 |
11 | public UDim2 ReadAttribute(RbxAttribute attr)
12 | {
13 | UDim x = UDimToken.ReadUDim(attr);
14 | UDim y = UDimToken.ReadUDim(attr);
15 |
16 | return new UDim2(x, y);
17 | }
18 |
19 | public void WriteAttribute(RbxAttribute attr, UDim2 value)
20 | {
21 | UDimToken.WriteUDim(attr, value.X);
22 | UDimToken.WriteUDim(attr, value.Y);
23 | }
24 |
25 | public bool ReadProperty(Property property, XmlNode token)
26 | {
27 | UDim xUDim = UDimToken.ReadUDim(token, "X");
28 | UDim yUDim = UDimToken.ReadUDim(token, "Y");
29 |
30 | if (xUDim != null && yUDim != null)
31 | {
32 | property.Type = PropertyType.UDim2;
33 | property.Value = new UDim2(xUDim, yUDim);
34 |
35 | return true;
36 | }
37 |
38 | return false;
39 | }
40 |
41 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
42 | {
43 | UDim2 value = prop.CastValue();
44 |
45 | UDim xUDim = value.X;
46 | UDimToken.WriteUDim(doc, node, xUDim, "X");
47 |
48 | UDim yUDim = value.Y;
49 | UDimToken.WriteUDim(doc, node, yUDim, "Y");
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tokens/UniqueId.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Xml;
4 | using RobloxFiles.DataTypes;
5 |
6 | namespace RobloxFiles.Tokens
7 | {
8 | public class UniqueIdToken : IXmlPropertyToken
9 | {
10 | public string XmlPropertyToken => "UniqueId";
11 |
12 | public bool ReadProperty(Property prop, XmlNode token)
13 | {
14 | string hex = token.InnerText;
15 |
16 | if (Guid.TryParse(hex, out var guid))
17 | {
18 | var bytes = new byte[16];
19 |
20 | for (int i = 0; i < 16; i++)
21 | {
22 | var hexChar = hex.Substring(i * 2, 2);
23 | bytes[15 - i] = Convert.ToByte(hexChar, 16);
24 | }
25 |
26 | var rand = BitConverter.ToInt64(bytes, 8);
27 | var time = BitConverter.ToUInt32(bytes, 4);
28 | var index = BitConverter.ToUInt32(bytes, 0);
29 |
30 | var uniqueId = new UniqueId(rand, time, index);
31 | prop.Value = uniqueId;
32 |
33 | return true;
34 | }
35 |
36 | return false;
37 | }
38 |
39 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
40 | {
41 | var uniqueId = prop.CastValue();
42 |
43 | var random = BitConverter.GetBytes(uniqueId.Random);
44 | var time = BitConverter.GetBytes(uniqueId.Time);
45 | var index = BitConverter.GetBytes(uniqueId.Index);
46 |
47 | var bytes = new byte[16];
48 | random.CopyTo(bytes, 0);
49 | time.CopyTo(bytes, 8);
50 | index.CopyTo(bytes, 12);
51 |
52 | var guid = new Guid(bytes);
53 | node.InnerText = guid.ToString("N");
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Tokens/Vector2.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class Vector2Token : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "Vector2";
9 | private static readonly string[] XmlCoords = new string[2] { "X", "Y" };
10 |
11 | public AttributeType AttributeType => AttributeType.Vector2;
12 | public Vector2 ReadAttribute(RbxAttribute attr) => ReadVector2(attr);
13 | public void WriteAttribute(RbxAttribute attr, Vector2 value) => WriteVector2(attr, value);
14 |
15 | public static Vector2 ReadVector2(XmlNode token)
16 | {
17 | float[] xy = new float[2];
18 |
19 | for (int i = 0; i < 2; i++)
20 | {
21 | string key = XmlCoords[i];
22 |
23 | try
24 | {
25 | var coord = token[key];
26 | string text = coord.InnerText;
27 | xy[i] = Formatting.ParseFloat(text);
28 | }
29 | catch
30 | {
31 | return null;
32 | }
33 | }
34 |
35 | return new Vector2(xy);
36 | }
37 |
38 | public static void WriteVector2(XmlDocument doc, XmlNode node, Vector2 value)
39 | {
40 | XmlElement x = doc.CreateElement("X");
41 | x.InnerText = value.X.ToInvariantString();
42 | node.AppendChild(x);
43 |
44 | XmlElement y = doc.CreateElement("Y");
45 | y.InnerText = value.Y.ToInvariantString();
46 | node.AppendChild(y);
47 | }
48 |
49 | public static Vector2 ReadVector2(RbxAttribute attr)
50 | {
51 | float x = attr.ReadFloat(),
52 | y = attr.ReadFloat();
53 |
54 | return new Vector2(x, y);
55 | }
56 |
57 | public static void WriteVector2(RbxAttribute attr, Vector2 value)
58 | {
59 | attr.WriteFloat(value.X);
60 | attr.WriteFloat(value.Y);
61 | }
62 |
63 | public bool ReadProperty(Property property, XmlNode token)
64 | {
65 | Vector2 result = ReadVector2(token);
66 | bool success = (result != null);
67 |
68 | if (success)
69 | {
70 | property.Type = PropertyType.Vector2;
71 | property.Value = result;
72 | }
73 |
74 | return success;
75 | }
76 |
77 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
78 | {
79 | Vector2 value = prop.CastValue();
80 | WriteVector2(doc, node, value);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Tokens/Vector3.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class Vector3Token : IXmlPropertyToken, IAttributeToken
7 | {
8 | public string XmlPropertyToken => "Vector3";
9 | private static readonly string[] XmlCoords = new string[3] { "X", "Y", "Z" };
10 |
11 | public AttributeType AttributeType => AttributeType.Vector3;
12 | public Vector3 ReadAttribute(RbxAttribute attr) => ReadVector3(attr);
13 | public void WriteAttribute(RbxAttribute attr, Vector3 value) => WriteVector3(attr, value);
14 |
15 | public static Vector3 ReadVector3(XmlNode token)
16 | {
17 | float[] xyz = new float[3];
18 |
19 | for (int i = 0; i < 3; i++)
20 | {
21 | string key = XmlCoords[i];
22 |
23 | try
24 | {
25 | var coord = token[key];
26 | xyz[i] = Formatting.ParseFloat(coord.InnerText);
27 | }
28 | catch
29 | {
30 | return null;
31 | }
32 | }
33 |
34 | return new Vector3(xyz);
35 | }
36 |
37 | public static void WriteVector3(XmlDocument doc, XmlNode node, Vector3 value)
38 | {
39 | XmlElement x = doc.CreateElement("X");
40 | x.InnerText = value.X.ToInvariantString();
41 | node.AppendChild(x);
42 |
43 | XmlElement y = doc.CreateElement("Y");
44 | y.InnerText = value.Y.ToInvariantString();
45 | node.AppendChild(y);
46 |
47 | XmlElement z = doc.CreateElement("Z");
48 | z.InnerText = value.Z.ToInvariantString();
49 | node.AppendChild(z);
50 | }
51 |
52 | public static Vector3 ReadVector3(RbxAttribute attr)
53 | {
54 | float x = attr.ReadFloat(),
55 | y = attr.ReadFloat(),
56 | z = attr.ReadFloat();
57 |
58 | return new Vector3(x, y, z);
59 | }
60 |
61 | public static void WriteVector3(RbxAttribute attr, Vector3 value)
62 | {
63 | attr.WriteFloat(value.X);
64 | attr.WriteFloat(value.Y);
65 | attr.WriteFloat(value.Z);
66 | }
67 |
68 | public bool ReadProperty(Property property, XmlNode token)
69 | {
70 | Vector3 result = ReadVector3(token);
71 | bool success = (result != null);
72 |
73 | if (success)
74 | {
75 | property.Type = PropertyType.Vector3;
76 | property.Value = result;
77 | }
78 |
79 | return success;
80 | }
81 |
82 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
83 | {
84 | Vector3 value = prop.CastValue();
85 | WriteVector3(doc, node, value);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Tokens/Vector3int16.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using RobloxFiles.DataTypes;
3 |
4 | namespace RobloxFiles.Tokens
5 | {
6 | public class Vector3int16Token : IXmlPropertyToken
7 | {
8 | public string XmlPropertyToken => "Vector3int16";
9 | private static readonly string[] Coords = new string[3] { "X", "Y", "Z" };
10 |
11 | public bool ReadProperty(Property property, XmlNode token)
12 | {
13 | short[] xyz = new short[3];
14 |
15 | for (int i = 0; i < 3; i++)
16 | {
17 | string key = Coords[i];
18 |
19 | try
20 | {
21 | var coord = token[key];
22 | xyz[i] = short.Parse(coord.InnerText);
23 | }
24 | catch
25 | {
26 | return false;
27 | }
28 | }
29 |
30 | short x = xyz[0],
31 | y = xyz[1],
32 | z = xyz[2];
33 |
34 | property.Type = PropertyType.Vector3int16;
35 | property.Value = new Vector3int16(x, y, z);
36 |
37 | return true;
38 | }
39 |
40 | public void WriteProperty(Property prop, XmlDocument doc, XmlNode node)
41 | {
42 | Vector3int16 value = prop.CastValue();
43 |
44 | XmlElement x = doc.CreateElement("X");
45 | x.InnerText = value.X.ToString();
46 | node.AppendChild(x);
47 |
48 | XmlElement y = doc.CreateElement("Y");
49 | y.InnerText = value.Y.ToString();
50 | node.AppendChild(y);
51 |
52 | XmlElement z = doc.CreateElement("Z");
53 | z.InnerText = value.Z.ToString();
54 | node.AppendChild(z);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Tree/Service.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace RobloxFiles
8 | {
9 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
10 | public class RbxService : Attribute
11 | {
12 | public bool IsRooted { get; set; } = true;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/UnitTest/Files/Binary.rbxl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaximumADHD/Roblox-File-Format/556fd674c5331e83f3b4e4be08fb7c53a1976c34/UnitTest/Files/Binary.rbxl
--------------------------------------------------------------------------------
/UnitTest/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Linq;
4 |
5 | namespace RobloxFiles.UnitTest
6 | {
7 | static class Program
8 | {
9 | static void PrintTreeImpl(Instance inst, int stack = 0)
10 | {
11 | string padding = "";
12 | string extension = "";
13 |
14 | for (int i = 0; i < stack; i++)
15 | padding += '\t';
16 |
17 | switch (inst.ClassName)
18 | {
19 | case "Script":
20 | {
21 | extension = ".server.lua";
22 | break;
23 | }
24 | case "LocalScript":
25 | {
26 | extension = ".client.lua";
27 | break;
28 | }
29 | case "ModuleScript":
30 | {
31 | extension = ".lua";
32 | break;
33 | }
34 | }
35 |
36 | Console.WriteLine($"{padding}{inst.Name}{extension}");
37 |
38 | var children = inst
39 | .GetChildren()
40 | .ToList();
41 |
42 | children.ForEach(child => PrintTreeImpl(child, stack + 1));
43 | }
44 |
45 | static void PrintTree(string path)
46 | {
47 | Console.WriteLine("Opening file...");
48 | RobloxFile target = RobloxFile.Open(path);
49 |
50 | foreach (Instance child in target.GetChildren())
51 | PrintTreeImpl(child);
52 |
53 | Debugger.Break();
54 | }
55 |
56 | [STAThread]
57 | static void Main(string[] args)
58 | {
59 | RobloxFile.LogErrors = true;
60 |
61 | if (args.Length > 0)
62 | {
63 | string path = args[0];
64 | PrintTree(path);
65 | }
66 | else
67 | {
68 | RobloxFile bin = RobloxFile.Open(@"Files\Binary.rbxl");
69 | RobloxFile xml = RobloxFile.Open(@"Files\Xml.rbxlx");
70 |
71 | Console.WriteLine("Files opened! Pausing execution for debugger analysis...");
72 | Debugger.Break();
73 |
74 | bin.Save(@"Files\Binary_SaveTest.rbxl");
75 | xml.Save(@"Files\Xml_SaveTest.rbxlx");
76 |
77 | Console.WriteLine("Files saved! Pausing execution for debugger analysis...");
78 | Debugger.Break();
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/UnitTest/RobloxFileFormat.UnitTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 | AnyCPU;x64
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Always
18 |
19 |
20 | Always
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Utility/DefaultProperty.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 |
6 | namespace RobloxFiles.Utility
7 | {
8 | public static class DefaultProperty
9 | {
10 | private static readonly Dictionary ClassMap;
11 | private static readonly HashSet Refreshed = new HashSet();
12 |
13 | static DefaultProperty()
14 | {
15 | var Instance = typeof(Instance);
16 | var assembly = Assembly.GetExecutingAssembly();
17 |
18 | var classes = assembly.GetTypes()
19 | .Where(type => !type.IsAbstract && Instance.IsAssignableFrom(type))
20 | .Select(type => Activator.CreateInstance(type))
21 | .Cast();
22 |
23 | ClassMap = classes.ToDictionary(inst => inst.ClassName);
24 | }
25 |
26 | public static object Get(string className, string propName)
27 | {
28 | if (!ClassMap.ContainsKey(className))
29 | return null;
30 |
31 | Instance inst = ClassMap[className];
32 |
33 | if (!Refreshed.Contains(inst))
34 | {
35 | inst.RefreshProperties();
36 | Refreshed.Add(inst);
37 | }
38 |
39 | var props = inst.Properties;
40 |
41 | if (!props.ContainsKey(propName))
42 | return null;
43 |
44 | var prop = props[propName];
45 | return prop.Value;
46 | }
47 |
48 | public static object Get(Instance inst, string propName)
49 | {
50 | return Get(inst.ClassName, propName);
51 | }
52 |
53 | public static object Get(Instance inst, Property prop)
54 | {
55 | return Get(inst.ClassName, prop.Name);
56 | }
57 |
58 | public static object Get(string className, Property prop)
59 | {
60 | return Get(className, prop.Name);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Utility/Formatting.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Globalization;
4 | using System.Text;
5 |
6 | internal static class Formatting
7 | {
8 | private static CultureInfo Invariant => CultureInfo.InvariantCulture;
9 |
10 | public static string ToInvariantString(this float value)
11 | {
12 | string result;
13 |
14 | if (float.IsPositiveInfinity(value))
15 | result = "INF";
16 | else if (float.IsNegativeInfinity(value))
17 | result = "-INF";
18 | else if (float.IsNaN(value))
19 | result = "NAN";
20 | else
21 | result = value.ToString(Invariant);
22 |
23 | return result;
24 | }
25 |
26 | public static string ToInvariantString(this double value)
27 | {
28 | string result;
29 |
30 | if (double.IsPositiveInfinity(value))
31 | result = "INF";
32 | else if (double.IsNegativeInfinity(value))
33 | result = "-INF";
34 | else if (double.IsNaN(value))
35 | result = "NAN";
36 | else
37 | result = value.ToString(Invariant);
38 |
39 | return result;
40 | }
41 |
42 | public static string ToInvariantString(this int value)
43 | {
44 | return value.ToString(Invariant);
45 | }
46 |
47 | public static string ToInvariantString(this object value)
48 | {
49 | switch (value)
50 | {
51 | case double d : return d.ToInvariantString();
52 | case float f : return f.ToInvariantString();
53 | case int i : return i.ToInvariantString();
54 | default : return value.ToString();
55 | }
56 | }
57 |
58 | public static string ToLowerInvariant(this string str)
59 | {
60 | return str.ToLower(Invariant);
61 | }
62 |
63 | public static string ToUpperInvariant(this string str)
64 | {
65 | return str.ToUpper(Invariant);
66 | }
67 |
68 | public static bool StartsWithInvariant(this string str, string other)
69 | {
70 | return str.StartsWith(other, StringComparison.InvariantCulture);
71 | }
72 |
73 | public static bool EndsWithInvariant(this string str, string other)
74 | {
75 | return str.EndsWith(other, StringComparison.InvariantCulture);
76 | }
77 |
78 | public static float ParseFloat(string value)
79 | {
80 | switch (value)
81 | {
82 | case "NAN" : return float.NaN;
83 | case "INF" : return float.PositiveInfinity;
84 | case "-INF" : return float.NegativeInfinity;
85 | default : return float.Parse(value, Invariant);
86 | }
87 | }
88 |
89 | public static double ParseDouble(string value)
90 | {
91 | switch (value)
92 | {
93 | case "NAN" : return double.NaN;
94 | case "INF" : return double.PositiveInfinity;
95 | case "-INF" : return double.NegativeInfinity;
96 | default : return double.Parse(value, Invariant);
97 | }
98 | }
99 |
100 | public static int ParseInt(string s)
101 | {
102 | return int.Parse(s, Invariant);
103 | }
104 |
105 | public static bool FuzzyEquals(this float a, float b, float epsilon = 10e-5f)
106 | {
107 | return Math.Abs(a - b) < epsilon;
108 | }
109 |
110 | public static bool FuzzyEquals(this double a, double b, double epsilon = 10e-5)
111 | {
112 | return Math.Abs(a - b) < epsilon;
113 | }
114 |
115 | public static byte[] ReadBuffer(this BinaryReader reader)
116 | {
117 | int len = reader.ReadInt32();
118 | return reader.ReadBytes(len);
119 | }
120 |
121 | public static string ReadString(this BinaryReader reader, bool useIntLength)
122 | {
123 | if (!useIntLength)
124 | return reader.ReadString();
125 |
126 | byte[] buffer = reader.ReadBuffer();
127 | return Encoding.UTF8.GetString(buffer);
128 | }
129 | }
--------------------------------------------------------------------------------
/Utility/ImplicitMember.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Reflection;
4 |
5 | namespace RobloxFiles.Utility
6 | {
7 | // This is a lazy helper class to disambiguate between FieldInfo and PropertyInfo
8 | internal class ImplicitMember
9 | {
10 | private const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase;
11 |
12 | private readonly object member;
13 | private readonly string inputName;
14 |
15 | private ImplicitMember(FieldInfo field, string name)
16 | {
17 | member = field;
18 | inputName = name;
19 | }
20 | private ImplicitMember(PropertyInfo prop, string name)
21 | {
22 | member = prop;
23 | inputName = name;
24 | }
25 |
26 | public static ImplicitMember Get(Type type, string name)
27 | {
28 | var field = type
29 | .GetFields(flags)
30 | .Where(f => f.Name == name)
31 | .FirstOrDefault();
32 |
33 | if (field != null)
34 | return new ImplicitMember(field, name);
35 |
36 | var prop = type
37 | .GetProperties(flags)
38 | .Where(p => p.Name == name)
39 | .FirstOrDefault();
40 |
41 | if (prop != null)
42 | return new ImplicitMember(prop, name);
43 |
44 | return null;
45 | }
46 |
47 | public Type MemberType
48 | {
49 | get
50 | {
51 | switch (member)
52 | {
53 | case PropertyInfo prop: return prop.PropertyType;
54 | case FieldInfo field: return field.FieldType;
55 |
56 | default: return null;
57 | }
58 | }
59 | }
60 |
61 | public object GetValue(object obj)
62 | {
63 | object result = null;
64 |
65 | switch (member)
66 | {
67 | case FieldInfo field:
68 | {
69 | result = field.GetValue(obj);
70 | break;
71 | }
72 | case PropertyInfo prop:
73 | {
74 | result = prop.GetValue(obj);
75 | break;
76 | }
77 | }
78 |
79 | return result;
80 | }
81 |
82 | public void SetValue(object obj, object value)
83 | {
84 | switch (member)
85 | {
86 | case FieldInfo field:
87 | {
88 | field.SetValue(obj, value);
89 | return;
90 | }
91 | case PropertyInfo prop:
92 | {
93 | prop.SetValue(obj, value);
94 | return;
95 | }
96 | }
97 |
98 | RobloxFile.LogError($"Unknown field '{inputName}' in ImplicitMember.SetValue");
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Utility/LostEnumValue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace RobloxFiles
4 | {
5 | ///
6 | /// Indicates an enum that was removed by Roblox without any enum name taking the place of its original value.
7 | /// This has only ever happened in a few specific narrow cases and is considered bad practice.
8 | ///
9 | public class LostEnumValue : Attribute
10 | {
11 | public int MapTo { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Utility/MaterialInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using RobloxFiles.Enums;
3 |
4 | namespace RobloxFiles.Utility
5 | {
6 | /*
7 | for i, material in Enum.Material:GetEnumItems() do
8 | local physics = PhysicalProperties.new(material)
9 |
10 | local entry = string.format(
11 | "{ Material.%-14s new PhysicalPropertyInfo(%.2ff, %.2ff, %.2ff, %.2ff, %.2ff) },",
12 | `{material.Name},`,
13 | physics.Density,
14 | physics.Friction,
15 | physics.Elasticity,
16 | physics.FrictionWeight,
17 | physics.ElasticityWeight
18 | )
19 |
20 | print(entry)
21 | end
22 | */
23 |
24 | public struct PhysicalPropertyInfo
25 | {
26 | public float Density;
27 | public float Friction;
28 | public float Elasticity;
29 | public float FrictionWeight;
30 | public float ElasticityWeight;
31 |
32 | public PhysicalPropertyInfo(float density, float friction, float elasticity, float frictionWeight, float elasticityWeight)
33 | {
34 | Density = density;
35 | Friction = friction;
36 | Elasticity = elasticity;
37 | FrictionWeight = frictionWeight;
38 | ElasticityWeight = elasticityWeight;
39 | }
40 | }
41 |
42 | public static class PhysicalPropertyData
43 | {
44 | ///
45 | /// A dictionary mapping materials to their default physical properties.
46 | ///
47 | public static readonly IReadOnlyDictionary Materials = new Dictionary()
48 | {
49 | { Material.Plastic, new PhysicalPropertyInfo(0.70f, 0.30f, 0.50f, 1.00f, 1.00f) },
50 | { Material.SmoothPlastic, new PhysicalPropertyInfo(0.70f, 0.20f, 0.50f, 1.00f, 1.00f) },
51 | { Material.Neon, new PhysicalPropertyInfo(0.70f, 0.30f, 0.20f, 1.00f, 1.00f) },
52 | { Material.Wood, new PhysicalPropertyInfo(0.35f, 0.48f, 0.20f, 1.00f, 1.00f) },
53 | { Material.WoodPlanks, new PhysicalPropertyInfo(0.35f, 0.48f, 0.20f, 1.00f, 1.00f) },
54 | { Material.Marble, new PhysicalPropertyInfo(2.56f, 0.20f, 0.17f, 1.00f, 1.00f) },
55 | { Material.Slate, new PhysicalPropertyInfo(2.69f, 0.40f, 0.20f, 1.00f, 1.00f) },
56 | { Material.Concrete, new PhysicalPropertyInfo(2.40f, 0.70f, 0.20f, 0.30f, 1.00f) },
57 | { Material.Granite, new PhysicalPropertyInfo(2.69f, 0.40f, 0.20f, 1.00f, 1.00f) },
58 | { Material.Brick, new PhysicalPropertyInfo(1.92f, 0.80f, 0.15f, 0.30f, 1.00f) },
59 | { Material.Pebble, new PhysicalPropertyInfo(2.40f, 0.40f, 0.17f, 1.00f, 1.50f) },
60 | { Material.Cobblestone, new PhysicalPropertyInfo(2.69f, 0.50f, 0.17f, 1.00f, 1.00f) },
61 | { Material.Rock, new PhysicalPropertyInfo(2.69f, 0.50f, 0.17f, 1.00f, 1.00f) },
62 | { Material.Sandstone, new PhysicalPropertyInfo(2.69f, 0.50f, 0.15f, 5.00f, 1.00f) },
63 | { Material.Basalt, new PhysicalPropertyInfo(2.69f, 0.70f, 0.15f, 0.30f, 1.00f) },
64 | { Material.CrackedLava, new PhysicalPropertyInfo(2.69f, 0.65f, 0.15f, 1.00f, 1.00f) },
65 | { Material.Limestone, new PhysicalPropertyInfo(2.69f, 0.50f, 0.15f, 1.00f, 1.00f) },
66 | { Material.Pavement, new PhysicalPropertyInfo(2.69f, 0.50f, 0.17f, 0.30f, 1.00f) },
67 | { Material.CorrodedMetal, new PhysicalPropertyInfo(7.85f, 0.70f, 0.20f, 1.00f, 1.00f) },
68 | { Material.DiamondPlate, new PhysicalPropertyInfo(7.85f, 0.35f, 0.25f, 1.00f, 1.00f) },
69 | { Material.Foil, new PhysicalPropertyInfo(2.70f, 0.40f, 0.25f, 1.00f, 1.00f) },
70 | { Material.Metal, new PhysicalPropertyInfo(7.85f, 0.40f, 0.25f, 1.00f, 1.00f) },
71 | { Material.Grass, new PhysicalPropertyInfo(0.90f, 0.40f, 0.10f, 1.00f, 1.50f) },
72 | { Material.LeafyGrass, new PhysicalPropertyInfo(0.90f, 0.40f, 0.10f, 2.00f, 2.00f) },
73 | { Material.Sand, new PhysicalPropertyInfo(1.60f, 0.50f, 0.05f, 5.00f, 2.50f) },
74 | { Material.Fabric, new PhysicalPropertyInfo(0.70f, 0.35f, 0.05f, 1.00f, 1.00f) },
75 | { Material.Snow, new PhysicalPropertyInfo(0.90f, 0.30f, 0.03f, 3.00f, 4.00f) },
76 | { Material.Mud, new PhysicalPropertyInfo(0.90f, 0.30f, 0.07f, 3.00f, 4.00f) },
77 | { Material.Ground, new PhysicalPropertyInfo(0.90f, 0.45f, 0.10f, 1.00f, 1.00f) },
78 | { Material.Asphalt, new PhysicalPropertyInfo(2.36f, 0.80f, 0.20f, 0.30f, 1.00f) },
79 | { Material.Salt, new PhysicalPropertyInfo(2.16f, 0.50f, 0.05f, 1.00f, 1.00f) },
80 | { Material.Ice, new PhysicalPropertyInfo(0.92f, 0.02f, 0.15f, 3.00f, 1.00f) },
81 | { Material.Glacier, new PhysicalPropertyInfo(0.92f, 0.05f, 0.15f, 2.00f, 1.00f) },
82 | { Material.Glass, new PhysicalPropertyInfo(2.40f, 0.25f, 0.20f, 1.00f, 1.00f) },
83 | { Material.ForceField, new PhysicalPropertyInfo(2.40f, 0.25f, 0.20f, 1.00f, 1.00f) },
84 | { Material.Air, new PhysicalPropertyInfo(0.01f, 0.01f, 0.01f, 1.00f, 1.00f) },
85 | { Material.Water, new PhysicalPropertyInfo(1.00f, 0.00f, 0.01f, 1.00f, 1.00f) },
86 | { Material.Cardboard, new PhysicalPropertyInfo(0.70f, 0.50f, 0.05f, 1.00f, 2.00f) },
87 | { Material.Carpet, new PhysicalPropertyInfo(1.10f, 0.40f, 0.25f, 1.00f, 2.00f) },
88 | { Material.CeramicTiles, new PhysicalPropertyInfo(2.40f, 0.51f, 0.20f, 1.00f, 1.00f) },
89 | { Material.ClayRoofTiles, new PhysicalPropertyInfo(2.00f, 0.51f, 0.20f, 1.00f, 1.00f) },
90 | { Material.RoofShingles, new PhysicalPropertyInfo(2.36f, 0.80f, 0.20f, 0.30f, 1.00f) },
91 | { Material.Leather, new PhysicalPropertyInfo(0.86f, 0.35f, 0.25f, 1.00f, 1.00f) },
92 | { Material.Plaster, new PhysicalPropertyInfo(0.75f, 0.60f, 0.20f, 0.30f, 1.00f) },
93 | { Material.Rubber, new PhysicalPropertyInfo(1.30f, 1.50f, 0.95f, 3.00f, 2.00f) },
94 | };
95 | }
96 | }
--------------------------------------------------------------------------------
/Utility/Specials.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Cryptography;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | using RobloxFiles.Enums;
9 | using RobloxFiles.DataTypes;
10 |
11 | using Newtonsoft.Json;
12 |
13 | namespace RobloxFiles
14 | {
15 | internal struct AccessoryBlob
16 | {
17 | public long AssetId;
18 | public int Order;
19 | public float Puffiness;
20 | public AccessoryType AccessoryType;
21 |
22 | public override bool Equals(object obj)
23 | {
24 | if (obj is AccessoryBlob blob)
25 | {
26 | if (AssetId != blob.AssetId)
27 | return false;
28 |
29 | if (Order != blob.Order)
30 | return false;
31 |
32 | if (!Puffiness.FuzzyEquals(blob.Puffiness))
33 | return false;
34 |
35 | if (!AccessoryType.Equals(blob.AccessoryType))
36 | return false;
37 |
38 | return true;
39 | }
40 |
41 | return false;
42 | }
43 |
44 | public override int GetHashCode()
45 | {
46 | return Order.GetHashCode() ^
47 | AssetId.GetHashCode() ^
48 | Puffiness.GetHashCode() ^
49 | AccessoryType.GetHashCode();
50 | }
51 | }
52 |
53 | static class Specials
54 | {
55 | public static BodyPartDescription GetBodyPart(HumanoidDescription hDesc, BodyPart bodyPart)
56 | {
57 | BodyPartDescription target = null;
58 | var existed = false;
59 |
60 | foreach (var bodyPartDesc in hDesc.GetChildrenOfType())
61 | {
62 | if (bodyPartDesc.BodyPart == bodyPart)
63 | {
64 | target = bodyPartDesc;
65 | existed = true;
66 | break;
67 | }
68 | }
69 |
70 | if (target == null)
71 | {
72 | target = new BodyPartDescription()
73 | {
74 | BodyPart = bodyPart,
75 | Parent = hDesc,
76 | };
77 | }
78 |
79 | if (!existed)
80 | {
81 | var bodyPartName = Enum.GetName(typeof(BodyPart), bodyPart);
82 | var propAssetId = hDesc.GetProperty(bodyPartName);
83 |
84 | var bodyColorName = bodyPartName + "Color";
85 | var propColor = hDesc.GetProperty(bodyColorName);
86 |
87 | if (propAssetId != null)
88 | {
89 | var newAssetId = new Property("AssetId", PropertyType.Int64, target);
90 | newAssetId.Value = propAssetId.CastValue();
91 | target.AddProperty(newAssetId);
92 | }
93 |
94 | if (propColor != null)
95 | {
96 | var newColor = new Property("Color", PropertyType.Color3, target);
97 | newColor.Value = propColor.CastValue();
98 | target.AddProperty(newColor);
99 | }
100 | }
101 |
102 | return target;
103 | }
104 |
105 | public static string GetAccessoryBlob(HumanoidDescription hDesc)
106 | {
107 | Property legacyProp = hDesc.GetProperty("AccessoryBlob");
108 |
109 | var layered = hDesc.GetChildrenOfType()
110 | .Where(child => child.IsLayered)
111 | .ToList();
112 |
113 | if (legacyProp != null)
114 | {
115 | if (!layered.Any())
116 | {
117 | string value = legacyProp.CastValue();
118 | var data = JsonConvert.DeserializeObject(value);
119 |
120 | if (data != null)
121 | {
122 | foreach (var blob in data)
123 | {
124 | layered.Add(new AccessoryDescription()
125 | {
126 | AccessoryType = blob.AccessoryType,
127 | Puffiness = blob.Puffiness,
128 | AssetId = blob.AssetId,
129 | Order = blob.Order,
130 | IsLayered = true,
131 | Parent = hDesc,
132 | });
133 | }
134 | }
135 | }
136 |
137 | hDesc.RemoveProperty("AccessoryBlob");
138 | }
139 |
140 | var accessoryBlobs = layered.Select(accDesc => new AccessoryBlob()
141 | {
142 | AccessoryType = accDesc.AccessoryType,
143 | Puffiness = accDesc.Puffiness,
144 | AssetId = accDesc.AssetId,
145 | Order = accDesc.Order,
146 | });
147 |
148 | return JsonConvert.SerializeObject(accessoryBlobs.ToArray());
149 | }
150 |
151 | public static string SetAccessoryBlob(HumanoidDescription hDesc, string value)
152 | {
153 | return "";
154 | }
155 |
156 | public static string GetAccessories(HumanoidDescription hDesc, AccessoryType accessoryType)
157 | {
158 | return "";
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/XmlFormat/XmlFileReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Xml;
3 |
4 | using RobloxFiles.DataTypes;
5 | using RobloxFiles.Tokens;
6 |
7 | namespace RobloxFiles.XmlFormat
8 | {
9 | public static class XmlRobloxFileReader
10 | {
11 | private static Func CreateErrorHandler(string label)
12 | {
13 | var errorHandler = new Func((message) =>
14 | {
15 | string contents = $"{nameof(XmlRobloxFileReader)}.{label} - {message}";
16 | return new Exception(contents);
17 | });
18 |
19 | return errorHandler;
20 | }
21 |
22 | public static void ReadSharedStrings(XmlNode sharedStrings, XmlRobloxFile file)
23 | {
24 | var error = CreateErrorHandler(nameof(ReadSharedStrings));
25 |
26 | if (sharedStrings.Name != "SharedStrings")
27 | throw error("Provided XmlNode's class must be 'SharedStrings'!");
28 |
29 | foreach (XmlNode sharedString in sharedStrings)
30 | {
31 | if (sharedString.Name == "SharedString")
32 | {
33 | XmlNode hashNode = sharedString.Attributes.GetNamedItem("md5");
34 |
35 | if (hashNode == null)
36 | throw error("Got a SharedString without an 'md5' attribute!");
37 |
38 | string key = hashNode.InnerText;
39 | string value = sharedString.InnerText.Replace("\n", "");
40 |
41 | byte[] hash = Convert.FromBase64String(key);
42 | var record = SharedString.FromBase64(value);
43 |
44 | if (hash.Length != 16)
45 | throw error($"SharedString base64 key '{key}' must align to byte[16]!");
46 |
47 | if (key != record.Key)
48 | {
49 | SharedString.Register(key, record.SharedValue);
50 | record.Key = key;
51 | }
52 |
53 | file.SharedStrings.Add(key);
54 | }
55 | }
56 | }
57 |
58 | public static void ReadMetadata(XmlNode meta, XmlRobloxFile file)
59 | {
60 | var error = CreateErrorHandler(nameof(ReadMetadata));
61 |
62 | if (meta.Name != "Meta")
63 | throw error("Provided XmlNode's class should be 'Meta'!");
64 |
65 | XmlNode propName = meta.Attributes.GetNamedItem("name");
66 |
67 | if (propName == null)
68 | throw error("Got a Meta node without a 'name' attribute!");
69 |
70 | string key = propName.InnerText;
71 | string value = meta.InnerText;
72 |
73 | file.Metadata[key] = value;
74 | }
75 |
76 | public static void ReadProperties(Instance instance, XmlNode propsNode)
77 | {
78 | var error = CreateErrorHandler(nameof(ReadProperties));
79 |
80 | if (propsNode.Name != "Properties")
81 | throw error("Provided XmlNode's class should be 'Properties'!");
82 |
83 | foreach (XmlNode propNode in propsNode.ChildNodes)
84 | {
85 | if (propNode.NodeType == XmlNodeType.Comment)
86 | continue;
87 |
88 | string propType = propNode.Name;
89 | XmlNode propName = propNode.Attributes.GetNamedItem("name");
90 |
91 | if (propName == null)
92 | {
93 | if (propNode.Name == "Item")
94 | continue;
95 |
96 | throw error("Got a property node without a 'name' attribute!");
97 | }
98 |
99 | IXmlPropertyToken tokenHandler = XmlPropertyTokens.GetHandler(propType);
100 |
101 | if (tokenHandler != null)
102 | {
103 | var prop = new Property()
104 | {
105 | Name = propName.InnerText,
106 | XmlToken = propType,
107 | Object = instance,
108 | };
109 |
110 | if (!tokenHandler.ReadProperty(prop, propNode) && RobloxFile.LogErrors)
111 | {
112 | var readError = error($"Could not read property: {prop.GetFullName()}!");
113 | RobloxFile.LogError(readError.Message);
114 | }
115 |
116 | instance.AddProperty(prop);
117 | }
118 | else if (RobloxFile.LogErrors)
119 | {
120 | var tokenError = error($"No {nameof(IXmlPropertyToken)} found for property type: {propType}!");
121 | RobloxFile.LogError(tokenError.Message);
122 | }
123 | }
124 | }
125 | public static Instance ReadInstance(XmlNode instNode, XmlRobloxFile file)
126 | {
127 | var error = CreateErrorHandler(nameof(ReadInstance));
128 |
129 | // Process the instance itself
130 | if (instNode.Name != "Item")
131 | throw error("Provided XmlNode's name should be 'Item'!");
132 |
133 | XmlNode classToken = instNode.Attributes.GetNamedItem("class");
134 |
135 | if (classToken == null)
136 | throw error("Got an Item without a defined 'class' attribute!");
137 |
138 | string className = classToken.InnerText;
139 | Type instType = Type.GetType($"RobloxFiles.{className}");
140 |
141 | if (instType == null)
142 | {
143 | if (RobloxFile.LogErrors)
144 | {
145 | var typeError = error($"Unknown class {className} while reading Item.");
146 | RobloxFile.LogError(typeError.Message);
147 | }
148 |
149 | return null;
150 | }
151 |
152 | Instance inst = Activator.CreateInstance(instType) as Instance;
153 |
154 | // The 'referent' attribute is optional, but should be defined if a Ref property needs to link to this Instance.
155 | XmlNode refToken = instNode.Attributes.GetNamedItem("referent");
156 |
157 | if (refToken != null && file != null)
158 | {
159 | string referent = refToken.InnerText;
160 | inst.Referent = referent;
161 |
162 | if (file.Instances.ContainsKey(referent))
163 | throw error("Got an Item with a duplicate 'referent' attribute!");
164 |
165 | file.Instances.Add(referent, inst);
166 | }
167 |
168 | // Process the child nodes of this instance.
169 | foreach (XmlNode childNode in instNode.ChildNodes)
170 | {
171 | switch (childNode.Name)
172 | {
173 | case "Item":
174 | {
175 | Instance child = ReadInstance(childNode, file);
176 |
177 | if (child != null)
178 | child.Parent = inst;
179 |
180 | break;
181 | }
182 | case "Properties":
183 | {
184 | ReadProperties(inst, childNode);
185 | break;
186 | }
187 | default:
188 | {
189 | break;
190 | }
191 | }
192 | }
193 |
194 | return inst;
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/XmlFormat/XmlPropertyTokens.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using System.ComponentModel;
5 | using System.Linq;
6 | using System.Xml;
7 |
8 | using RobloxFiles.Tokens;
9 |
10 | namespace RobloxFiles.XmlFormat
11 | {
12 | public static class XmlPropertyTokens
13 | {
14 | private static readonly Dictionary Handlers = new Dictionary();
15 |
16 | static XmlPropertyTokens()
17 | {
18 | // Initialize the PropertyToken handler singletons.
19 | Type IXmlPropertyToken = typeof(IXmlPropertyToken);
20 | var assembly = Assembly.GetExecutingAssembly();
21 |
22 | var handlerTypes = assembly.GetTypes()
23 | .Where(type => IXmlPropertyToken.IsAssignableFrom(type))
24 | .Where(type => type != IXmlPropertyToken);
25 |
26 | var propTokens = handlerTypes
27 | .Select(handlerType => Activator.CreateInstance(handlerType))
28 | .Cast();
29 |
30 | foreach (IXmlPropertyToken propToken in propTokens)
31 | {
32 | var tokens = propToken.XmlPropertyToken.Split(';')
33 | .Select(token => token.Trim())
34 | .ToList();
35 |
36 | tokens.ForEach(token => Handlers.Add(token, propToken));
37 | }
38 | }
39 |
40 | public static bool ReadPropertyGeneric(XmlNode token, out T outValue) where T : struct
41 | {
42 | if (token == null)
43 | {
44 | var name = nameof(token);
45 | throw new ArgumentNullException(name);
46 | }
47 |
48 | try
49 | {
50 | string value = token.InnerText;
51 | Type type = typeof(T);
52 |
53 | object result = null;
54 |
55 | if (type == typeof(int))
56 | result = Formatting.ParseInt(value);
57 | else if (type == typeof(float))
58 | result = Formatting.ParseFloat(value);
59 | else if (type == typeof(double))
60 | result = Formatting.ParseDouble(value);
61 |
62 | if (result == null)
63 | {
64 | Type resultType = typeof(T);
65 | var converter = TypeDescriptor.GetConverter(resultType);
66 | result = converter.ConvertFromString(token.InnerText);
67 | }
68 |
69 | outValue = (T)result;
70 | return true;
71 | }
72 | catch (NotSupportedException)
73 | {
74 | outValue = default;
75 | return false;
76 | }
77 | }
78 |
79 | public static bool ReadPropertyGeneric(Property prop, PropertyType propType, XmlNode token) where T : struct
80 | {
81 | if (prop == null)
82 | {
83 | var name = nameof(prop);
84 | throw new ArgumentNullException(name);
85 | }
86 |
87 | if (ReadPropertyGeneric(token, out T result))
88 | {
89 | prop.Type = propType;
90 | prop.Value = result;
91 |
92 | return true;
93 | }
94 |
95 | return false;
96 | }
97 |
98 | public static IXmlPropertyToken GetHandler(string tokenName)
99 | {
100 | IXmlPropertyToken result = null;
101 |
102 | if (Handlers.ContainsKey(tokenName))
103 | result = Handlers[tokenName];
104 |
105 | return result;
106 | }
107 |
108 | public static T GetHandler() where T : IXmlPropertyToken
109 | {
110 | IXmlPropertyToken result = Handlers.Values
111 | .Where(token => token is T)
112 | .First();
113 |
114 | return (T)result;
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/XmlFormat/XmlRobloxFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | using System.Linq;
6 | using System.Text;
7 | using System.Xml;
8 |
9 | using RobloxFiles.DataTypes;
10 | using RobloxFiles.XmlFormat;
11 |
12 | namespace RobloxFiles
13 | {
14 | public class XmlRobloxFile : RobloxFile
15 | {
16 | public readonly XmlDocument XmlDocument = new XmlDocument();
17 |
18 | internal Dictionary Instances = new Dictionary();
19 | internal HashSet SharedStrings = new HashSet();
20 |
21 | public Dictionary Metadata { get; private set; } = new Dictionary();
22 | internal int RefCounter = 0;
23 |
24 | public XmlRobloxFile()
25 | {
26 | Name = "Xml:";
27 | Referent = "null";
28 | ParentLocked = true;
29 | }
30 |
31 | protected override void ReadFile(byte[] buffer)
32 | {
33 | try
34 | {
35 | string xml = Encoding.UTF8.GetString(buffer);
36 | var settings = new XmlReaderSettings() { XmlResolver = null };
37 |
38 | using (StringReader reader = new StringReader(xml))
39 | {
40 | XmlReader xmlReader = XmlReader.Create(reader, settings);
41 | XmlDocument.Load(xmlReader);
42 | xmlReader.Dispose();
43 | }
44 | }
45 | catch
46 | {
47 | throw new Exception("XmlRobloxFile: Could not read provided buffer as XML!");
48 | }
49 |
50 | XmlNode roblox = XmlDocument.FirstChild;
51 |
52 | if (roblox != null && roblox.Name == "roblox")
53 | {
54 | // Verify the version we are using.
55 | XmlNode version = roblox.Attributes.GetNamedItem("version");
56 |
57 | if (version == null || !int.TryParse(version.Value, out int schemaVersion))
58 | throw new Exception("XmlRobloxFile: No version number defined!");
59 | else if (schemaVersion < 4)
60 | throw new Exception("XmlRobloxFile: Provided version must be at least 4!");
61 |
62 | // Process the instances.
63 | foreach (XmlNode child in roblox.ChildNodes)
64 | {
65 | if (child.Name == "Item")
66 | {
67 | Instance item = XmlRobloxFileReader.ReadInstance(child, this);
68 |
69 | if (item == null)
70 | continue;
71 |
72 | item.Parent = this;
73 | }
74 | else if (child.Name == "SharedStrings")
75 | {
76 | XmlRobloxFileReader.ReadSharedStrings(child, this);
77 | }
78 | else if (child.Name == "Meta")
79 | {
80 | XmlRobloxFileReader.ReadMetadata(child, this);
81 | }
82 | }
83 |
84 | // Query the properties.
85 | var allProps = Instances.Values
86 | .SelectMany(inst => inst.Properties)
87 | .Select(pair => pair.Value);
88 |
89 | // Resolve referent properties.
90 | var refProps = allProps.Where(prop => prop.Type == PropertyType.Ref);
91 |
92 | foreach (Property refProp in refProps)
93 | {
94 | string refId = refProp.XmlToken;
95 | refProp.XmlToken = "Ref";
96 |
97 | if (Instances.ContainsKey(refId))
98 | {
99 | Instance refInst = Instances[refId];
100 | refProp.Value = refInst;
101 | }
102 | else if (refId != "null" && refId != "Ref")
103 | {
104 | LogError($"XmlRobloxFile: Could not resolve reference for {refProp.GetFullName()}");
105 | refProp.Value = null;
106 | }
107 | }
108 |
109 | // Record shared strings.
110 | var sharedProps = allProps.Where(prop => prop.Type == PropertyType.SharedString);
111 |
112 | foreach (Property sharedProp in sharedProps)
113 | {
114 | SharedString shared = sharedProp.CastValue();
115 |
116 | if (shared == null)
117 | {
118 | var nullBuffer = Array.Empty();
119 | shared = SharedString.FromBuffer(nullBuffer);
120 | sharedProp.Value = shared;
121 | }
122 |
123 | SharedStrings.Add(shared.Key);
124 | }
125 | }
126 | else
127 | {
128 | throw new Exception("XmlRobloxFile: No 'roblox' element found!");
129 | }
130 | }
131 |
132 | public override void Save(Stream stream)
133 | {
134 | XmlDocument doc = new XmlDocument();
135 |
136 | XmlElement roblox = doc.CreateElement("roblox");
137 | roblox.SetAttribute("version", "4");
138 | doc.AppendChild(roblox);
139 |
140 | RefCounter = 0;
141 | Instances.Clear();
142 | SharedStrings.Clear();
143 |
144 | // First, append the metadata
145 | foreach (string key in Metadata.Keys)
146 | {
147 | string value = Metadata[key];
148 |
149 | XmlElement meta = doc.CreateElement("Meta");
150 | meta.SetAttribute("name", key);
151 | meta.InnerText = value;
152 |
153 | roblox.AppendChild(meta);
154 | }
155 |
156 | Instance[] children = GetChildren();
157 |
158 | // Record all of the instances.
159 | foreach (Instance inst in children)
160 | XmlRobloxFileWriter.RecordInstances(this, inst);
161 |
162 | // Now append them into the document.
163 | foreach (Instance inst in children)
164 | {
165 | if (inst.Archivable)
166 | {
167 | XmlNode instNode = XmlRobloxFileWriter.WriteInstance(inst, doc, this);
168 | roblox.AppendChild(instNode);
169 | }
170 | }
171 |
172 | // Append the shared strings.
173 | if (SharedStrings.Count > 0)
174 | {
175 | XmlNode sharedStrings = XmlRobloxFileWriter.WriteSharedStrings(doc, this);
176 | roblox.AppendChild(sharedStrings);
177 | }
178 |
179 | // Write the XML file.
180 | using (StringWriter buffer = new StringWriter())
181 | {
182 | XmlWriterSettings settings = XmlRobloxFileWriter.Settings;
183 |
184 | using (XmlWriter xmlWriter = XmlWriter.Create(buffer, settings))
185 | doc.WriteContentTo(xmlWriter);
186 |
187 | string result = buffer.ToString()
188 | .Replace("", "");
189 |
190 | using (BinaryWriter writer = new BinaryWriter(stream))
191 | {
192 | byte[] data = Encoding.UTF8.GetBytes(result);
193 | stream.SetLength(0);
194 | writer.Write(data);
195 | }
196 | }
197 | }
198 | }
199 | }
--------------------------------------------------------------------------------
/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------