import { Instance, InstanceRoot } from './Instance'; /** * Escape a string for inclusion in XML. Only the minimal set of characters * are escaped; this is sufficient for Roblox XML files. */ function escapeXml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Serialize an Instance tree to a minimal Roblox XML (.rbxmx/.rbxlx) representation. * This function produces an XML string that can be saved as a .rbxmx/.rbxlx file. * * Note: Only primitive property types are serialized (string/number/boolean). * Other types (Vector3, CFrame, etc.) are converted to their string * representation via `toString()`. SharedString and Attributes are ignored. * * @param root The root InstanceRoot returned by parseBuffer(). */ /** * Convert the provided Instance tree into a Roblox XML representation. The * resulting XML is intended to be saved as a .rbxmx or .rbxlx file. This * serializer attempts to follow the structure produced by Roblox Studio as * closely as practical. Each Instance becomes an element with a * class and referent attribute; properties are serialized using the * appropriate tag based on their type. Unknown property types fall back * to string representation. Note: this serializer does not include * attributes or custom shared string tables. * * @param root The root container returned by parseBuffer(). */ export function toXmlString(root: InstanceRoot): string { // Assign referent identifiers to every instance. Roblox XML uses // "R" strings to refer to instances in property values. The order // here follows the order returned by getDescendants() so parents are // guaranteed to have a lower referent than their children. We skip the // InstanceRoot itself and only refer to actual instances. const referentMap = new Map(); let nextRef = 0; function assignRefs(inst: Instance): void { nextRef++; referentMap.set(inst, 'R' + nextRef); inst.Children.forEach(assignRefs); } root.getChildren().forEach(assignRefs); // Serialize an individual property based on its type. Returns an XML // fragment without indentation. Unknown or unsupported types fall back // to string serialization. function serializeProperty(name: string, type: string, value: any): string | null { if (value === undefined || value === null) return null; const propName = escapeXml(name); switch (type) { case 'string': { return `${escapeXml(String(value))}`; } case 'bool': { return `${value ? 'true' : 'false'}`; } case 'int': { return `${value}`; } case 'float': case 'double': { return `${value}`; } case 'UDim': { // Value is [scale, offset] const [scale, offset] = value as [number, number]; // Roblox uses Scale, Offset as child tags on UDim return `${scale}${offset}`; } case 'UDim2': { // Value is [[sx, ox], [sy, oy]] const [[sx, ox], [sy, oy]] = value as [[number, number], [number, number]]; return `${sx}${ox}${sy}${oy}`; } case 'Vector2': { const [x, y] = value as [number, number]; return `${x},${y}`; } case 'Vector3': { const [x, y, z] = value as [number, number, number]; return `${x},${y},${z}`; } case 'Color3': { const [r, g, b] = value as [number, number, number]; return `${r},${g},${b}`; } case 'Color3uint8': { const [r, g, b] = value as [number, number, number]; return `${r},${g},${b}`; } case 'BrickColor': { return `${value}`; } case 'CFrame': { // Value is an array of 12 numbers [x,y,z,r00,r01,r02,r10,r11,r12,r20,r21,r22] const arr = value as number[]; return `${arr.join(',')}`; } case 'Enum': { // Without reflection metadata we cannot convert to token names. Output as integer. return `${value}`; } case 'Instance': { // Value is another Instance; replace with its referent if present const ref = referentMap.get(value as Instance); if (ref) return `${ref}`; return null; } case 'NumberSequence': { // Value is array of keys with Time, Value, Envelope const seq = value as Array<{ Time: number; Value: number; Envelope: number }>; let xml = ``; for (const key of seq) { xml += ``; } xml += ``; return xml; } case 'ColorSequence': { const seq = value as Array<{ Time: number; Color: [number, number, number]; EnvelopeMaybe: number }>; let xml = ``; for (const key of seq) { const [r, g, b] = key.Color; const env = key.EnvelopeMaybe; xml += ``; } xml += ``; return xml; } case 'NumberRange': { const { Min, Max } = value as { Min: number; Max: number }; return `${Min},${Max}`; } case 'Rect2D': { const [x0, y0, x1, y1] = value as [number, number, number, number]; return `${x0},${y0},${x1},${y1}`; } case 'PhysicalProperties': { const phys = value as any; let xml = ``; xml += `${phys.CustomPhysics ? 'true' : 'false'}`; if (phys.CustomPhysics) { xml += `${phys.Density}${phys.Friction}${phys.Elasticity}`; xml += `${phys.FrictionWeight}${phys.ElasticityWeight}`; } xml += ``; return xml; } case 'int64': { return `${value}`; } case 'SharedString': { // SharedStrings in binary files are already decoded to strings. Encode as base64. let b64: string; if (typeof Buffer !== 'undefined') { // Node environment b64 = Buffer.from(value as string).toString('base64'); } else { // Browser fallback using btoa; encodeURIComponent ensures UTF-8 correctness const utf8 = encodeURIComponent(value as string).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))); b64 = btoa(utf8); } return `${b64}`; } case 'UniqueId': { return `${value}`; } default: { // Fallback: stringify unknown types return `${escapeXml(String(value))}`; } } } // Recursively serialize an instance and its children. Each Item includes // its class name and referent. Properties are indented by two spaces. function serializeInstance(inst: Instance, indent: string): string { const ref = referentMap.get(inst)!; let xml = `${indent}\n`; xml += `${indent} \n`; // Serialize Name explicitly from getter; skip ClassName and Parent properties const nameProp = inst.Name; xml += `${indent} ${escapeXml(String(nameProp))}\n`; for (const prop of Object.keys(inst.Properties)) { if (prop === 'ClassName' || prop === 'Name' || prop === 'Parent') continue; const { type, value } = inst.Properties[prop]; const propXml = serializeProperty(prop, type, value); if (propXml) xml += `${indent} ${propXml}\n`; } xml += `${indent} \n`; for (const child of inst.Children) { xml += serializeInstance(child, indent); } xml += `${indent}\n`; return xml; } // Build the document let xmlDoc = '\n'; for (const child of root.getChildren()) { xmlDoc += serializeInstance(child, ''); } xmlDoc += '\n'; return xmlDoc; }