import assert from 'assert'; /** * The Instance class is a lightweight representation of an object in a * Roblox place or model. Each instance has a class name, a name, an * optional parent, a list of children and a set of arbitrary properties. * Instances can form a tree and provide utilities for traversing that tree. */ export class Instance { /** * Child instances belonging to this instance. */ public readonly Children: Instance[] = []; /** * A dictionary of properties keyed by their name. Each entry holds the * type of the property and its value. Types mirror the DataTypes found * in Roblox binary files (e.g. string, bool, int, float, Instance, etc.). */ public readonly Properties: { [key: string]: { type: string; value: any } } = {}; /** * A dictionary of attribute values attached to this instance. Attributes * are exposed as plain JavaScript values. This library does not * currently attempt to decode the attribute serialization format present * in .rbxm/.rbxl files; consumers may populate this dictionary manually. */ public Attributes: { [key: string]: any } = {}; /** * Create a new Instance of the given class. The Name property * defaults to the literal string "Instance" and the Parent property * defaults to undefined. Use setProperty() to adjust these values. */ constructor(className: string) { assert(typeof className === 'string', 'className must be a string'); // initialise core properties this.setProperty('ClassName', className, 'string'); this.setProperty('Name', 'Instance', 'string'); this.setProperty('Parent', undefined, 'Instance'); } /** * Convenience factory to create a new instance. */ static new(className: string): Instance { return new Instance(className); } /** * Getter for the ClassName property. */ get ClassName(): string { return this.Properties['ClassName'].value; } /** * Getter for the Name property. */ get Name(): string { return this.Properties['Name'].value; } /** * Getter for the Parent property. */ get Parent(): Instance | InstanceRoot | undefined { return this.Properties['Parent'].value; } /** * Set a property on this instance. If the property already exists its * type must match the previously specified type. Certain properties * receive special treatment: when setting the Parent property the * Children arrays of the old and new parent are updated accordingly. */ setProperty(name: string, value: any, type?: string): void { // Attempt to infer a type if none is provided if (!type) { if (typeof value === 'boolean') type = 'bool'; else if (value instanceof Instance || value instanceof InstanceRoot) type = 'Instance'; else if (typeof value === 'string') type = 'string'; else if (typeof value === 'number') type = 'number'; else throw new TypeError('You need to specify property type since it cannot be inferred'); } let descriptor = this.Properties[name]; if (descriptor) { assert(descriptor.type === type, `Property type mismatch: ${type} !== ${descriptor.type}`); // handle removal from previous parent if (name === 'Parent' && descriptor.value instanceof Instance) { const idx = descriptor.value.Children.indexOf(this); if (idx !== -1) descriptor.value.Children.splice(idx, 1); } descriptor.value = value; } else { descriptor = this.Properties[name] = { type, value }; } if (name === 'Parent' && value instanceof Instance) { value.Children.push(this); } // Mirror non-reserved properties onto the instance for convenience if (name !== 'Children' && name !== 'Properties' && !(name in Object.getPrototypeOf(this))) { (this as any)[name] = value; } } /** * Retrieve the value of a property by name. If caseInsensitive is * true then a case insensitive lookup will be performed. */ getProperty(name: string, caseInsensitive = false): any { const descriptor = this.Properties[name] || (caseInsensitive && Object.entries(this.Properties).find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1]); return descriptor ? descriptor.value : undefined; } /** * Return this instance's children. */ getChildren(): Instance[] { return this.Children; } /** * Recursively walk the instance tree and return all descendants. */ getDescendants(): Instance[] { const list: Instance[] = []; const stack = [...this.Children]; while (stack.length > 0) { const inst = stack.shift()!; list.push(inst); stack.push(...inst.Children); } return list; } /** * Recursively find the first child of this instance with a given name. */ findFirstChild(name: string, recursive = false): Instance | undefined { for (const child of this.Children) { if (child.getProperty('Name') === name) return child; } if (recursive) { for (const child of this.Children) { const found = child.findFirstChild(name, true); if (found) return found; } } return undefined; } /** * Recursively find the first child of this instance with a given class name. */ findFirstChildOfClass(className: string, recursive = false): Instance | undefined { for (const child of this.Children) { if (child.getProperty('ClassName') === className) return child; } if (recursive) { for (const child of this.Children) { const found = child.findFirstChildOfClass(className, true); if (found) return found; } } return undefined; } /** * Compute the dotted path of this instance by traversing parents up to the root. */ getFullName(): string { if (this.Parent && !(this.Parent instanceof InstanceRoot)) { return `${(this.Parent as Instance).getFullName()}.${this.Name}`; } else { return `${this.Name}`; } } /** * Test whether this instance defines a property with the given name. */ hasProperty(name: string, caseInsensitive = false): boolean { if (name in this.Properties) return true; if (caseInsensitive) { return !!Object.keys(this.Properties).find((k) => k.toLowerCase() === name.toLowerCase()); } return false; } /** * Retrieve the value of an attribute by name. */ getAttribute(name: string): any { return this.Attributes[name]; } } /** * The InstanceRoot class is simply an array of Instance objects with a few * helper methods. It acts as the top-level container returned by the * parser. The root itself is not an actual Roblox instance; it is a * convenience wrapper that exposes findFirstChild and findFirstChildOfClass * on the root container. */ export class InstanceRoot extends Array { getChildren(): Instance[] { return this as Instance[]; } getDescendants(): Instance[] { const desc: Instance[] = [...this]; for (const inst of this) { desc.push(...inst.getDescendants()); } return desc; } findFirstChild(name: string, recursive = false): Instance | undefined { for (const child of this) { if (child.getProperty('Name') === name) return child; } if (recursive) { for (const child of this) { const found = child.findFirstChild(name, true); if (found) return found; } } return undefined; } findFirstChildOfClass(className: string, recursive = false): Instance | undefined { for (const child of this) { if (child.getProperty('ClassName') === className) return child; } if (recursive) { for (const child of this) { const found = child.findFirstChildOfClass(className, true); if (found) return found; } } return undefined; } } export default Instance;