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;
}
}
}