diff --git a/.gitignore b/.gitignore index 227ea7a..081bed0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ obj/* .vs/* *.suo *.ide -*.user \ No newline at end of file +*.user +Program.cs \ No newline at end of file diff --git a/BinaryFormat/BinaryChunk.cs b/BinaryFormat/BinaryFileChunk.cs similarity index 70% rename from BinaryFormat/BinaryChunk.cs rename to BinaryFormat/BinaryFileChunk.cs index c5f50e2..4a8376d 100644 --- a/BinaryFormat/BinaryChunk.cs +++ b/BinaryFormat/BinaryFileChunk.cs @@ -6,46 +6,41 @@ using LZ4; namespace RobloxFiles.BinaryFormat { /// - /// BinaryRobloxChunk represents a generic LZ4-compressed chunk + /// BinaryRobloxFileChunk represents a generic LZ4-compressed chunk /// of data in Roblox's Binary File Format. /// - public class BinaryRobloxChunk + public class BinaryRobloxFileChunk { public readonly string ChunkType; + public readonly byte[] Reserved; public readonly int CompressedSize; public readonly int Size; - public readonly byte[] Reserved; - public readonly byte[] CompressedData; public readonly byte[] Data; public bool HasCompressedData => (CompressedSize > 0); + public BinaryRobloxFileReader GetDataReader() + { + MemoryStream buffer = new MemoryStream(Data); + return new BinaryRobloxFileReader(buffer); + } + public override string ToString() { return ChunkType + " Chunk [" + Size + " bytes]"; } - public BinaryRobloxReader GetReader(string chunkType) - { - if (ChunkType == chunkType) - { - MemoryStream buffer = new MemoryStream(Data); - return new BinaryRobloxReader(buffer); - } - - throw new Exception("Expected " + chunkType + " ChunkType from the input RobloxBinaryChunk"); - } - - public BinaryRobloxChunk(BinaryRobloxReader reader) + public BinaryRobloxFileChunk(BinaryRobloxFileReader reader) { byte[] bChunkType = reader.ReadBytes(4); ChunkType = Encoding.ASCII.GetString(bChunkType); CompressedSize = reader.ReadInt32(); Size = reader.ReadInt32(); + Reserved = reader.ReadBytes(4); if (HasCompressedData) diff --git a/BinaryFormat/BinaryReader.cs b/BinaryFormat/BinaryFileReader.cs similarity index 96% rename from BinaryFormat/BinaryReader.cs rename to BinaryFormat/BinaryFileReader.cs index 8e04d86..e6de267 100644 --- a/BinaryFormat/BinaryReader.cs +++ b/BinaryFormat/BinaryFileReader.cs @@ -5,9 +5,9 @@ using System.Text; namespace RobloxFiles.BinaryFormat { - public class BinaryRobloxReader : BinaryReader + public class BinaryRobloxFileReader : BinaryReader { - public BinaryRobloxReader(Stream stream) : base(stream) { } + public BinaryRobloxFileReader(Stream stream) : base(stream) { } private byte[] lastStringBuffer = new byte[0] { }; // Reads 'count * sizeof(T)' interleaved bytes and converts diff --git a/BinaryFormat/BinaryRobloxFile.cs b/BinaryFormat/BinaryRobloxFile.cs index 7c231ab..e1bbbb2 100644 --- a/BinaryFormat/BinaryRobloxFile.cs +++ b/BinaryFormat/BinaryRobloxFile.cs @@ -7,7 +7,7 @@ using RobloxFiles.BinaryFormat.Chunks; namespace RobloxFiles.BinaryFormat { - public class BinaryRobloxFile : IRobloxFile + public class BinaryRobloxFile : RobloxFile { // Header Specific public const string MagicHeader = " BinContents; - // Runtime Specific - public List Chunks = new List(); + public List Chunks = new List(); public override string ToString() => GetType().Name; public Instance[] Instances; @@ -30,11 +26,17 @@ namespace RobloxFiles.BinaryFormat public Dictionary Metadata; public Dictionary SharedStrings; + + internal BinaryRobloxFile() + { + Name = "BinaryRobloxFile"; + ParentLocked = true; + } - public void ReadFile(byte[] contents) + protected override void ReadFile(byte[] contents) { using (MemoryStream file = new MemoryStream(contents)) - using (BinaryRobloxReader reader = new BinaryRobloxReader(file)) + using (BinaryRobloxFileReader reader = new BinaryRobloxFileReader(file)) { // Verify the signature of the file. byte[] binSignature = reader.ReadBytes(14); @@ -59,7 +61,7 @@ namespace RobloxFiles.BinaryFormat { try { - BinaryRobloxChunk chunk = new BinaryRobloxChunk(reader); + BinaryRobloxFileChunk chunk = new BinaryRobloxFileChunk(reader); Chunks.Add(chunk); switch (chunk.ChunkType) @@ -88,7 +90,7 @@ namespace RobloxFiles.BinaryFormat reading = false; break; default: - Console.WriteLine("Unhandled chunk type: {0}!", chunk.ChunkType); + Console.WriteLine("BinaryRobloxFile: Unhandled chunk type: {0}!", chunk.ChunkType); Chunks.Remove(chunk); break; } @@ -101,7 +103,7 @@ namespace RobloxFiles.BinaryFormat } } - public void WriteFile(Stream stream) + public override void Save(Stream stream) { throw new NotImplementedException("Not implemented yet!"); } diff --git a/BinaryFormat/ChunkTypes/INST.cs b/BinaryFormat/Chunks/INST.cs similarity index 82% rename from BinaryFormat/ChunkTypes/INST.cs rename to BinaryFormat/Chunks/INST.cs index e51a852..2c97e54 100644 --- a/BinaryFormat/ChunkTypes/INST.cs +++ b/BinaryFormat/Chunks/INST.cs @@ -13,9 +13,9 @@ return TypeName; } - public INST(BinaryRobloxChunk chunk) + public INST(BinaryRobloxFileChunk chunk) { - using (BinaryRobloxReader reader = chunk.GetReader("INST")) + using (BinaryRobloxFileReader reader = chunk.GetDataReader()) { TypeIndex = reader.ReadInt32(); TypeName = reader.ReadString(); @@ -30,7 +30,7 @@ { foreach (int instId in InstanceIds) { - Instance inst = new Instance(TypeName); + Instance inst = new Instance() { ClassName = TypeName }; file.Instances[instId] = inst; } diff --git a/BinaryFormat/ChunkTypes/META.cs b/BinaryFormat/Chunks/META.cs similarity index 82% rename from BinaryFormat/ChunkTypes/META.cs rename to BinaryFormat/Chunks/META.cs index bff903a..a4d1bb2 100644 --- a/BinaryFormat/ChunkTypes/META.cs +++ b/BinaryFormat/Chunks/META.cs @@ -7,9 +7,9 @@ namespace RobloxFiles.BinaryFormat.Chunks public int NumEntries; public Dictionary Data = new Dictionary(); - public META(BinaryRobloxChunk chunk) + public META(BinaryRobloxFileChunk chunk) { - using (BinaryRobloxReader reader = chunk.GetReader("META")) + using (BinaryRobloxFileReader reader = chunk.GetDataReader()) { NumEntries = reader.ReadInt32(); diff --git a/BinaryFormat/ChunkTypes/PRNT.cs b/BinaryFormat/Chunks/PRNT.cs similarity index 69% rename from BinaryFormat/ChunkTypes/PRNT.cs rename to BinaryFormat/Chunks/PRNT.cs index 6cb941c..5c96cb0 100644 --- a/BinaryFormat/ChunkTypes/PRNT.cs +++ b/BinaryFormat/Chunks/PRNT.cs @@ -8,9 +8,9 @@ public readonly int[] ChildrenIds; public readonly int[] ParentIds; - public PRNT(BinaryRobloxChunk chunk) + public PRNT(BinaryRobloxFileChunk chunk) { - using (BinaryRobloxReader reader = chunk.GetReader("PRNT")) + using (BinaryRobloxFileReader reader = chunk.GetDataReader()) { Format = reader.ReadByte(); NumRelations = reader.ReadInt32(); @@ -28,14 +28,7 @@ int parentId = ParentIds[i]; Instance child = file.Instances[childId]; - Instance parent = null; - - if (parentId >= 0) - parent = file.Instances[parentId]; - else - parent = file.BinContents; - - child.Parent = parent; + child.Parent = (parentId >= 0 ? file.Instances[parentId] : file); } } } diff --git a/BinaryFormat/ChunkTypes/PROP.cs b/BinaryFormat/Chunks/PROP.cs similarity index 98% rename from BinaryFormat/ChunkTypes/PROP.cs rename to BinaryFormat/Chunks/PROP.cs index 069b286..0912618 100644 --- a/BinaryFormat/ChunkTypes/PROP.cs +++ b/BinaryFormat/Chunks/PROP.cs @@ -14,11 +14,11 @@ namespace RobloxFiles.BinaryFormat.Chunks public readonly int TypeIndex; public readonly PropertyType Type; - private BinaryRobloxReader Reader; + private BinaryRobloxFileReader Reader; - public PROP(BinaryRobloxChunk chunk) + public PROP(BinaryRobloxFileChunk chunk) { - Reader = chunk.GetReader("PROP"); + Reader = chunk.GetDataReader(); TypeIndex = Reader.ReadInt32(); Name = Reader.ReadString(); @@ -78,7 +78,7 @@ namespace RobloxFiles.BinaryFormat.Chunks // Leave an access point for the original byte sequence, in case this is a BinaryString. // This will allow the developer to read the sequence without any mangling from C# strings. byte[] buffer = Reader.GetLastStringBuffer(); - props[i].SetRawBuffer(buffer); + props[i].RawBuffer = buffer; return result; }); diff --git a/BinaryFormat/ChunkTypes/SSTR.cs b/BinaryFormat/Chunks/SSTR.cs similarity index 88% rename from BinaryFormat/ChunkTypes/SSTR.cs rename to BinaryFormat/Chunks/SSTR.cs index 7a897c0..46aa6e5 100644 --- a/BinaryFormat/ChunkTypes/SSTR.cs +++ b/BinaryFormat/Chunks/SSTR.cs @@ -11,9 +11,9 @@ namespace RobloxFiles.BinaryFormat.Chunks public Dictionary Lookup = new Dictionary(); public Dictionary Strings = new Dictionary(); - public SSTR(BinaryRobloxChunk chunk) + public SSTR(BinaryRobloxFileChunk chunk) { - using (BinaryRobloxReader reader = chunk.GetReader("SSTR")) + using (BinaryRobloxFileReader reader = chunk.GetDataReader()) { Version = reader.ReadInt32(); NumHashes = reader.ReadInt32(); diff --git a/Interfaces/IRobloxFile.cs b/Interfaces/IRobloxFile.cs deleted file mode 100644 index b28d5c9..0000000 --- a/Interfaces/IRobloxFile.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace RobloxFiles -{ - /// - /// Interface which represents a RobloxFile implementation. - /// - public interface IRobloxFile - { - Instance Contents { get; } - - void ReadFile(byte[] buffer); - void WriteFile(Stream stream); - } -} diff --git a/RobloxFile.cs b/RobloxFile.cs index e14b0ee..eb3a4c6 100644 --- a/RobloxFile.cs +++ b/RobloxFile.cs @@ -12,79 +12,36 @@ namespace RobloxFiles /// Represents a loaded *.rbxl/*.rbxm Roblox file. /// All of the surface-level Instances are stored in the RobloxFile's 'Contents' property. /// - public class RobloxFile : IRobloxFile + public abstract class RobloxFile : Instance { - /// - /// Indicates if this RobloxFile has loaded data already. - /// - public bool Initialized { get; private set; } + protected abstract void ReadFile(byte[] buffer); + public abstract void Save(Stream stream); /// - /// A reference to the inner IRobloxFile implementation that this RobloxFile opened with. - /// It can be a BinaryRobloxFile, or an XmlRobloxFile. + /// Opens a RobloxFile using the provided buffer. /// - public IRobloxFile InnerFile { get; private set; } - - /// - /// A reference to a Folder Instance that stores all of the contents that were loaded. - /// - public Instance Contents => InnerFile.Contents; - - /// - /// Initializes the RobloxFile from the provided buffer, if it hasn't been Initialized yet. - /// - /// - public void ReadFile(byte[] buffer) - { - if (!Initialized) - { - if (buffer.Length > 14) - { - string header = Encoding.UTF7.GetString(buffer, 0, 14); - IRobloxFile file = null; - - if (header == BinaryRobloxFile.MagicHeader) - file = new BinaryRobloxFile(); - else if (header.StartsWith(" - /// Creates a RobloxFile from a provided byte sequence that represents the file. - /// - /// - private RobloxFile(byte[] buffer) - { - ReadFile(buffer); - } - - /// - /// Opens a Roblox file from a byte sequence that represents the file. - /// - /// A byte sequence that represents the file. public static RobloxFile Open(byte[] buffer) { - return new RobloxFile(buffer); - } + if (buffer.Length > 14) + { + string header = Encoding.UTF7.GetString(buffer, 0, 14); + RobloxFile file = null; + if (header == BinaryRobloxFile.MagicHeader) + file = new BinaryRobloxFile(); + else if (header.StartsWith(" /// Opens a Roblox file by reading from a provided Stream. /// @@ -138,17 +95,5 @@ namespace RobloxFiles { return Task.Run(() => Open(filePath)); } - - /// - /// Allows you to access a child/descendant of this file's contents, and/or one of its properties. - /// The provided string should be a period-separated (.) path to what you wish to access. - /// This will throw an exception if any part of the path cannot be found. - /// - /// ~ Examples ~ - /// var terrain = robloxFile["Workspace.Terrain"] as Instance; - /// var currentCamera = robloxFile["Workspace.CurrentCamera"] as Property; - /// - /// - public object this[string accessor] => Contents[accessor]; } } diff --git a/RobloxFileFormat.csproj b/RobloxFileFormat.csproj index 2e4b406..d822947 100644 --- a/RobloxFileFormat.csproj +++ b/RobloxFileFormat.csproj @@ -62,14 +62,15 @@ - - + + - - - - - + + + + + + @@ -87,8 +88,7 @@ - - + @@ -101,38 +101,38 @@ - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tree/Instance.cs b/Tree/Instance.cs index 117f348..4ec8cf8 100644 --- a/Tree/Instance.cs +++ b/Tree/Instance.cs @@ -15,40 +15,23 @@ namespace RobloxFiles public string ClassName; /// A list of properties that are defined under this Instance. - public Dictionary Properties = new Dictionary(); + private Dictionary props = new Dictionary(); + public IReadOnlyDictionary Properties => props; private List Children = new List(); - private Instance rawParent; + private Instance parent; /// The name of this Instance, if a Name property is defined. - public string Name => ReadProperty("Name", ClassName); public override string ToString() => Name; - internal string XmlReferent; + /// A unique identifier for this instance when being serialized as XML. + public string XmlReferent { get; internal set; } - /// Creates an instance using the provided ClassName. - /// The ClassName to use for this Instance. - public Instance(string className = "Instance") - { - ClassName = className; - } + /// Indicates whether the parent of this object is locked. + public bool ParentLocked { get; protected set; } - /// Creates an instance using the provided ClassName and Name. - /// The ClassName to use for this Instance. - /// The Name to use for this Instance. - public Instance(string className = "Instance", string name = "Instance") - { - Property propName = new Property() - { - Type = PropertyType.String, - Instance = this, - Name = "Name", - Value = name, - }; - - ClassName = className; - AddProperty(ref propName); - } + /// Indicates whether this object should be serialized. + public bool Archivable = true; /// Returns true if this Instance is an ancestor to the provided Instance. /// The instance whose descendance will be tested against this Instance. @@ -72,6 +55,23 @@ namespace RobloxFiles return ancestor.IsAncestorOf(this); } + public string Name + { + get + { + Property propName = GetProperty("Name"); + + if (propName == null) + SetProperty("Name", "Instance"); + + return propName.Value.ToString(); + } + set + { + SetProperty("Name", value); + } + } + /// /// The parent of this Instance, or null if the instance is the root of a tree. /// Setting the value of this property will throw an exception if: @@ -82,21 +82,24 @@ namespace RobloxFiles { get { - return rawParent; + return parent; } set { + if (ParentLocked) + throw new Exception("The Parent property of this instance is locked."); + if (IsAncestorOf(value)) throw new Exception("Parent would result in circular reference."); if (Parent == this) throw new Exception("Attempt to set parent to self."); - if (rawParent != null) - rawParent.Children.Remove(this); + if (parent != null) + parent.Children.Remove(this); value.Children.Add(this); - rawParent = value; + parent = value; } } @@ -180,6 +183,11 @@ namespace RobloxFiles return ancestor; } + /// + /// Returns the first ancestor of this Instance whose ClassName is the provided string className. + /// If the instance is not found, this returns null. + /// + /// The Name of the Instance to find. public Instance FindFirstAncestorOfClass(string className) { Instance ancestor = Parent; @@ -196,7 +204,8 @@ namespace RobloxFiles } /// - /// Returns the first Instance whose ClassName is the provided string className. If the instance is not found, this returns null. + /// Returns the first Instance whose ClassName is the provided string className. + /// If the instance is not found, this returns null. /// /// The ClassName of the Instance to find. public Instance FindFirstChildOfClass(string className, bool recursive = false) @@ -228,21 +237,77 @@ namespace RobloxFiles } /// - /// Looks for a property with the specified property name, and returns it as an object. + /// Returns a Property object if a property with the specified name is defined in this Instance. + /// + public Property GetProperty(string name) + { + Property result = null; + + if (Properties.ContainsKey(name)) + result = Properties[name]; + + return result; + } + + /// + /// Finds or creates a property with the specified name, and sets its value to the provided object. + /// Returns the property object that had its value set, if the value is not null. + /// + public Property SetProperty(string name, object value, PropertyType? preferType = null) + { + Property prop = GetProperty(name) ?? new Property() + { + Type = preferType ?? PropertyType.Unknown, + Name = name + }; + + if (preferType == null) + { + object oldValue = prop.Value; + + Type oldType = oldValue?.GetType(); + Type newType = value?.GetType(); + + if (oldType != newType) + { + if (value == null) + { + RemoveProperty(name); + return prop; + } + + string typeName = newType.Name; + + if (value is Instance) + typeName = "Ref"; + else if (value is int) + typeName = "Int"; + else if (value is long) + typeName = "Int64"; + + Enum.TryParse(typeName, out prop.Type); + } + } + + prop.Value = value; + + if (prop.Instance == null) + AddProperty(ref prop); + + return prop; + } + + /// + /// Looks for a property with the specified property name, and returns its value as an object. /// The resulting value may be null if the property is not serialized. /// You can use the templated ReadProperty overload to fetch it as a specific type with a default value provided. /// /// The name of the property to be fetched from this Instance. /// An object reference to the value of the specified property, if it exists. - /// public object ReadProperty(string propertyName) { - Property property = null; - - if (Properties.ContainsKey(propertyName)) - property = Properties[propertyName]; - - return (property != null ? property.Value : null); + Property property = GetProperty(propertyName); + return property?.Value; } /// @@ -291,52 +356,39 @@ namespace RobloxFiles /// /// Adds a property by reference to this Instance's property list. - /// This is used during the file loading procedure. /// /// A reference to the property that will be added. internal void AddProperty(ref Property prop) { - Properties.Add(prop.Name, prop); + prop.Instance = this; + + if (prop.Name == "Name") + { + Property nameProp = GetProperty("Name"); + + if (nameProp != null) + { + nameProp.Value = prop.Value; + return; + } + } + + props.Add(prop.Name, prop); } /// - /// Allows you to access a child/descendant of this Instance, and/or one of its properties. - /// The provided string should be a period-separated (.) path to what you wish to access. - /// This will throw an exception if any part of the path cannot be found. - /// - /// ~ Examples ~ - /// var terrain = robloxFile["Workspace.Terrain"] as Instance; - /// var currentCamera = robloxFile["Workspace.CurrentCamera"] as Property; - /// + /// Removes a property with the provided name if a property with the provided name exists. /// - public object this[string accessor] + /// The name of the property to be removed. + /// True if a property with the provided name was removed. + public bool RemoveProperty(string name) { - get - { - Instance result = this; + Property prop = GetProperty(name); - foreach (string name in accessor.Split('.')) - { - Instance next = result.FindFirstChild(name); + if (prop != null) + prop.Instance = null; - if (next == null) - { - // Check if there is any property with this name. - Property prop = null; - - if (result.Properties.ContainsKey(name)) - prop = result.Properties[name]; - else - throw new Exception(name + " is not a valid member of " + result.Name); - - return prop; - } - - result = next; - } - - return result; - } + return props.Remove(name); } } } \ No newline at end of file diff --git a/Tree/Property.cs b/Tree/Property.cs index bc95504..f422573 100644 --- a/Tree/Property.cs +++ b/Tree/Property.cs @@ -39,13 +39,13 @@ namespace RobloxFiles public class Property { public string Name; - public Instance Instance; + public Instance Instance { get; internal set; } public PropertyType Type; public object Value; public string XmlToken = ""; - private byte[] RawBuffer = null; + public byte[] RawBuffer { get; internal set; } public bool HasRawBuffer { @@ -116,15 +116,5 @@ namespace RobloxFiles return string.Join(" ", typeName, Name, '=', valueLabel); } - - internal void SetRawBuffer(byte[] buffer) - { - RawBuffer = buffer; - } - - public byte[] GetRawBuffer() - { - return RawBuffer; - } } } \ No newline at end of file diff --git a/XmlFormat/XmlDataReader.cs b/XmlFormat/IO/XmlFileReader.cs similarity index 87% rename from XmlFormat/XmlDataReader.cs rename to XmlFormat/IO/XmlFileReader.cs index e06996d..80fd12d 100644 --- a/XmlFormat/XmlDataReader.cs +++ b/XmlFormat/IO/XmlFileReader.cs @@ -3,7 +3,7 @@ using System.Xml; namespace RobloxFiles.XmlFormat { - public static class XmlDataReader + public static class XmlRobloxFileReader { private static Func createErrorHandler(string label) { @@ -59,10 +59,12 @@ namespace RobloxFiles.XmlFormat if (tokenHandler != null) { - Property prop = new Property(); - prop.Name = propName.InnerText; - prop.Instance = instance; - prop.XmlToken = propType; + Property prop = new Property() + { + Name = propName.InnerText, + Instance = instance, + XmlToken = propType + }; if (!tokenHandler.ReadProperty(prop, propNode)) Console.WriteLine("Could not read property: " + prop.GetFullName() + '!'); @@ -88,19 +90,20 @@ namespace RobloxFiles.XmlFormat if (classToken == null) throw error("Got an Item without a defined 'class' attribute!"); - Instance inst = new Instance(classToken.InnerText); + Instance inst = new Instance() { ClassName = classToken.InnerText }; // The 'referent' attribute is optional, but should be defined if a Ref property needs to link to this Instance. XmlNode refToken = instNode.Attributes.GetNamedItem("referent"); if (refToken != null && file != null) { - string refId = refToken.InnerText; + string referent = refToken.InnerText; + inst.XmlReferent = referent; - if (file.Instances.ContainsKey(refId)) + if (file.Instances.ContainsKey(referent)) throw error("Got an Item with a duplicate 'referent' attribute!"); - file.Instances.Add(refId, inst); + file.Instances.Add(referent, inst); } // Process the child nodes of this instance. diff --git a/XmlFormat/XmlDataWriter.cs b/XmlFormat/IO/XmlFileWriter.cs similarity index 89% rename from XmlFormat/XmlDataWriter.cs rename to XmlFormat/IO/XmlFileWriter.cs index 67b8f4e..dc97deb 100644 --- a/XmlFormat/XmlDataWriter.cs +++ b/XmlFormat/IO/XmlFileWriter.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Xml; @@ -7,19 +8,18 @@ using RobloxFiles.XmlFormat.PropertyTokens; namespace RobloxFiles.XmlFormat { - public static class XmlDataWriter + public static class XmlRobloxFileWriter { - public static XmlWriterSettings Settings = new XmlWriterSettings() + public static readonly XmlWriterSettings Settings = new XmlWriterSettings() { Indent = true, IndentChars = "\t", NewLineChars = "\r\n", Encoding = Encoding.UTF8, - OmitXmlDeclaration = true, - NamespaceHandling = NamespaceHandling.Default + OmitXmlDeclaration = true }; - private static string CreateReferent() + public static string CreateReferent() { Guid referentGuid = Guid.NewGuid(); @@ -40,21 +40,18 @@ namespace RobloxFiles.XmlFormat foreach (Instance child in inst.GetChildren()) RecordInstances(file, child); - string referent = CreateReferent(); - file.Instances.Add(referent, inst); - inst.XmlReferent = referent; + if (inst.XmlReferent == "") + inst.XmlReferent = CreateReferent(); + + file.Instances.Add(inst.XmlReferent, inst); } public static XmlElement CreateRobloxElement(XmlDocument doc) { XmlElement roblox = doc.CreateElement("roblox"); - doc.AppendChild(roblox); - - roblox.SetAttribute("xmlns:xmime", "http://www.w3.org/2005/05/xmlmime"); - roblox.SetAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - roblox.SetAttribute("xsi:noNamespaceSchemaLocation", "http://www.roblox.com/roblox.xsd"); roblox.SetAttribute("version", "4"); - + doc.AppendChild(roblox); + XmlElement externalNull = doc.CreateElement("External"); roblox.AppendChild(externalNull); externalNull.InnerText = "null"; @@ -150,7 +147,7 @@ namespace RobloxFiles.XmlFormat instNode.AppendChild(propsNode); var props = instance.Properties; - + foreach (string propName in props.Keys) { Property prop = props[propName]; @@ -182,7 +179,7 @@ namespace RobloxFiles.XmlFormat string data = file.SharedStrings[md5]; byte[] buffer = Convert.FromBase64String(data); - bufferProp.SetRawBuffer(buffer); + bufferProp.RawBuffer = buffer; binaryWriter.WriteProperty(bufferProp, doc, sharedString); sharedStrings.AppendChild(sharedString); diff --git a/Interfaces/IXmlPropertyToken.cs b/XmlFormat/PropertyTokens/IXmlPropertyToken.cs similarity index 64% rename from Interfaces/IXmlPropertyToken.cs rename to XmlFormat/PropertyTokens/IXmlPropertyToken.cs index 52ec3d8..5a53f94 100644 --- a/Interfaces/IXmlPropertyToken.cs +++ b/XmlFormat/PropertyTokens/IXmlPropertyToken.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; +using System.Xml; namespace RobloxFiles.XmlFormat { diff --git a/XmlFormat/PropertyTokens/Axes.cs b/XmlFormat/PropertyTokens/Tokens/Axes.cs similarity index 100% rename from XmlFormat/PropertyTokens/Axes.cs rename to XmlFormat/PropertyTokens/Tokens/Axes.cs diff --git a/XmlFormat/PropertyTokens/BinaryString.cs b/XmlFormat/PropertyTokens/Tokens/BinaryString.cs similarity index 93% rename from XmlFormat/PropertyTokens/BinaryString.cs rename to XmlFormat/PropertyTokens/Tokens/BinaryString.cs index 4ad735c..ad5cd15 100644 --- a/XmlFormat/PropertyTokens/BinaryString.cs +++ b/XmlFormat/PropertyTokens/Tokens/BinaryString.cs @@ -15,14 +15,14 @@ namespace RobloxFiles.XmlFormat.PropertyTokens prop.Value = base64; byte[] buffer = Convert.FromBase64String(base64); - prop.SetRawBuffer(buffer); + prop.RawBuffer = buffer; return true; } public void WriteProperty(Property prop, XmlDocument doc, XmlNode node) { - byte[] data = prop.GetRawBuffer(); + byte[] data = prop.RawBuffer; string value = Convert.ToBase64String(data); if (value.Length > 72) diff --git a/XmlFormat/PropertyTokens/Boolean.cs b/XmlFormat/PropertyTokens/Tokens/Boolean.cs similarity index 100% rename from XmlFormat/PropertyTokens/Boolean.cs rename to XmlFormat/PropertyTokens/Tokens/Boolean.cs diff --git a/XmlFormat/PropertyTokens/BrickColor.cs b/XmlFormat/PropertyTokens/Tokens/BrickColor.cs similarity index 100% rename from XmlFormat/PropertyTokens/BrickColor.cs rename to XmlFormat/PropertyTokens/Tokens/BrickColor.cs diff --git a/XmlFormat/PropertyTokens/CFrame.cs b/XmlFormat/PropertyTokens/Tokens/CFrame.cs similarity index 100% rename from XmlFormat/PropertyTokens/CFrame.cs rename to XmlFormat/PropertyTokens/Tokens/CFrame.cs diff --git a/XmlFormat/PropertyTokens/Color3.cs b/XmlFormat/PropertyTokens/Tokens/Color3.cs similarity index 100% rename from XmlFormat/PropertyTokens/Color3.cs rename to XmlFormat/PropertyTokens/Tokens/Color3.cs diff --git a/XmlFormat/PropertyTokens/Color3uint8.cs b/XmlFormat/PropertyTokens/Tokens/Color3uint8.cs similarity index 100% rename from XmlFormat/PropertyTokens/Color3uint8.cs rename to XmlFormat/PropertyTokens/Tokens/Color3uint8.cs diff --git a/XmlFormat/PropertyTokens/ColorSequence.cs b/XmlFormat/PropertyTokens/Tokens/ColorSequence.cs similarity index 100% rename from XmlFormat/PropertyTokens/ColorSequence.cs rename to XmlFormat/PropertyTokens/Tokens/ColorSequence.cs diff --git a/XmlFormat/PropertyTokens/Content.cs b/XmlFormat/PropertyTokens/Tokens/Content.cs similarity index 96% rename from XmlFormat/PropertyTokens/Content.cs rename to XmlFormat/PropertyTokens/Tokens/Content.cs index 2bb7e60..514bbe8 100644 --- a/XmlFormat/PropertyTokens/Content.cs +++ b/XmlFormat/PropertyTokens/Tokens/Content.cs @@ -22,7 +22,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens { // Roblox technically doesn't support this anymore, but load it anyway :P byte[] buffer = Convert.FromBase64String(content); - prop.SetRawBuffer(buffer); + prop.RawBuffer = buffer; } } diff --git a/XmlFormat/PropertyTokens/Double.cs b/XmlFormat/PropertyTokens/Tokens/Double.cs similarity index 100% rename from XmlFormat/PropertyTokens/Double.cs rename to XmlFormat/PropertyTokens/Tokens/Double.cs diff --git a/XmlFormat/PropertyTokens/Enum.cs b/XmlFormat/PropertyTokens/Tokens/Enum.cs similarity index 100% rename from XmlFormat/PropertyTokens/Enum.cs rename to XmlFormat/PropertyTokens/Tokens/Enum.cs diff --git a/XmlFormat/PropertyTokens/Faces.cs b/XmlFormat/PropertyTokens/Tokens/Faces.cs similarity index 100% rename from XmlFormat/PropertyTokens/Faces.cs rename to XmlFormat/PropertyTokens/Tokens/Faces.cs diff --git a/XmlFormat/PropertyTokens/Float.cs b/XmlFormat/PropertyTokens/Tokens/Float.cs similarity index 100% rename from XmlFormat/PropertyTokens/Float.cs rename to XmlFormat/PropertyTokens/Tokens/Float.cs diff --git a/XmlFormat/PropertyTokens/Int.cs b/XmlFormat/PropertyTokens/Tokens/Int.cs similarity index 98% rename from XmlFormat/PropertyTokens/Int.cs rename to XmlFormat/PropertyTokens/Tokens/Int.cs index c6d0d0c..2223036 100644 --- a/XmlFormat/PropertyTokens/Int.cs +++ b/XmlFormat/PropertyTokens/Tokens/Int.cs @@ -20,8 +20,6 @@ namespace RobloxFiles.XmlFormat.PropertyTokens { return XmlPropertyTokens.ReadPropertyGeneric(prop, PropertyType.Int, token); } - - } public void WriteProperty(Property prop, XmlDocument doc, XmlNode node) diff --git a/XmlFormat/PropertyTokens/Int64.cs b/XmlFormat/PropertyTokens/Tokens/Int64.cs similarity index 100% rename from XmlFormat/PropertyTokens/Int64.cs rename to XmlFormat/PropertyTokens/Tokens/Int64.cs diff --git a/XmlFormat/PropertyTokens/NumberRange.cs b/XmlFormat/PropertyTokens/Tokens/NumberRange.cs similarity index 100% rename from XmlFormat/PropertyTokens/NumberRange.cs rename to XmlFormat/PropertyTokens/Tokens/NumberRange.cs diff --git a/XmlFormat/PropertyTokens/NumberSequence.cs b/XmlFormat/PropertyTokens/Tokens/NumberSequence.cs similarity index 100% rename from XmlFormat/PropertyTokens/NumberSequence.cs rename to XmlFormat/PropertyTokens/Tokens/NumberSequence.cs diff --git a/XmlFormat/PropertyTokens/PhysicalProperties.cs b/XmlFormat/PropertyTokens/Tokens/PhysicalProperties.cs similarity index 99% rename from XmlFormat/PropertyTokens/PhysicalProperties.cs rename to XmlFormat/PropertyTokens/Tokens/PhysicalProperties.cs index b1ccc6a..2950156 100644 --- a/XmlFormat/PropertyTokens/PhysicalProperties.cs +++ b/XmlFormat/PropertyTokens/Tokens/PhysicalProperties.cs @@ -78,6 +78,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens XmlElement element = doc.CreateElement(elementType); element.InnerText = value.ToInvariantString(); + node.AppendChild(element); } } diff --git a/XmlFormat/PropertyTokens/Ray.cs b/XmlFormat/PropertyTokens/Tokens/Ray.cs similarity index 97% rename from XmlFormat/PropertyTokens/Ray.cs rename to XmlFormat/PropertyTokens/Tokens/Ray.cs index ef1acfb..7c3c98d 100644 --- a/XmlFormat/PropertyTokens/Ray.cs +++ b/XmlFormat/PropertyTokens/Tokens/Ray.cs @@ -22,7 +22,7 @@ namespace RobloxFiles.XmlFormat.PropertyTokens read[i] = Vector3Token.ReadVector3(fieldToken); } - Vector3 origin = read[0], + Vector3 origin = read[0], direction = read[1]; Ray ray = new Ray(origin, direction); @@ -42,11 +42,12 @@ namespace RobloxFiles.XmlFormat.PropertyTokens Ray ray = prop.Value as Ray; XmlElement origin = doc.CreateElement("origin"); - Vector3Token.WriteVector3(doc, origin, ray.Origin); - node.AppendChild(origin); - XmlElement direction = doc.CreateElement("direction"); + + Vector3Token.WriteVector3(doc, origin, ray.Origin); Vector3Token.WriteVector3(doc, direction, ray.Direction); + + node.AppendChild(origin); node.AppendChild(direction); } } diff --git a/XmlFormat/PropertyTokens/Rect.cs b/XmlFormat/PropertyTokens/Tokens/Rect.cs similarity index 100% rename from XmlFormat/PropertyTokens/Rect.cs rename to XmlFormat/PropertyTokens/Tokens/Rect.cs diff --git a/XmlFormat/PropertyTokens/Ref.cs b/XmlFormat/PropertyTokens/Tokens/Ref.cs similarity index 100% rename from XmlFormat/PropertyTokens/Ref.cs rename to XmlFormat/PropertyTokens/Tokens/Ref.cs diff --git a/XmlFormat/PropertyTokens/SharedString.cs b/XmlFormat/PropertyTokens/Tokens/SharedString.cs similarity index 100% rename from XmlFormat/PropertyTokens/SharedString.cs rename to XmlFormat/PropertyTokens/Tokens/SharedString.cs diff --git a/XmlFormat/PropertyTokens/String.cs b/XmlFormat/PropertyTokens/Tokens/String.cs similarity index 100% rename from XmlFormat/PropertyTokens/String.cs rename to XmlFormat/PropertyTokens/Tokens/String.cs diff --git a/XmlFormat/PropertyTokens/UDim.cs b/XmlFormat/PropertyTokens/Tokens/UDim.cs similarity index 100% rename from XmlFormat/PropertyTokens/UDim.cs rename to XmlFormat/PropertyTokens/Tokens/UDim.cs diff --git a/XmlFormat/PropertyTokens/UDim2.cs b/XmlFormat/PropertyTokens/Tokens/UDim2.cs similarity index 100% rename from XmlFormat/PropertyTokens/UDim2.cs rename to XmlFormat/PropertyTokens/Tokens/UDim2.cs diff --git a/XmlFormat/PropertyTokens/Vector2.cs b/XmlFormat/PropertyTokens/Tokens/Vector2.cs similarity index 100% rename from XmlFormat/PropertyTokens/Vector2.cs rename to XmlFormat/PropertyTokens/Tokens/Vector2.cs diff --git a/XmlFormat/PropertyTokens/Vector3.cs b/XmlFormat/PropertyTokens/Tokens/Vector3.cs similarity index 100% rename from XmlFormat/PropertyTokens/Vector3.cs rename to XmlFormat/PropertyTokens/Tokens/Vector3.cs diff --git a/XmlFormat/PropertyTokens/Vector3int16.cs b/XmlFormat/PropertyTokens/Tokens/Vector3int16.cs similarity index 100% rename from XmlFormat/PropertyTokens/Vector3int16.cs rename to XmlFormat/PropertyTokens/Tokens/Vector3int16.cs diff --git a/XmlFormat/XmlPropertyTokens.cs b/XmlFormat/PropertyTokens/XmlPropertyTokens.cs similarity index 98% rename from XmlFormat/XmlPropertyTokens.cs rename to XmlFormat/PropertyTokens/XmlPropertyTokens.cs index ac56c20..4e31090 100644 --- a/XmlFormat/XmlPropertyTokens.cs +++ b/XmlFormat/PropertyTokens/XmlPropertyTokens.cs @@ -4,8 +4,6 @@ using System.ComponentModel; using System.Linq; using System.Xml; -using RobloxFiles; - namespace RobloxFiles.XmlFormat { public static class XmlPropertyTokens @@ -60,7 +58,6 @@ namespace RobloxFiles.XmlFormat } prop.Type = propType; - return true; } catch diff --git a/XmlFormat/XmlRobloxFile.cs b/XmlFormat/XmlRobloxFile.cs index 6616de2..ddf2eba 100644 --- a/XmlFormat/XmlRobloxFile.cs +++ b/XmlFormat/XmlRobloxFile.cs @@ -8,23 +8,22 @@ using System.Xml; namespace RobloxFiles.XmlFormat { - public class XmlRobloxFile : IRobloxFile + public class XmlRobloxFile : RobloxFile { - // IRobloxFile - internal readonly Instance XmlContents = new Instance("Folder", "XmlRobloxFile"); - public Instance Contents => XmlContents; - // Runtime Specific public readonly XmlDocument Root = new XmlDocument(); - public Dictionary Instances = new Dictionary(); - public Dictionary SharedStrings = new Dictionary(); - - public void ReadFile(byte[] buffer) - { - Instances.Clear(); - SharedStrings.Clear(); + internal Dictionary Instances = new Dictionary(); + internal Dictionary SharedStrings = new Dictionary(); + internal XmlRobloxFile() + { + Name = "XmlRobloxFile"; + ParentLocked = true; + } + + protected override void ReadFile(byte[] buffer) + { try { string xml = Encoding.UTF8.GetString(buffer); @@ -53,22 +52,22 @@ namespace RobloxFiles.XmlFormat { if (child.Name == "Item") { - Instance item = XmlDataReader.ReadInstance(child, this); - item.Parent = XmlContents; + Instance item = XmlRobloxFileReader.ReadInstance(child, this); + item.Parent = this; } else if (child.Name == "SharedStrings") { - XmlDataReader.ReadSharedStrings(child, this); + XmlRobloxFileReader.ReadSharedStrings(child, this); } } // Query the properties. - var props = Instances.Values + var allProps = Instances.Values .SelectMany(inst => inst.Properties) .Select(pair => pair.Value); // Resolve referent properties. - var refProps = props.Where(prop => prop.Type == PropertyType.Ref); + var refProps = allProps.Where(prop => prop.Type == PropertyType.Ref); foreach (Property refProp in refProps) { @@ -87,7 +86,7 @@ namespace RobloxFiles.XmlFormat } // Resolve shared strings. - var sharedProps = props.Where(prop => prop.Type == PropertyType.SharedString); + var sharedProps = allProps.Where(prop => prop.Type == PropertyType.SharedString); foreach (Property sharedProp in sharedProps) { @@ -99,13 +98,13 @@ namespace RobloxFiles.XmlFormat sharedProp.Value = value; byte[] data = Convert.FromBase64String(value); - sharedProp.SetRawBuffer(data); - - continue; + sharedProp.RawBuffer = data; + } + else + { + string name = sharedProp.GetFullName(); + Console.WriteLine("XmlRobloxFile: Could not resolve shared string for {0}", name); } - - string name = sharedProp.GetFullName(); - Console.WriteLine("XmlRobloxFile: Could not resolve shared string for {0}", name); } } else @@ -114,38 +113,41 @@ namespace RobloxFiles.XmlFormat } } - public void WriteFile(Stream stream) + public override void Save(Stream stream) { XmlDocument doc = new XmlDocument(); - XmlElement roblox = XmlDataWriter.CreateRobloxElement(doc); + + XmlElement roblox = doc.CreateElement("roblox"); + roblox.SetAttribute("version", "4"); + doc.AppendChild(roblox); Instances.Clear(); SharedStrings.Clear(); - Instance[] topLevelItems = Contents.GetChildren(); + Instance[] children = GetChildren(); // First, record all of the instances. - foreach (Instance inst in topLevelItems) - XmlDataWriter.RecordInstances(this, inst); + foreach (Instance inst in children) + XmlRobloxFileWriter.RecordInstances(this, inst); // Now append them into the document. - foreach (Instance inst in Contents.GetChildren()) + foreach (Instance inst in children) { - XmlNode instNode = XmlDataWriter.WriteInstance(inst, doc, this); + XmlNode instNode = XmlRobloxFileWriter.WriteInstance(inst, doc, this); roblox.AppendChild(instNode); } // Append the shared strings. if (SharedStrings.Count > 0) { - XmlNode sharedStrings = XmlDataWriter.WriteSharedStrings(doc, this); + XmlNode sharedStrings = XmlRobloxFileWriter.WriteSharedStrings(doc, this); roblox.AppendChild(sharedStrings); } // Write the XML file. using (StringWriter buffer = new StringWriter()) { - XmlWriterSettings settings = XmlDataWriter.Settings; + XmlWriterSettings settings = XmlRobloxFileWriter.Settings; using (XmlWriter xmlWriter = XmlWriter.Create(buffer, settings)) doc.WriteContentTo(xmlWriter);