├── .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 | --------------------------------------------------------------------------------