diff --git a/BinaryFormat/Chunks/SSTR.cs b/BinaryFormat/Chunks/SSTR.cs index 80ac007..38e1709 100644 --- a/BinaryFormat/Chunks/SSTR.cs +++ b/BinaryFormat/Chunks/SSTR.cs @@ -29,8 +29,8 @@ namespace RobloxFiles.BinaryFormat.Chunks byte[] data = reader.ReadBuffer(); SharedString value = SharedString.FromBuffer(data); - Lookup.Add(key, id); - Strings.Add(id, value); + Lookup[key] = id; + Strings[id] = value; } file.SSTR = this; diff --git a/DataTypes/CFrame.cs b/DataTypes/CFrame.cs index 6955c44..3aa776c 100644 --- a/DataTypes/CFrame.cs +++ b/DataTypes/CFrame.cs @@ -170,6 +170,25 @@ namespace RobloxFiles.DataTypes } } + public override bool Equals(object obj) + { + if (obj is CFrame) + { + CFrame other = obj as CFrame; + + float[] a = GetComponents(); + float[] b = other.GetComponents(); + + for (int i = 0; i < 12; i++) + if (!a[i].FuzzyEquals(b[i])) + return false; + + return true; + } + + return base.Equals(obj); + } + public static CFrame operator +(CFrame a, Vector3 b) { float[] ac = a.GetComponents(); diff --git a/DataTypes/Color3.cs b/DataTypes/Color3.cs index 90ea391..254bca2 100644 --- a/DataTypes/Color3.cs +++ b/DataTypes/Color3.cs @@ -14,6 +14,15 @@ namespace RobloxFiles.DataTypes B = b; } + public override int GetHashCode() + { + int r = R.GetHashCode(), + g = G.GetHashCode(), + b = B.GetHashCode(); + + return (r ^ g ^ b); + } + internal Color3(Attribute attr) { R = attr.readFloat(); diff --git a/DataTypes/Color3uint8.cs b/DataTypes/Color3uint8.cs index d173359..1031f8e 100644 --- a/DataTypes/Color3uint8.cs +++ b/DataTypes/Color3uint8.cs @@ -16,6 +16,11 @@ B = b; } + public override int GetHashCode() + { + return (R << 24) | (G << 8) | B; + } + public static implicit operator Color3(Color3uint8 color) { float r = color.R / 255f; diff --git a/DataTypes/ColorSequence.cs b/DataTypes/ColorSequence.cs index a22048e..68fa610 100644 --- a/DataTypes/ColorSequence.cs +++ b/DataTypes/ColorSequence.cs @@ -21,10 +21,11 @@ namespace RobloxFiles.DataTypes public ColorSequence(Color3 c0, Color3 c1) { - ColorSequenceKeypoint a = new ColorSequenceKeypoint(0, c0); - ColorSequenceKeypoint b = new ColorSequenceKeypoint(1, c1); - - Keypoints = new ColorSequenceKeypoint[2] { a, b }; + Keypoints = new ColorSequenceKeypoint[2] + { + new ColorSequenceKeypoint(0, c0), + new ColorSequenceKeypoint(1, c1) + }; } public ColorSequence(ColorSequenceKeypoint[] keypoints) diff --git a/DataTypes/ColorSequenceKeypoint.cs b/DataTypes/ColorSequenceKeypoint.cs index 33652c4..cff98f4 100644 --- a/DataTypes/ColorSequenceKeypoint.cs +++ b/DataTypes/ColorSequenceKeypoint.cs @@ -3,12 +3,13 @@ public class ColorSequenceKeypoint { public readonly float Time; - public readonly Color3 Value; + public readonly Color3uint8 Value; public readonly int Envelope; public override string ToString() { - return $"{Time} {Value.R} {Value.G} {Value.B} {Envelope}"; + Color3 Color = Value; + return $"{Time} {Color.R} {Color.G} {Color.B} {Envelope}"; } public ColorSequenceKeypoint(float time, Color3 value, int envelope = 0) diff --git a/DataTypes/SharedString.cs b/DataTypes/SharedString.cs index e924d27..0550d2c 100644 --- a/DataTypes/SharedString.cs +++ b/DataTypes/SharedString.cs @@ -59,7 +59,7 @@ namespace RobloxFiles.DataTypes public static SharedString FromBuffer(byte[] buffer) { - return new SharedString(buffer); + return new SharedString(buffer ?? Array.Empty()); } public static SharedString FromString(string value) diff --git a/DataTypes/Vector3.cs b/DataTypes/Vector3.cs index 43bdb93..251db23 100644 --- a/DataTypes/Vector3.cs +++ b/DataTypes/Vector3.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using RobloxFiles.Enums; namespace RobloxFiles.DataTypes @@ -65,6 +66,36 @@ namespace RobloxFiles.DataTypes return new Vector3(coords); } + public override bool Equals(object obj) + { + if (obj is Vector3) + { + Vector3 other = obj as Vector3; + + if (!X.FuzzyEquals(other.X)) + return false; + + if (!Y.FuzzyEquals(other.Y)) + return false; + + if (!Z.FuzzyEquals(other.Z)) + return false; + + return true; + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + int x = X.GetHashCode(), + y = Y.GetHashCode(), + z = Z.GetHashCode(); + + return x ^ y ^ z; + } + private delegate Vector3 Operator(Vector3 a, Vector3 b); private static Vector3 upcastFloatOp(Vector3 vec, float num, Operator upcast) diff --git a/Generated/Classes.cs b/Generated/Classes.cs index 7e2eb27..4b7345d 100644 --- a/Generated/Classes.cs +++ b/Generated/Classes.cs @@ -2251,7 +2251,9 @@ namespace RobloxFiles { public ModelLevelOfDetail LevelOfDetail = ModelLevelOfDetail.Automatic; public CFrame ModelInPrimary = new CFrame(); + public CFrame ModelMeshCFrame = new CFrame(); public byte[] ModelMeshData = Array.Empty(); + public Vector3 ModelMeshSize = new Vector3(); public BasePart PrimaryPart; } diff --git a/RobloxFileFormat.csproj b/RobloxFileFormat.csproj index da3da51..333feb3 100644 --- a/RobloxFileFormat.csproj +++ b/RobloxFileFormat.csproj @@ -121,6 +121,7 @@ + diff --git a/RobloxFileFormat.dll b/RobloxFileFormat.dll index 354882e..b625642 100644 Binary files a/RobloxFileFormat.dll and b/RobloxFileFormat.dll differ diff --git a/Tree/Instance.cs b/Tree/Instance.cs index 74a651a..49121d2 100644 --- a/Tree/Instance.cs +++ b/Tree/Instance.cs @@ -530,11 +530,44 @@ namespace RobloxFiles if (fieldName.EndsWith("_")) fieldName = instType.Name; + string xmlToken = fieldType.Name; + + if (fieldType.IsEnum) + xmlToken = "token"; + + switch (xmlToken) + { + case "String": + case "Double": + xmlToken = xmlToken.ToLowerInvariant(); + break; + case "Boolean": + xmlToken = "bool"; + break; + case "Single": + xmlToken = "float"; + break; + case "Int32": + xmlToken = "int"; + break; + case "Int64": + xmlToken = "int64"; + break; + case "Rect": + xmlToken = "Rect2D"; + break; + case "CFrame": + xmlToken = "CoordinateFrame"; + break; + default: break; + } + if (!props.ContainsKey(fieldName)) { Property newProp = new Property() { Value = field.GetValue(this), + XmlToken = xmlToken, Name = fieldName, Type = propType, Instance = this @@ -546,6 +579,7 @@ namespace RobloxFiles { Property prop = props[fieldName]; prop.Value = field.GetValue(this); + prop.XmlToken = xmlToken; prop.Type = propType; } } diff --git a/Tree/Property.cs b/Tree/Property.cs index da959c0..a3b54fe 100644 --- a/Tree/Property.cs +++ b/Tree/Property.cs @@ -100,35 +100,45 @@ namespace RobloxFiles private void ImproviseRawBuffer() { - if (RawValue is SharedString) - { - var sharedString = CastValue(); - RawBuffer = sharedString.SharedValue; - return; - } - else if (RawValue is ProtectedString) - { - var protectedString = CastValue(); - RawBuffer = protectedString.RawBuffer; - return; - } - else if (RawValue is byte[]) + if (RawValue is byte[]) { RawBuffer = RawValue as byte[]; return; } + + if (RawValue is SharedString) + { + var sharedString = CastValue(); + + if (sharedString != null) + { + RawBuffer = sharedString.SharedValue; + return; + } + } + + if (RawValue is ProtectedString) + { + var protectedString = CastValue(); + + if (protectedString != null) + { + RawBuffer = protectedString.RawBuffer; + return; + } + } switch (Type) { case PropertyType.Int: RawBuffer = BitConverter.GetBytes((int)Value); break; - case PropertyType.Int64: - RawBuffer = BitConverter.GetBytes((long)Value); - break; case PropertyType.Bool: RawBuffer = BitConverter.GetBytes((bool)Value); break; + case PropertyType.Int64: + RawBuffer = BitConverter.GetBytes((long)Value); + break; case PropertyType.Float: RawBuffer = BitConverter.GetBytes((float)Value); break; diff --git a/Utility/DefaultProperty.cs b/Utility/DefaultProperty.cs new file mode 100644 index 0000000..5c18cf7 --- /dev/null +++ b/Utility/DefaultProperty.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace RobloxFiles.Utility +{ + static class DefaultProperty + { + private static Dictionary ClassMap; + private static HashSet Refreshed = new HashSet(); + + static DefaultProperty() + { + var Instance = typeof(Instance); + var assembly = Assembly.GetExecutingAssembly(); + + var classes = assembly.GetTypes() + .Where(type => !type.IsAbstract && Instance.IsAssignableFrom(type)) + .Select(type => Activator.CreateInstance(type)) + .Cast(); + + ClassMap = classes.ToDictionary(inst => inst.ClassName); + } + + public static object Get(string className, string propName) + { + if (!ClassMap.ContainsKey(className)) + return null; + + Instance inst = ClassMap[className]; + + if (!Refreshed.Contains(inst)) + { + inst.RefreshProperties(); + Refreshed.Add(inst); + } + + var props = inst.Properties; + + if (!props.ContainsKey(propName)) + return null; + + var prop = props[propName]; + return prop.Value; + } + + public static object Get(Instance inst, string propName) + { + return Get(inst.ClassName, propName); + } + + public static object Get(Instance inst, Property prop) + { + return Get(inst.ClassName, prop.Name); + } + + public static object Get(string className, Property prop) + { + return Get(className, prop.Name); + } + } +} diff --git a/XmlFormat/IO/XmlFileWriter.cs b/XmlFormat/IO/XmlFileWriter.cs index e019b3a..f0c3c44 100644 --- a/XmlFormat/IO/XmlFileWriter.cs +++ b/XmlFormat/IO/XmlFileWriter.cs @@ -6,6 +6,7 @@ using System.Text; using System.Xml; using RobloxFiles.DataTypes; +using RobloxFiles.Utility; using RobloxFiles.XmlFormat.PropertyTokens; namespace RobloxFiles.XmlFormat @@ -41,12 +42,11 @@ namespace RobloxFiles.XmlFormat internal static void RecordInstances(XmlRobloxFile file, Instance inst) { + inst.Referent = "RBX" + file.RefCounter++; + foreach (Instance child in inst.GetChildren()) RecordInstances(file, child); - if (inst.Referent == null || inst.Referent.Length < 35) - inst.Referent = CreateReferent(); - file.Instances.Add(inst.Referent, inst); } @@ -100,15 +100,21 @@ namespace RobloxFiles.XmlFormat if (prop.Type == PropertyType.SharedString) { - SharedString value = prop.CastValue(); - - if (value.ComputedKey == null) + SharedString str = prop.CastValue(); + + if (str == null) { - var newShared = SharedString.FromBuffer(value.SharedValue); - value.Key = newShared.ComputedKey; + byte[] value = prop.CastValue(); + str = SharedString.FromBuffer(value); + } + + if (str.ComputedKey == null) + { + var newShared = SharedString.FromBuffer(str.SharedValue); + str.Key = newShared.ComputedKey; } - file.SharedStrings.Add(value.Key); + file.SharedStrings.Add(str.Key); } XmlElement propElement = doc.CreateElement(propType); @@ -148,12 +154,43 @@ namespace RobloxFiles.XmlFormat foreach (string propName in orderedKeys) { Property prop = props[propName]; - XmlNode propNode = WriteProperty(prop, doc, file); + bool isDefault = false; - if (propNode == null) - continue; + object a = DefaultProperty.Get(instance, prop); + object b = prop.Value; - propsNode.AppendChild(propNode); + if (a is float) + { + float f0 = (float)a, + f1 = (float)b; + + isDefault = f0.FuzzyEquals(f1); + } + else if (a is double) + { + double d0 = (double)a, + d1 = (double)b; + + isDefault = d0.FuzzyEquals(d1); + } + else if (b != null) + { + isDefault = b.Equals(a); + } + else if (a == b) + { + isDefault = true; + } + + if (!isDefault) + { + XmlNode propNode = WriteProperty(prop, doc, file); + + if (propNode == null) + continue; + + propsNode.AppendChild(propNode); + } } foreach (Instance child in instance.GetChildren()) diff --git a/XmlFormat/Tokens/SharedString.cs b/XmlFormat/Tokens/SharedString.cs index ff210d4..8c2415e 100644 --- a/XmlFormat/Tokens/SharedString.cs +++ b/XmlFormat/Tokens/SharedString.cs @@ -18,16 +18,20 @@ namespace RobloxFiles.XmlFormat.PropertyTokens public void WriteProperty(Property prop, XmlDocument doc, XmlNode node) { - var value = prop.CastValue(); - string key = value.Key; + var value = prop.Value as SharedString; - if (value.ComputedKey == null) + if (value != null) { - var newShared = SharedString.FromBuffer(value.SharedValue); - key = newShared.ComputedKey; - } + string key = value.Key; - node.InnerText = key; + if (value.ComputedKey == null) + { + var newShared = SharedString.FromBuffer(value.SharedValue); + key = newShared.ComputedKey; + } + + node.InnerText = key; + } } } } diff --git a/XmlFormat/XmlRobloxFile.cs b/XmlFormat/XmlRobloxFile.cs index ab072b3..f9a932b 100644 --- a/XmlFormat/XmlRobloxFile.cs +++ b/XmlFormat/XmlRobloxFile.cs @@ -20,6 +20,7 @@ namespace RobloxFiles private Dictionary RawMetadata = new Dictionary(); public Dictionary Metadata => RawMetadata; + internal int RefCounter = 0; public XmlRobloxFile() { @@ -121,11 +122,12 @@ namespace RobloxFiles public override void Save(Stream stream) { XmlDocument doc = new XmlDocument(); - + XmlElement roblox = doc.CreateElement("roblox"); roblox.SetAttribute("version", "4"); doc.AppendChild(roblox); - + + RefCounter = 0; Instances.Clear(); SharedStrings.Clear(); diff --git a/packages.config b/packages.config index 146db44..83bf735 100644 --- a/packages.config +++ b/packages.config @@ -4,5 +4,6 @@ + \ No newline at end of file