forked from iiDk-the-actual/convert.iidk.online
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcsgPostProcess.js
More file actions
231 lines (207 loc) · 9 KB
/
csgPostProcess.js
File metadata and controls
231 lines (207 loc) · 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
"use strict";
// ─── csgPostProcess.js ──────────────────────────────────────────────────────
// Post-processes a decoded RBXM/RBXMX instance tree to handle
// UnionOperation / IntersectOperation / NegateOperation instances.
//
// For each such instance the processor:
// 1. Resolves ChildData / ChildData2 (whichever is a valid RBXM blob) by
// recursively parsing it inline, and attaches the result as
// `_childInstances` on the instance.
// 2. Falls back to AssetData if no direct ChildData: that blob is parsed
// as a PartOperationAsset, and its own ChildData is then extracted.
// 3. Falls back to AssetId if neither blob is present: fetches the
// PartOperationAsset via the supplied async `fetchAsset(id)` callback.
// 4. Decodes the `Tags` BinaryString on every instance into `_tags`
// (string[]) so the client can identify `rbxNegated` parts.
// 5. Strips all raw binary properties (ChildData, ChildData2, AssetData,
// MeshData, MeshData2, PhysicsData, PhysicalConfigData) to keep the
// JSON lean.
// ─────────────────────────────────────────────────────────────────────────────
const CSG_CLASSES = new Set([
"UnionOperation",
"IntersectOperation",
"NegateOperation",
]);
const BINARY_BLOB_PROPS = [
"ChildData", "ChildData2",
"AssetData",
"MeshData", "MeshData2",
"PhysicsData", "PhysicalConfigData",
];
// ── Buffer helpers ────────────────────────────────────────────────────────────
/**
* Convert a property string to a Buffer.
* - XML parser outputs BinaryString properties as base64 text.
* - Binary parser outputs String properties as raw latin1 bytes.
* The regex test discriminates between the two encodings reliably because
* a real RBXM binary blob contains bytes like 0x89 / 0xFF that cannot appear
* in base64 text.
*/
function toBuffer(raw) {
if (!raw || typeof raw !== "string" || raw.length === 0) return null;
// Try base64 first (XML path)
try {
const stripped = raw.replace(/[\r\n\s]/g, "");
if (/^[A-Za-z0-9+/]+=*$/.test(stripped) && stripped.length > 0) {
const b64 = Buffer.from(stripped, "base64");
if (b64.length > 0) return b64;
}
} catch (_) {}
// Fall back to latin1 (binary parser path)
return Buffer.from(raw, "latin1");
}
function isRbxmBinary(buf) {
return buf.length >= 8 && buf.slice(0, 8).toString("ascii") === "<roblox!";
}
function isRbxmXml(buf) {
if (buf.length < 10) return false;
const head = buf.slice(0, Math.min(buf.length, 512)).toString("utf8").trimStart();
return head.startsWith("<roblox xmlns")
|| head.startsWith("<roblox\n")
|| head.startsWith("<roblox ");
}
function isCsgPhs(buf) {
return buf.length >= 6 && buf.slice(0, 6).toString("ascii") === "CSGPHS";
}
/** Attempt to parse a Buffer as binary or XML RBXM. Returns null if invalid. */
function parseBlob(buf, decode, decodeXml) {
if (!buf || buf.length < 8 || isCsgPhs(buf)) return null;
if (isRbxmBinary(buf)) {
try {
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
return decode(ab);
} catch (e) {
console.warn("[csgPostProcess] binary parse failed:", e.message);
}
} else if (isRbxmXml(buf)) {
try {
return decodeXml(buf);
} catch (e) {
console.warn("[csgPostProcess] xml parse failed:", e.message);
}
}
return null;
}
/** Resolve ChildData / ChildData2 on `src` into a parsed instance tree. */
function resolveDirectChildData(src, decode, decodeXml) {
for (const prop of ["ChildData", "ChildData2"]) {
const buf = toBuffer(src[prop]);
if (!buf) continue;
const tree = parseBlob(buf, decode, decodeXml);
if (tree) return tree;
}
return null;
}
/** Walk a root array looking for the first instance with `className`. */
function findInTree(roots, className) {
for (const inst of roots) {
if (inst.ClassName === className) return inst;
const found = findInTree(inst.Children || [], className);
if (found) return found;
}
return null;
}
/**
* Decode the null-terminated tag list stored in the Tags property.
* Returns a plain string array, e.g. ["rbxNegated"].
* Handles both base64 (XML) and raw latin1 (binary) encodings.
*/
function decodeTags(raw) {
if (!raw || typeof raw !== "string") return [];
let bytes;
// Try base64 first (XML BinaryString encoding)
try {
const stripped = raw.replace(/[\r\n\s]/g, "");
if (/^[A-Za-z0-9+/]+=*$/.test(stripped)) {
bytes = Buffer.from(stripped, "base64").toString("latin1");
}
} catch (_) {}
if (!bytes) bytes = raw; // fallback: binary parser already gave us latin1
return bytes.split("\0").map(t => t.trim()).filter(t => t.length > 0);
}
/** Strip raw binary blob properties that are no longer useful. */
function stripBinaryProps(inst) {
for (const p of BINARY_BLOB_PROPS) delete inst[p];
}
// ── Core walker ───────────────────────────────────────────────────────────────
async function processInstance(inst, decode, decodeXml, fetchAsset) {
// Recurse into regular children first (depth-first)
for (const child of (inst.Children || [])) {
await processInstance(child, decode, decodeXml, fetchAsset);
}
// Decode Tags on every instance (not just CSG classes)
if (inst.Tags) {
inst._tags = decodeTags(inst.Tags);
delete inst.Tags;
}
if (!CSG_CLASSES.has(inst.ClassName)) {
stripBinaryProps(inst);
return;
}
// ── 1. Try ChildData / ChildData2 directly on the instance ───────────────
let childTree = resolveDirectChildData(inst, decode, decodeXml);
// ── 2. Try AssetData (contains a PartOperationAsset blob) ────────────────
if (!childTree) {
const buf = toBuffer(inst.AssetData);
if (buf) {
const assetTree = parseBlob(buf, decode, decodeXml);
if (assetTree) {
const poa = findInTree(assetTree, "PartOperationAsset");
if (poa) {
childTree = resolveDirectChildData(poa, decode, decodeXml);
}
}
}
}
// ── 3. Try fetching via AssetId (uploaded unions) ─────────────────────────
if (!childTree && inst.AssetId && typeof fetchAsset === "function") {
const raw = String(inst.AssetId);
const match = raw.match(/(\d+)/);
const id = match ? parseInt(match[1]) : null;
if (id && id > 0) {
try {
const assetBuf = await fetchAsset(id);
if (assetBuf) {
const buf = Buffer.isBuffer(assetBuf)
? assetBuf
: Buffer.from(assetBuf);
const assetTree = parseBlob(buf, decode, decodeXml);
if (assetTree) {
const poa = findInTree(assetTree, "PartOperationAsset");
if (poa) {
childTree = resolveDirectChildData(poa, decode, decodeXml);
} else {
// The blob itself may already be the ChildData tree
childTree = assetTree;
}
}
}
} catch (e) {
console.warn(`[csgPostProcess] AssetId ${id} fetch failed:`, e.message);
}
}
}
// ── 4. Recurse into child tree and attach ─────────────────────────────────
if (childTree) {
for (const root of childTree) {
await processInstance(root, decode, decodeXml, fetchAsset);
}
inst._childInstances = childTree;
}
// AssetId is left on the instance so the client knows it exists if needed.
stripBinaryProps(inst);
}
/**
* Walk the decoded instance tree and resolve all CSG operations.
*
* @param {object[]} roots - Root instances from either parser.
* @param {Function} decode - rbxBinaryParser.decode
* @param {Function} decodeXml - rbxXmlParser.decodeXml
* @param {Function} [fetchAsset] - async (id: number) => Buffer | null
*/
async function processCSGOperations(roots, decode, decodeXml, fetchAsset) {
for (const root of roots) {
await processInstance(root, decode, decodeXml, fetchAsset);
}
}
module.exports = { processCSGOperations };