using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Reflection; using System.Text; namespace RobloxFiles { /// /// Describes an object in Roblox's DataModel hierarchy. /// Instances can have sets of properties loaded from *.rbxl/*.rbxm files. /// public class Instance { public Instance() { Name = ClassName; } /// The ClassName of this Instance. public string ClassName => GetType().Name; /// Internal list of properties that are under this Instance. private Dictionary props = new Dictionary(); /// A list of properties that are defined under this Instance. public IReadOnlyDictionary Properties => props; /// The raw list of children for this Instance. internal HashSet Children = new HashSet(); /// The raw value of the Instance's parent. private Instance RawParent; /// The name of this Instance. public string Name; /// Indicates whether this Instance should be serialized. public bool Archivable = true; /// The name of this Instance, if a Name property is defined. public override string ToString() => Name; /// A unique identifier for this instance when being serialized. public string Referent { get; internal set; } /// Indicates whether the parent of this object is locked. public bool ParentLocked { get; internal set; } /// Indicates whether this Instance is a Service. public bool IsService { get; internal set; } /// A list of CollectionService tags assigned to this Instance. public List Tags { get; } = new List(); /// The attributes defined for this Instance. public Attributes Attributes { get; private set; } /// The internal serialized data of this Instance's attributes internal byte[] AttributesSerialize { get { return Attributes?.Serialize() ?? Array.Empty(); } set { MemoryStream data = new MemoryStream(value); Attributes = new Attributes(data); } } /// /// Internal format of the Instance's CollectionService tags. /// Property objects will look to this member for serializing the Tags property. /// internal byte[] SerializedTags { get { string fullString = string.Join("\0", Tags.ToArray()); byte[] buffer = fullString.ToCharArray() .Select(ch => (byte)ch) .ToArray(); return buffer; } set { int length = value.Length; List buffer = new List(); Tags.Clear(); for (int i = 0; i < length; i++) { byte id = value[i]; if (id != 0) buffer.Add(id); if (id == 0 || i == (length - 1)) { byte[] data = buffer.ToArray(); buffer.Clear(); string tag = Encoding.UTF8.GetString(data); Tags.Add(tag); } } } } /// Returns true if this Instance is an ancestor to the provided Instance. /// The instance whose descendance will be tested against this Instance. public bool IsAncestorOf(Instance descendant) { while (descendant != null) { if (descendant == this) return true; descendant = descendant.Parent; } return false; } /// Returns true if this Instance is a descendant of the provided Instance. /// The instance whose ancestry will be tested against this Instance. public bool IsDescendantOf(Instance ancestor) { Contract.Requires(ancestor != null); return ancestor.IsAncestorOf(this); } /// /// Returns true if the provided instance inherits from the provided instance type. /// [Obsolete("Use the `is` operator instead.")] public bool IsA() where T : Instance { Type myType = GetType(); Type classType = typeof(T); return classType.IsAssignableFrom(myType); } /// /// Attempts to cast this Instance to an inherited class of type ''. /// Returns null if the instance cannot be casted to the provided type. /// /// The type of Instance to cast to. /// The instance as the type '' if it can be converted, or null. public T Cast() where T : Instance { T result = null; if (this is T) result = this as T; return result; } /// /// 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: /// - The parent is currently locked. /// - The value is set to itself. /// - The value is a descendant of the Instance. /// public Instance Parent { get { return RawParent; } 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."); RawParent?.Children.Remove(this); value?.Children.Add(this); RawParent = value; } } /// /// Returns an array containing all the children of this Instance. /// public Instance[] GetChildren() { return Children.ToArray(); } /// /// Returns an array containing all the children of this Instance, whose type is ''. /// public T[] GetChildrenOfType() where T : Instance { T[] ofType = GetChildren() .Where(child => child is T) .Cast() .ToArray(); return ofType; } /// /// Returns an array containing all the descendants of this Instance. /// public Instance[] GetDescendants() { var results = new List(); foreach (Instance child in Children) { // Add this child to the results. results.Add(child); // Add its descendants to the results. Instance[] descendants = child.GetDescendants(); results.AddRange(descendants); } return results.ToArray(); } /// /// Returns an array containing all the descendants of this Instance, whose type is ''. /// public T[] GetDescendantsOfType() where T : Instance { T[] ofType = GetDescendants() .Where(desc => desc is T) .Cast() .ToArray(); return ofType; } /// /// Returns the first child of this Instance whose Name is the provided string name. /// If the instance is not found, this returns null. /// /// The Name of the Instance to find. /// Indicates if we should search descendants as well. public T FindFirstChild(string name, bool recursive = false) where T : Instance { T result = null; var query = Children .Where(child => child is T) .Where(child => name == child.Name) .Cast(); if (query.Any()) { result = query.First(); } else if (recursive) { foreach (Instance child in Children) { T found = child.FindFirstChild(name, true); if (found != null) { result = found; break; } } } return result; } /// /// Returns the first child of this Instance whose Name is the provided string name. /// If the instance is not found, this returns null. /// /// The Name of the Instance to find. /// Indicates if we should search descendants as well. public Instance FindFirstChild(string name, bool recursive = false) { return FindFirstChild(name, recursive); } /// /// Returns the first ancestor of this Instance whose Name is the provided string name. /// If the instance is not found, this returns null. /// /// The Name of the Instance to find. public T FindFirstAncestor(string name) where T : Instance { Instance ancestor = Parent; while (ancestor != null) { if (ancestor is T && ancestor.Name == name) return ancestor as T; ancestor = ancestor.Parent; } return null; } /// /// Returns the first ancestor of this Instance whose Name is the provided string name. /// If the instance is not found, this returns null. /// /// The Name of the Instance to find. public Instance FindFirstAncestor(string name) { return FindFirstAncestor(name); } /// /// 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 T FindFirstAncestorOfClass() where T : Instance { Instance ancestor = Parent; while (ancestor != null) { if (ancestor is T) return ancestor as T; ancestor = ancestor.Parent; } return null; } /// /// Returns the first ancestor of this Instance which derives from the provided type . /// If the instance is not found, this returns null. /// /// The Name of the Instance to find. public T FindFirstAncestorWhichIsA() where T : Instance { T ancestor = null; Instance check = Parent; while (check != null) { if (check is T) { ancestor = check as T; break; } check = check.Parent; } return ancestor; } /// /// 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 T FindFirstChildOfClass(bool recursive = false) where T : Instance { var query = Children .Where(child => child is T) .Cast(); T result = null; if (query.Any()) { result = query.First(); } else if (recursive) { foreach (Instance child in Children) { T found = child.FindFirstChildOfClass(true); if (found != null) { result = found; break; } } } return result; } /// /// Returns the first child of this Instance which derives from the provided type . /// If the instance is not found, this returns null. /// /// Whether this should search descendants as well. public T FindFirstChildWhichIsA(bool recursive = false) where T : Instance { var query = Children .Where(child => child is T) .Cast(); if (query.Any()) return query.First(); if (recursive) { foreach (Instance child in Children) { T found = child.FindFirstChildWhichIsA(true); if (found == null) continue; return found; } } return null; } /// /// Returns a string describing the index traversal of this Instance, starting from its root ancestor. /// public string GetFullName(string separator = ".") { string fullName = Name; Instance at = Parent; while (at != null) { fullName = at.Name + separator + fullName; at = at.Parent; } return fullName; } /// /// Returns a Property object whose name is the provided string name. /// public Property GetProperty(string name) { Property result = null; if (props.ContainsKey(name)) result = props[name]; return result; } /// /// Adds a property by reference to this Instance's property list. /// /// A reference to the property that will be added. internal void AddProperty(ref Property prop) { prop.Instance = this; if (props.ContainsKey(prop.Name)) props.Remove(prop.Name); props.Add(prop.Name, prop); } /// /// Removes a property with the provided name if a property with the provided name exists. /// /// The name of the property to be removed. /// True if a property with the provided name was removed. internal bool RemoveProperty(string name) { if (props.ContainsKey(name)) { Property prop = Properties[name]; prop.Instance = null; } return props.Remove(name); } /// /// Ensures that all serializable properties of this Instance have /// a registered Property object with the correct PropertyType. /// internal IReadOnlyDictionary RefreshProperties() { Type instType = GetType(); FieldInfo[] fields = instType.GetFields(Property.BindingFlags); foreach (FieldInfo field in fields) { string fieldName = field.Name; Type fieldType = field.FieldType; if (field.GetCustomAttribute() != null) continue; if (fieldName == "Archivable" || fieldName.EndsWith("k__BackingField")) continue; else if (fieldName == "Bevel_Roundness") fieldName = "Bevel Roundness"; PropertyType propType = PropertyType.Unknown; if (Property.Types.ContainsKey(fieldType)) propType = Property.Types[fieldType]; else if (fieldType.IsEnum) propType = PropertyType.Enum; if (propType != PropertyType.Unknown) { if (fieldName.EndsWith("_")) fieldName = instType.Name; string xmlToken = fieldType.Name; if (fieldType.IsEnum) xmlToken = "token"; switch (xmlToken) { case "String": case "Double": case "Int64": xmlToken = xmlToken.ToLowerInvariant(); break; case "Boolean": xmlToken = "bool"; break; case "Single": xmlToken = "float"; break; case "Int32": xmlToken = "int"; break; case "Rect": xmlToken = "Rect2D"; break; case "CFrame": xmlToken = "CoordinateFrame"; break; default: break; } if (!props.ContainsKey(fieldName)) { var newProp = new Property() { Value = field.GetValue(this), XmlToken = xmlToken, Name = fieldName, Type = propType, Instance = this }; AddProperty(ref newProp); } else { Property prop = props[fieldName]; prop.Value = field.GetValue(this); prop.XmlToken = xmlToken; prop.Type = propType; } } } Property tags = GetProperty("Tags"); Property attributes = GetProperty("AttributesSerialize"); if (tags == null) { tags = new Property("Tags", PropertyType.String); AddProperty(ref tags); } if (attributes == null) { attributes = new Property("AttributesSerialize", PropertyType.String); AddProperty(ref attributes); } return Properties; } } }