using System;
using System.Collections.Generic;
using System.Linq;
namespace RobloxFiles
{
///
/// Describes an object in Roblox's DataModel hierarchy.
/// Instances can have sets of properties loaded from *.rbxl/*.rbxm files.
///
public class Instance
{
/// The ClassName of this Instance.
public readonly string ClassName;
/// A list of properties that are defined under this Instance.
public Dictionary Properties = new Dictionary();
private List Children = new List();
private Instance rawParent;
/// The name of this Instance, if a Name property is defined.
public string Name => ReadProperty("Name", ClassName);
public override string ToString() => Name;
/// Creates an instance using the provided ClassName.
/// The ClassName to use for this Instance.
public Instance(string className = "Instance")
{
ClassName = className;
}
/// 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);
}
/// 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);
}
///
/// 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 value is set to itself.
/// - The value is a descendant of the Instance.
///
public Instance Parent
{
get
{
return rawParent;
}
set
{
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);
value.Children.Add(this);
rawParent = value;
}
}
///
/// Returns a snapshot of the Instances currently parented to this Instance, as an array.
///
public Instance[] GetChildren()
{
return Children.ToArray();
}
///
/// Returns a snapshot of the Instances that are descendants of this Instance, as an array.
///
public Instance[] GetDescendants()
{
List 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 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)
{
Instance result = null;
var query = Children.Where((child) => name == child.Name);
if (query.Count() > 0)
{
result = query.First();
}
else if (recursive)
{
foreach (Instance child in Children)
{
Instance found = child.FindFirstChild(name, true);
if (found != null)
{
result = found;
break;
}
}
}
return result;
}
///
/// 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)
{
Instance ancestor = Parent;
while (ancestor != null)
{
if (ancestor.Name == name)
break;
ancestor = ancestor.Parent;
}
return ancestor;
}
public Instance FindFirstAncestorOfClass(string className)
{
Instance ancestor = Parent;
while (ancestor != null)
{
if (ancestor.ClassName == className)
break;
ancestor = ancestor.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 Instance FindFirstChildOfClass(string className, bool recursive = false)
{
Instance result = null;
var query = Children.Where((child) => className == child.ClassName);
if (query.Count() > 0)
result = query.First();
return result;
}
///
/// Returns a string descrbing the index traversal of this Instance, starting from its root ancestor.
///
public string GetFullName()
{
string fullName = Name;
Instance at = Parent;
while (at != null)
{
fullName = at.Name + '.' + fullName;
at = at.Parent;
}
return fullName;
}
///
/// Looks for a property with the specified property name, and returns it 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);
}
///
/// Looks for a property with the specified property name, and returns it as the specified type.
/// If it cannot be converted, the provided nullFallback value will be returned instead.
///
/// The value type to convert to when finding the specified property name.
/// The name of the property to be fetched from this Instance.
/// A fallback value to be returned if casting to T fails, or the property is not found.
///
public T ReadProperty(string propertyName, T nullFallback)
{
try
{
object result = ReadProperty(propertyName);
return (T)result;
}
catch
{
return nullFallback;
}
}
///
/// Looks for a property with the specified property name. If found, it will try to set the value of the referenced outValue to its value.
/// Returns true if the property was found and its value was casted to the referenced outValue.
/// If it returns false, the outValue will not have its value set.
///
/// The value type to convert to when finding the specified property name.
/// The name of the property to be fetched from this Instance.
/// The value to write to if the property can be casted to T correctly.
public bool TryReadProperty(string propertyName, ref T outValue)
{
try
{
object result = ReadProperty(propertyName);
outValue = (T)result;
return true;
}
catch
{
return false;
}
}
///
/// 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.
public void AddProperty(ref Property prop)
{
Properties.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;
///
///
public object this[string accessor]
{
get
{
Instance result = this;
foreach (string name in accessor.Split('.'))
{
Instance next = result.FindFirstChild(name);
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;
}
}
}
}