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(); const soundSet = new Set(); const customSets: { [key: string]: Set } = {}; for (const key of Object.keys(additionalExtractors)) { customSets[key] = new Set(); } // 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 };