rbx-reader-rts
Version:
A modern TypeScript library for parsing Roblox binary files (.rbxm, .rbxl) in Node.js and the browser. Provides utilities to extract Animation and Sound asset IDs and is easily extensible.
144 lines (138 loc) • 5.96 kB
text/typescript
import BinaryParser from './BinaryParser';
import ByteReader from './ByteReader';
import Instance, { InstanceRoot } from './Instance';
import { toXmlString } from './XmlSerializer';
import { updateAnimationIds } from './utils';
/**
* Options controlling how asset extraction is performed. By default both
* animation and sound ID extraction are enabled. Additional custom
* extractors may be provided; each extractor receives an instance and
* returns an array of numeric IDs to include in the final result.
*/
export interface ParseOptions {
/** When true (default), extract animation IDs from recognised properties. */
extractAnimations?: boolean;
/** When true (default), extract sound IDs from recognised properties. */
extractSounds?: boolean;
/** User defined extractors keyed by name. Each extractor is called for
* every instance in the parsed model and should return any asset IDs it
* discovers. The IDs will be collected under the corresponding key in
* the returned AssetExtractionResults. */
additionalExtractors?: { [name: string]: (instance: Instance) => number[] };
}
/**
* Results returned from parseBuffer(). In addition to the root instance
* hierarchy and the flat list of all instances, any discovered asset IDs
* are grouped into categories. The builtin categories are animationIds
* and soundIds; custom extractors will appear as additional keys.
*/
export interface AssetExtractionResults {
animationIds: number[];
soundIds: number[];
[key: string]: number[];
}
export interface ParseResult {
root: InstanceRoot;
instances: Instance[];
assets: AssetExtractionResults;
}
/**
* Extract all sequences of three or more digits from a string. These
* sequences correspond to Roblox asset identifiers which are always
* numeric. Duplicate IDs are not filtered here; callers should use a
* Set to eliminate duplicates.
*/
function extractIdsFromString(value: string): number[] {
const ids: number[] = [];
const re = /\d{3,}/g;
let match: RegExpExecArray | null;
while ((match = re.exec(value)) !== null) {
// Parse as integer; Roblox asset IDs can be up to 64 bits so use base 10
const num = parseInt(match[0], 10);
// Only push if parse succeeded
if (!Number.isNaN(num)) ids.push(num);
}
return ids;
}
/**
* Parse a Roblox binary buffer (.rbxm or .rbxl) and return its instance
* hierarchy along with any extracted asset references. This function
* handles only the binary format; XML formatted files are not supported.
*/
export function parseBuffer(buffer: ArrayBuffer, options: ParseOptions = {}): ParseResult {
const { extractAnimations = true, extractSounds = true, additionalExtractors = {} } = options;
const { result: root, instances } = BinaryParser.parse(buffer);
// Use Sets to deduplicate IDs
const animationSet = new Set<number>();
const soundSet = new Set<number>();
const customSets: { [key: string]: Set<number> } = {};
for (const key of Object.keys(additionalExtractors)) {
customSets[key] = new Set<number>();
}
// Inspect each instance for relevant properties
for (const inst of instances) {
// Builtin extractors
for (const [propName, descriptor] of Object.entries(inst.Properties)) {
const value = descriptor.value;
if (typeof value === 'string' || typeof value === 'number') {
const valueStr = String(value);
// AnimationId property detection
if (extractAnimations) {
const nameLower = propName.toLowerCase();
// Recognise both explicit AnimationId properties and generic "animation" patterns
if (nameLower.includes('animationid') || (inst.ClassName.toLowerCase() === 'animation' && nameLower.includes('id'))) {
for (const id of extractIdsFromString(valueStr)) animationSet.add(id);
}
}
// SoundId property detection
if (extractSounds) {
const nameLower = propName.toLowerCase();
if (nameLower.includes('soundid') || (inst.ClassName.toLowerCase() === 'sound' && nameLower.includes('id'))) {
for (const id of extractIdsFromString(valueStr)) soundSet.add(id);
}
}
} else if (Array.isArray(value)) {
// Some properties may hold nested arrays (e.g. Color3). Scan
// primitive elements for numeric strings just in case the asset ID
// lurks there (unlikely but harmless).
const flatten = (arr: any[]): string[] => {
const out: string[] = [];
for (const elem of arr) {
if (Array.isArray(elem)) out.push(...flatten(elem));
else if (typeof elem === 'string' || typeof elem === 'number') out.push(String(elem));
}
return out;
};
const strings = flatten(value);
for (const s of strings) {
if (extractAnimations && /animation/.test(propName.toLowerCase())) {
for (const id of extractIdsFromString(s)) animationSet.add(id);
}
if (extractSounds && /sound/.test(propName.toLowerCase())) {
for (const id of extractIdsFromString(s)) soundSet.add(id);
}
}
}
}
// Run user supplied extractors on this instance
for (const key of Object.keys(additionalExtractors)) {
const extractor = additionalExtractors[key];
const ids = extractor(inst);
if (ids && Array.isArray(ids)) {
for (const id of ids) {
if (typeof id === 'number' && !Number.isNaN(id)) customSets[key].add(id);
}
}
}
}
// Construct final asset results object
const assets: AssetExtractionResults = {
animationIds: Array.from(animationSet),
soundIds: Array.from(soundSet)
};
for (const key of Object.keys(customSets)) {
assets[key] = Array.from(customSets[key]);
}
return { root, instances, assets };
}
export { Instance, InstanceRoot, BinaryParser, ByteReader, toXmlString, updateAnimationIds };