using System; using System.Collections.Generic; 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; RefreshProperties(); } /// The ClassName of this Instance. public string ClassName => GetType().Name; /// Internal list of properties that are under this Instance. private readonly 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 unsafe value of the Instance's parent. private Instance ParentUnsafe; /// The name of this Instance. public string Name; /// Indicates whether this Instance should be serialized. public bool Archivable = true; /// The source AssetId this instance was created in. public long SourceAssetId = -1; /// A unique identifier declared for this instance. public UniqueId UniqueId; /// The name of this Instance, if a Name property is defined. public override string ToString() => Name; /// A context-dependent unique identifier for this instance when being serialized. public string Referent { get; 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; } /// Indicates whether this Instance has been destroyed. public bool Destroyed { get; internal set; } /// A hashset of CollectionService tags assigned to this Instance. public readonly HashSet Tags = new HashSet(); /// The public readonly access point of the attributes on this Instance. public readonly RbxAttributes Attributes = new RbxAttributes(); /// The internal serialized data of this Instance's attributes internal byte[] AttributesSerialize { get => Attributes.Save(); set => Attributes.Load(value); } /// /// Internal format of the Instance's CollectionService tags. /// Property objects will look to this member for serializing the Tags property. /// internal byte[] SerializedTags { get { if (Tags == null) // ????? return null; if (Tags.Count == 0) return null; string fullString = string.Join("\0", Tags); char[] buffer = fullString.ToCharArray(); return Encoding.UTF8.GetBytes(buffer); } set { int length = value.Length; var 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); } } } } /// /// Attempts to get the value of an attribute whose type is T. /// Returns false if no attribute was found with that type. /// /// The expected type of the attribute. /// The name of the attribute. /// The out value to set. /// True if the attribute could be read and the out value was set, false otherwise. public bool GetAttribute(string key, out T value) { if (Attributes.TryGetValue(key, out RbxAttribute attr)) { if (attr?.Value is T result) { value = result; return true; } } value = default; return false; } /// /// Attempts to set an attribute to the provided value. The provided key must be no longer than 100 characters. /// Returns false if the key is too long or the provided type is not supported by Roblox. /// If an attribute with the provide key already exists, it will be overwritten. /// /// /// The name of the attribute. /// The value to be assigned to the attribute. /// True if the attribute was set, false otherwise. public bool SetAttribute(string key, T value) { if (key.Length > 100) return false; if (key.StartsWith("RBX", StringComparison.InvariantCulture)) return false; if (value == null) { Attributes[key] = null; return true; } Type type = value.GetType(); if (!RbxAttribute.SupportsType(type)) return false; var attr = new RbxAttribute(value); Attributes[key] = attr; return true; } /// 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) { return ancestor?.IsAncestorOf(this) ?? false; } /// /// Returns true if the provided instance inherits from the provided instance type. /// [Obsolete("Use the `is` operator instead.")] public bool IsA() where T : Instance { return this is T; } /// /// 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. [Obsolete("Use the `as` operator instead.")] public T Cast() where T : Instance { return this as T; } /// /// 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 => ParentUnsafe; set { if (ParentLocked) { string newParent = value?.Name ?? "NULL", currParent = Parent?.Name ?? "NULL"; throw new InvalidOperationException($"The Parent property of {Name} is locked, current parent: {currParent}, new parent {newParent}"); } if (IsAncestorOf(value)) { string pathA = GetFullName("."), pathB = value.GetFullName("."); throw new InvalidOperationException($"Attempt to set parent of {pathA} to {pathB} would result in circular reference"); } if (Parent == this) throw new InvalidOperationException($"Attempt to set {Name} as its own parent"); ParentUnsafe?.Children.Remove(this); value?.Children.Add(this); ParentUnsafe = 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; } /// /// Disposes of this instance and its descendants, and locks its parent. /// All property bindings, tags, and attributes are cleared. /// public void Destroy() { Destroyed = true; props.Clear(); Parent = null; ParentLocked = true; Tags.Clear(); Attributes.Clear(); while (Children.Any()) { var child = Children.First(); child.Destroy(); } } /// /// Creates a deep copy of this instance and its descendants. /// Any instances that have Archivable set to false are not included. /// This can include the instance itself, in which case this will return null. /// public Instance Clone() { var mitosis = new Dictionary(); var refProps = new List(); var insts = GetDescendants().ToList(); insts.Insert(0, this); foreach (var oldInst in insts) { if (!oldInst.Archivable) continue; var type = oldInst.GetType(); var newInst = Activator.CreateInstance(type) as Instance; foreach (var pair in oldInst.Properties) { // Create memberwise copy of the property. var oldProp = pair.Value; var newProp = new Property() { Instance = newInst, Name = oldProp.Name, Type = oldProp.Type, Value = oldProp.Value, XmlToken = oldProp.XmlToken, }; if (newProp.Type == PropertyType.Ref) refProps.Add(newProp); newInst.AddProperty(ref newProp); } var oldParent = oldInst.Parent; mitosis[oldInst] = newInst; if (oldParent == null) continue; if (!mitosis.TryGetValue(oldParent, out var newParent)) continue; newInst.Parent = newParent; } // Patch referents where applicable. foreach (var prop in refProps) { if (!(prop.Value is Instance source)) continue; if (!mitosis.TryGetValue(source, out var copy)) continue; prop.Value = copy; } // Grab the copy of ourselves that we created. mitosis.TryGetValue(this, out Instance clone); return clone; } /// /// 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) { string name = prop.Name; RemoveProperty(name); prop.Instance = this; props.Add(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; // A few specific edge case hacks. I wish these didn't need to exist :( if (fieldName == "Archivable" || fieldName.EndsWith("k__BackingField")) continue; else if (fieldName == "Bevel_Roundness") fieldName = "Bevel Roundness"; PropertyType propType = PropertyType.Unknown; if (fieldType.IsEnum) propType = PropertyType.Enum; else if (Property.Types.ContainsKey(fieldType)) propType = Property.Types[fieldType]; else if (typeof(Instance).IsAssignableFrom(fieldType)) propType = PropertyType.Ref; if (propType != PropertyType.Unknown) { if (fieldName.EndsWith("_")) fieldName = instType.Name; string xmlToken = fieldType.Name; if (fieldType.IsEnum) xmlToken = "token"; else if (propType == PropertyType.Ref) xmlToken = "Ref"; 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; } case "FontFace": { xmlToken = "Font"; break; } case "Optional`1": { // TODO: If more optional types are added, // this needs disambiguation. xmlToken = "OptionalCoordinateFrame"; 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; } } }