reverseport 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +71 -26
  2. package/THIRD_PARTY_NOTICES.md +1 -0
  3. package/bin/reverseport.mjs +13 -1
  4. package/dist/generatedPackageMetadata.js +1 -1
  5. package/licenses/README.md +1 -0
  6. package/licenses/godot-engine-MIT.txt +20 -0
  7. package/native/windows/build/manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/port/cli.mjs +6 -4
  10. package/port/knowledge/apis/godot-3-to-4-renames.json +1 -0
  11. package/port/knowledge/apis/godot-3.json +1 -0
  12. package/port/knowledge/apis/godot-4.3.json +1 -0
  13. package/port/knowledge/apis/godot-4.7.json +1 -0
  14. package/port/src/agent.mjs +1 -1
  15. package/port/src/agentdisk.mjs +83 -0
  16. package/port/src/apicheck.mjs +280 -0
  17. package/port/src/archive.mjs +34 -4
  18. package/port/src/convert.mjs +3 -1
  19. package/port/src/dormant.mjs +159 -0
  20. package/port/src/formats.mjs +1 -0
  21. package/port/src/gamearchives.mjs +476 -0
  22. package/port/src/godotbin.mjs +544 -0
  23. package/port/src/il2cpp.mjs +148 -0
  24. package/port/src/jsworkspace.mjs +2 -2
  25. package/port/src/knowledge.mjs +3 -1
  26. package/port/src/physics.mjs +135 -0
  27. package/port/src/plan.mjs +94 -11
  28. package/port/src/porters.mjs +87 -5
  29. package/port/src/refs.mjs +120 -0
  30. package/port/src/rpgmaker.mjs +416 -0
  31. package/port/src/save.mjs +326 -0
  32. package/port/src/scene.mjs +37 -3
  33. package/port/src/shader.mjs +385 -0
  34. package/port/src/strings.mjs +327 -0
  35. package/port/src/tools.mjs +48 -8
  36. package/port/src/trace.mjs +302 -0
  37. package/port/src/unity-anim.mjs +229 -0
  38. package/port/src/upgrade.mjs +219 -0
  39. package/skills/build-inspector/SKILL.md +8 -0
  40. package/skills/godot-port/SKILL.md +2 -0
  41. package/skills/port-verify/SKILL.md +20 -9
  42. package/skills/reverse-engineer-anything/SKILL.md +18 -2
  43. package/skills/reverse-engineer-anything/references/porting-and-rebuilds.md +35 -4
  44. package/skills/unity-to-godot/SKILL.md +14 -2
@@ -0,0 +1,544 @@
1
+ // Godot export leftovers back to editable form: compiled GDScript (.gdc, Godot 3 bytecode v10–13 and Godot 4.3+
2
+ // binary tokens v100/v101, zstd-compressed or not), binary resources and scenes (RSRC/RSCC .res/.scn → .tres/.tscn,
3
+ // including PackedScene _bundled trees) and exported project settings (project.binary → project.godot).
4
+ // Everything here is a faithful re-serialisation of what the file stores; comments and formatting are gone for good.
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import zlib from 'node:zlib';
8
+ import { RPError, requireFile, requireDir, walk, statOrNull } from './util.mjs';
9
+ import { workspaceDir } from './workspace.mjs';
10
+
11
+ // ------------------------------------------------------------------------------------------------ marshalled Variants
12
+ const M3 = ['NIL', 'BOOL', 'INT', 'FLOAT', 'STRING', 'VECTOR2', 'RECT2', 'VECTOR3', 'TRANSFORM2D', 'PLANE', 'QUATERNION', 'AABB', 'BASIS', 'TRANSFORM3D', 'COLOR', 'NODE_PATH', 'RID', 'OBJECT', 'DICTIONARY', 'ARRAY', 'PACKED_BYTE_ARRAY', 'PACKED_INT32_ARRAY', 'PACKED_FLOAT32_ARRAY', 'PACKED_STRING_ARRAY', 'PACKED_VECTOR2_ARRAY', 'PACKED_VECTOR3_ARRAY', 'PACKED_COLOR_ARRAY'];
13
+ const M4 = ['NIL', 'BOOL', 'INT', 'FLOAT', 'STRING', 'VECTOR2', 'VECTOR2I', 'RECT2', 'RECT2I', 'VECTOR3', 'VECTOR3I', 'TRANSFORM2D', 'VECTOR4', 'VECTOR4I', 'PLANE', 'QUATERNION', 'AABB', 'BASIS', 'TRANSFORM3D', 'PROJECTION', 'COLOR', 'STRING_NAME', 'NODE_PATH', 'RID', 'OBJECT', 'CALLABLE', 'SIGNAL', 'DICTIONARY', 'ARRAY', 'PACKED_BYTE_ARRAY', 'PACKED_INT32_ARRAY', 'PACKED_INT64_ARRAY', 'PACKED_FLOAT32_ARRAY', 'PACKED_FLOAT64_ARRAY', 'PACKED_STRING_ARRAY', 'PACKED_VECTOR2_ARRAY', 'PACKED_VECTOR3_ARRAY', 'PACKED_COLOR_ARRAY', 'PACKED_VECTOR4_ARRAY'];
14
+ const V = (kind, value) => ({ kind, value });
15
+ const pad4 = n => (n + 3) & ~3;
16
+
17
+ /** decode_variant (core/io/marshalls.cpp) for Godot 3 or 4. Returns { value: {kind, value}, end }. */
18
+ export function decodeMarshal(buf, off, major = 4, depth = 0) {
19
+ if (depth > 64 || off + 4 > buf.length) throw new RPError('BAD_FORMAT', 'variant truncated');
20
+ const head = buf.readUInt32LE(off), type = head & 0xff, flag64 = !!(head & (1 << 16));
21
+ const names = major >= 4 ? M4 : M3;
22
+ const kind = names[type];
23
+ if (!kind) throw new RPError('BAD_FORMAT', `unknown variant type ${type}`);
24
+ let p = off + 4;
25
+ const f32 = () => { const v = buf.readFloatLE(p); p += 4; return v; };
26
+ const real = () => { if (flag64) { const v = buf.readDoubleLE(p); p += 8; return v; } return f32(); };
27
+ const str = () => { const n = buf.readUInt32LE(p); const s = buf.toString('utf8', p + 4, p + 4 + n).replace(/\0+$/, ''); p += 4 + pad4(n); return s; };
28
+ const vec = n => { const out = []; for (let i = 0; i < n; i++) out.push(real()); return out; };
29
+ const arr = (n, each) => { const out = []; for (let i = 0; i < n; i++) out.push(each()); return out; };
30
+ switch (kind) {
31
+ case 'NIL': return { value: V('null', null), end: p };
32
+ case 'BOOL': { const v = buf.readUInt32LE(p) !== 0; return { value: V('bool', v), end: p + 4 }; }
33
+ case 'INT': { if (flag64) { const v = Number(buf.readBigInt64LE(p)); return { value: V('int', v), end: p + 8 }; } return { value: V('int', buf.readInt32LE(p)), end: p + 4 }; }
34
+ case 'FLOAT': return { value: V('float', real()), end: p };
35
+ case 'STRING': return { value: V('string', str()), end: p };
36
+ case 'STRING_NAME': return { value: V('stringname', str()), end: p };
37
+ case 'VECTOR2': return { value: V('Vector2', vec(2)), end: p };
38
+ case 'VECTOR2I': { const v = [buf.readInt32LE(p), buf.readInt32LE(p + 4)]; return { value: V('Vector2i', v), end: p + 8 }; }
39
+ case 'RECT2': return { value: V('Rect2', vec(4)), end: p };
40
+ case 'RECT2I': { const v = arr(4, () => { const x = buf.readInt32LE(p); p += 4; return x; }); return { value: V('Rect2i', v), end: p }; }
41
+ case 'VECTOR3': return { value: V('Vector3', vec(3)), end: p };
42
+ case 'VECTOR3I': { const v = arr(3, () => { const x = buf.readInt32LE(p); p += 4; return x; }); return { value: V('Vector3i', v), end: p }; }
43
+ case 'VECTOR4': return { value: V('Vector4', vec(4)), end: p };
44
+ case 'VECTOR4I': { const v = arr(4, () => { const x = buf.readInt32LE(p); p += 4; return x; }); return { value: V('Vector4i', v), end: p }; }
45
+ case 'TRANSFORM2D': return { value: V('Transform2D', vec(6)), end: p };
46
+ case 'PLANE': return { value: V('Plane', vec(4)), end: p };
47
+ case 'QUATERNION': return { value: V('Quaternion', vec(4)), end: p };
48
+ case 'AABB': return { value: V('AABB', vec(6)), end: p };
49
+ case 'BASIS': return { value: V('Basis', vec(9)), end: p };
50
+ case 'TRANSFORM3D': return { value: V('Transform3D', vec(12)), end: p };
51
+ case 'PROJECTION': return { value: V('Projection', vec(16)), end: p };
52
+ case 'COLOR': { const v = arr(4, f32); return { value: V('Color', v), end: p }; }
53
+ case 'NODE_PATH': {
54
+ let nc = buf.readUInt32LE(p); p += 4;
55
+ if (nc & 0x80000000) { nc &= 0x7fffffff; const snc = buf.readUInt32LE(p); const flags = buf.readUInt32LE(p + 4); p += 8; const names2 = arr(nc, str), subs = arr(snc, str); return { value: V('NodePath', `${flags & 1 ? '/' : ''}${names2.join('/')}${subs.length ? ':' + subs.join(':') : ''}`), end: p }; }
56
+ const s = buf.toString('utf8', p, p + nc); p += pad4(nc); return { value: V('NodePath', s), end: p };
57
+ }
58
+ case 'RID': return { value: V('RID', buf.readUInt32LE(p)), end: p + 4 };
59
+ case 'OBJECT': {
60
+ if (head & (1 << 16)) { const id = Number(buf.readBigUInt64LE(p)); return { value: V('object', `instance id ${id}`), end: p + 8 }; }
61
+ const cls = str(); const n = buf.readUInt32LE(p); p += 4; const props = {};
62
+ for (let i = 0; i < n; i++) { const k = str(); const r = decodeMarshal(buf, p, major, depth + 1); props[k] = r.value; p = r.end; }
63
+ return { value: V('object', { class: cls, props }), end: p };
64
+ }
65
+ case 'CALLABLE': case 'SIGNAL': return { value: V('null', null), end: p };
66
+ case 'DICTIONARY': { const n = buf.readUInt32LE(p) & 0x7fffffff; p += 4; const out = []; for (let i = 0; i < n; i++) { const k = decodeMarshal(buf, p, major, depth + 1); const v = decodeMarshal(buf, k.end, major, depth + 1); out.push([k.value, v.value]); p = v.end; } return { value: V('dict', out), end: p }; }
67
+ case 'ARRAY': { const n = buf.readUInt32LE(p) & 0x7fffffff; p += 4; const out = []; for (let i = 0; i < n; i++) { const v = decodeMarshal(buf, p, major, depth + 1); out.push(v.value); p = v.end; } return { value: V('array', out), end: p }; }
68
+ case 'PACKED_BYTE_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = [...buf.subarray(p, p + n)]; p += pad4(n); return { value: V('PackedByteArray', v), end: p }; }
69
+ case 'PACKED_INT32_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedInt32Array', arr(n, () => { const x = buf.readInt32LE(p); p += 4; return x; })), end: p }; }
70
+ case 'PACKED_INT64_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedInt64Array', arr(n, () => { const x = Number(buf.readBigInt64LE(p)); p += 8; return x; })), end: p }; }
71
+ case 'PACKED_FLOAT32_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedFloat32Array', arr(n, f32)), end: p }; }
72
+ case 'PACKED_FLOAT64_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedFloat64Array', arr(n, () => { const x = buf.readDoubleLE(p); p += 8; return x; })), end: p }; }
73
+ case 'PACKED_STRING_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedStringArray', arr(n, str)), end: p }; }
74
+ case 'PACKED_VECTOR2_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedVector2Array', arr(n, () => [f32(), f32()])), end: p }; }
75
+ case 'PACKED_VECTOR3_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedVector3Array', arr(n, () => [f32(), f32(), f32()])), end: p }; }
76
+ case 'PACKED_VECTOR4_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedVector4Array', arr(n, () => [f32(), f32(), f32(), f32()])), end: p }; }
77
+ case 'PACKED_COLOR_ARRAY': { const n = buf.readUInt32LE(p); p += 4; return { value: V('PackedColorArray', arr(n, () => [f32(), f32(), f32(), f32()])), end: p }; }
78
+ default: throw new RPError('BAD_FORMAT', `variant ${kind} not handled`);
79
+ }
80
+ }
81
+
82
+ const fnum = n => { if (!Number.isFinite(n)) return n > 0 ? 'inf' : n < 0 ? '-inf' : 'nan'; const s = Number(n.toPrecision(7)).toString(); return /[.e]/.test(s) ? s : `${s}.0`; };
83
+ const inum = n => String(n);
84
+ const qstr = s => `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t')}"`;
85
+ const PACKED_3 = { PackedByteArray: 'PoolByteArray', PackedInt32Array: 'PoolIntArray', PackedFloat32Array: 'PoolRealArray', PackedStringArray: 'PoolStringArray', PackedVector2Array: 'PoolVector2Array', PackedVector3Array: 'PoolVector3Array', PackedColorArray: 'PoolColorArray' };
86
+ /** Godot text-format literal for a decoded variant. major picks Pool/Quat/Transform names (3) or Packed/Quaternion/Transform3D (4). */
87
+ export function variantText(v, major = 4, ctx = {}) {
88
+ const names3 = { Quaternion: 'Quat', Transform3D: 'Transform' };
89
+ const typeName = k => (major === 3 ? (names3[k] ?? PACKED_3[k] ?? k) : k);
90
+ switch (v.kind) {
91
+ case 'null': return 'null';
92
+ case 'bool': return v.value ? 'true' : 'false';
93
+ case 'int': return inum(v.value);
94
+ case 'float': return fnum(v.value);
95
+ case 'string': return qstr(v.value);
96
+ case 'stringname': return `&${qstr(v.value)}`;
97
+ case 'NodePath': return `NodePath(${qstr(v.value)})`;
98
+ case 'RID': return `RID(${v.value})`;
99
+ case 'Color': return `Color(${v.value.map(fnum).join(', ')})`;
100
+ case 'Vector2i': case 'Vector3i': case 'Vector4i': case 'Rect2i': return `${v.kind}(${v.value.map(inum).join(', ')})`;
101
+ case 'Vector2': case 'Vector3': case 'Vector4': case 'Rect2': case 'Plane': case 'Quaternion': case 'AABB': case 'Basis': case 'Transform2D': case 'Transform3D': case 'Projection': return `${typeName(v.kind)}(${v.value.map(fnum).join(', ')})`;
102
+ case 'array': return `[${v.value.map(x => variantText(x, major, ctx)).join(', ')}]`;
103
+ case 'dict': return v.value.length ? `{\n${v.value.map(([k, val]) => `${variantText(k, major, ctx)}: ${variantText(val, major, ctx)}`).join(',\n')}\n}` : '{}';
104
+ case 'PackedByteArray': case 'PackedInt32Array': case 'PackedInt64Array': case 'PackedFloat32Array': case 'PackedFloat64Array': case 'PackedStringArray':
105
+ return `${typeName(v.kind)}(${v.value.map(x => (typeof x === 'string' ? qstr(x) : /Float/.test(v.kind) ? fnum(x) : inum(x))).join(', ')})`;
106
+ case 'PackedVector2Array': case 'PackedVector3Array': case 'PackedVector4Array': case 'PackedColorArray': return `${typeName(v.kind)}(${v.value.flat().map(fnum).join(', ')})`;
107
+ case 'ext': return major === 3 ? `ExtResource( ${v.value} )` : `ExtResource(${qstr(v.value)})`;
108
+ case 'sub': return major === 3 ? `SubResource( ${v.value} )` : `SubResource(${qstr(v.value)})`;
109
+ case 'object': return typeof v.value === 'string' ? `null # ${v.value}` : `Object(${v.value.class},${Object.entries(v.value.props).map(([k, x]) => `${qstr(k)}:${variantText(x, major, ctx)}`).join(',')})`;
110
+ default: return `null # ${v.kind}`;
111
+ }
112
+ }
113
+
114
+ // ------------------------------------------------------------------------------------------------ GDScript bytecode
115
+ export const TK3 = ['EMPTY', 'IDENTIFIER', 'CONSTANT', 'SELF', 'BUILT_IN_TYPE', 'BUILT_IN_FUNC', 'in', '==', '!=', '<', '<=', '>', '>=', 'and', 'or', 'not', '+', '-', '*', '/', '%', '<<', '>>', '=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '&=', '|=', '^=', '&', '|', '^', '~',
116
+ 'if', 'elif', 'else', 'for', 'while', 'break', 'continue', 'pass', 'return', 'match', 'func', 'class', 'class_name', 'extends', 'is', 'onready', 'tool', 'static', 'export', 'setget', 'const', 'var', 'as', 'void', 'enum', 'preload', 'assert', 'yield', 'signal', 'breakpoint', 'remote', 'sync', 'master', 'slave', 'puppet', 'remotesync', 'mastersync', 'puppetsync',
117
+ '[', ']', '{', '}', '(', ')', ',', ';', '.', '?', ':', '$', '->', 'NEWLINE', 'PI', 'TAU', '_', 'INF', 'NAN', 'ERROR', 'EOF', 'CURSOR'];
118
+ const TK3_TYPES = ['null', 'bool', 'int', 'float', 'String', 'Vector2', 'Rect2', 'Vector3', 'Transform2D', 'Plane', 'Quat', 'AABB', 'Basis', 'Transform', 'Color', 'NodePath', 'RID', 'Object', 'Dictionary', 'Array', 'PoolByteArray', 'PoolIntArray', 'PoolRealArray', 'PoolStringArray', 'PoolVector2Array', 'PoolVector3Array', 'PoolColorArray'];
119
+ const TK3_FUNCS = 'sin cos tan sinh cosh tanh asin acos atan atan2 sqrt fmod fposmod posmod floor ceil round abs sign pow log exp is_nan is_inf is_equal_approx is_zero_approx ease decimals step_decimals stepify lerp lerp_angle inverse_lerp range_lerp smoothstep move_toward dectime randomize randi randf rand_range seed rand_seed deg2rad rad2deg linear2db db2linear polar2cartesian cartesian2polar wrapi wrapf max min clamp nearest_po2 weakref funcref convert typeof type_exists char ord str print printt prints printerr printraw print_debug push_error push_warning var2str str2var var2bytes bytes2var range load inst2dict dict2inst validate_json parse_json to_json hash Color8 ColorN print_stack get_stack instance_from_id len is_instance_valid deep_equal'.split(' ');
120
+ // Godot 4 token enum (v100 = 4.3/4.4 without "..." ; v101 = 4.5+ with PERIOD_PERIOD_PERIOD).
121
+ const TK4_BASE = ['EMPTY', 'ANNOTATION', 'IDENTIFIER', 'LITERAL', '<', '<=', '>', '>=', '==', '!=', 'and', 'or', 'not', '&&', '||', '!', '&', '|', '~', '^', '<<', '>>', '+', '-', '*', '**', '/', '%', '=', '+=', '-=', '*=', '**=', '/=', '%=', '<<=', '>>=', '&=', '|=', '^=',
122
+ 'if', 'elif', 'else', 'for', 'while', 'break', 'continue', 'pass', 'return', 'match', 'when', 'as', 'assert', 'await', 'breakpoint', 'class', 'class_name', 'const', 'enum', 'extends', 'func', 'in', 'is', 'namespace', 'preload', 'self', 'signal', 'static', 'super', 'trait', 'var', 'void', 'yield',
123
+ '[', ']', '{', '}', '(', ')', ',', ';', '.', '..', ':', '$', '->', '_', 'NEWLINE', 'INDENT', 'DEDENT', 'PI', 'TAU', 'INF', 'NAN', 'VCS', '`', '?', 'ERROR', 'EOF'];
124
+ const TK4_101 = [...TK4_BASE.slice(0, TK4_BASE.indexOf('..') + 1), '...', ...TK4_BASE.slice(TK4_BASE.indexOf('..') + 1)];
125
+
126
+ const NO_SPACE_BEFORE = new Set([')', ']', '}', ',', '.', ':', ';']);
127
+ const NO_SPACE_AFTER = new Set(['(', '[', '.', '$', '~', '@', '{']);
128
+ const KEYWORD_RE = /^[a-z_]+$/;
129
+
130
+ function joinTokens(parts) {
131
+ // parts: [{text, kind}] with kind in word|punct|open|close|op|newline|dollar
132
+ let out = '';
133
+ let prev = null;
134
+ for (const t of parts) {
135
+ if (t.kind === 'newline') { out = out.replace(/[ \t]+$/, '') + t.text; prev = t; continue; }
136
+ const prevText = prev?.text ?? '';
137
+ let space = true;
138
+ if (!prev || prev.kind === 'newline') space = false;
139
+ else if (NO_SPACE_BEFORE.has(t.text) && t.kind !== 'word') space = false;
140
+ else if (NO_SPACE_AFTER.has(prevText)) space = false;
141
+ else if (prevText === '-' && (prev.unary)) space = false;
142
+ else if (t.text === '=' && prevText === ':' && prev.kind !== 'word') space = false;
143
+ else if ((t.text === '(' || t.text === '[') && (prev.kind === 'word' && /^[A-Za-z_@$]/.test(prevText) && !['and', 'or', 'not', 'in', 'is', 'as', 'return', 'if', 'elif', 'while', 'for', 'match', 'await', 'when', 'else', 'var', 'const'].includes(prevText))) space = false;
144
+ else if ((t.text === '(' || t.text === '[') && (prevText === ')' || prevText === ']')) space = false;
145
+ if (space) out += ' ';
146
+ out += t.text;
147
+ prev = t;
148
+ }
149
+ return out;
150
+ }
151
+
152
+ export function decodeGdc(buf) {
153
+ if (buf.length < 12 || buf.toString('latin1', 0, 4) !== 'GDSC') throw new RPError('BAD_FORMAT', 'Not a compiled GDScript (GDSC).');
154
+ const version = buf.readUInt32LE(4);
155
+ if (version <= 13) return decodeGdc3(buf, version);
156
+ if (version === 100 || version === 101) return decodeGdc4(buf, version);
157
+ throw new RPError('BAD_FORMAT', `GDSC version ${version} is not known (Godot 3: ≤13, Godot 4.3+: 100/101).`);
158
+ }
159
+
160
+ function decodeGdc3(buf, version) {
161
+ const idCount = buf.readUInt32LE(8), constCount = buf.readUInt32LE(12), lineCount = buf.readUInt32LE(16), tokenCount = buf.readUInt32LE(20);
162
+ let p = 24;
163
+ const identifiers = [];
164
+ for (let i = 0; i < idCount; i++) { const len = buf.readUInt32LE(p); p += 4; const cs = Buffer.from(buf.subarray(p, p + len).map(b => b ^ 0xb6)); identifiers.push(cs.toString('utf8').replace(/\0.*$/s, '')); p += len; }
165
+ const constants = [];
166
+ for (let i = 0; i < constCount; i++) { const r = decodeMarshal(buf, p, 3); constants.push(r.value); p = r.end; }
167
+ const lines = new Map();
168
+ for (let i = 0; i < lineCount; i++) { lines.set(buf.readUInt32LE(p), buf.readUInt32LE(p + 4)); p += 8; }
169
+ const tokens = [];
170
+ for (let i = 0; i < tokenCount; i++) { if (buf[p] & 0x80) { tokens.push(buf.readUInt32LE(p) & ~0x80); p += 4; } else { tokens.push(buf[p]); p += 1; } }
171
+ const parts = [];
172
+ const warnings = [];
173
+ let indent = 0;
174
+ for (let i = 0; i < tokens.length; i++) {
175
+ const tk = tokens[i] & 0xff, data = tokens[i] >> 8;
176
+ const name = TK3[tk];
177
+ if (name === undefined) { warnings.push(`token ${tk} unknown at ${i}`); continue; }
178
+ switch (name) {
179
+ case 'EMPTY': case 'CURSOR': case 'ERROR': break;
180
+ case 'EOF': break;
181
+ case 'IDENTIFIER': parts.push({ text: identifiers[data] ?? `_id${data}`, kind: 'word' }); break;
182
+ case 'CONSTANT': parts.push({ text: constants[data] ? variantText(constants[data], 3) : 'null', kind: 'word' }); break;
183
+ case 'SELF': parts.push({ text: 'self', kind: 'word' }); break;
184
+ case 'BUILT_IN_TYPE': parts.push({ text: TK3_TYPES[data] ?? `Variant`, kind: 'word' }); break;
185
+ case 'BUILT_IN_FUNC': parts.push({ text: TK3_FUNCS[data] ?? `builtin_${data}`, kind: 'word' }); break;
186
+ case 'NEWLINE': indent = data; parts.push({ text: '\n' + '\t'.repeat(indent), kind: 'newline' }); break;
187
+ case 'PI': case 'TAU': case 'INF': case 'NAN': case '_': parts.push({ text: name, kind: 'word' }); break;
188
+ default: {
189
+ const isWord = KEYWORD_RE.test(name);
190
+ const unary = name === '-' && (i === 0 || ['(', '[', ',', '=', 'return', '+', '-', '*', '/', '<', '>', '==', '!=', '<=', '>=', 'NEWLINE', 'and', 'or', 'not', 'in', ':'].includes(TK3[tokens[i - 1] & 0xff]));
191
+ parts.push({ text: name, kind: isWord ? 'word' : ['(', '[', '{'].includes(name) ? 'open' : [')', ']', '}'].includes(name) ? 'close' : 'punct', unary });
192
+ }
193
+ }
194
+ }
195
+ const source = joinTokens(parts).replace(/^\n+/, '').replace(/[ \t]+\n/g, '\n').replace(/\n+$/, '') + '\n';
196
+ return { engine: 'godot3', bytecodeVersion: version, identifiers: identifiers.length, constants: constants.length, tokens: tokens.length, source, warnings };
197
+ }
198
+
199
+ function decodeGdc4(buf, version) {
200
+ const decompressedSize = buf.readUInt32LE(8);
201
+ let c;
202
+ if (decompressedSize === 0) c = buf.subarray(12);
203
+ else {
204
+ if (typeof zlib.zstdDecompressSync !== 'function') throw new RPError('UNSUPPORTED', 'This .gdc is zstd-compressed and this Node version has no zstd (needs Node 22.15+ / 23.8+).');
205
+ c = zlib.zstdDecompressSync(buf.subarray(12));
206
+ if (c.length !== decompressedSize) throw new RPError('BAD_FORMAT', 'zstd output size does not match the header.');
207
+ }
208
+ // v100 (Godot 4.3/4.4): token count at 16, data at 20 (offset 12 unused); v101 (4.5+): token count at 12, data at 16.
209
+ const idCount = c.readUInt32LE(0), constCount = c.readUInt32LE(4), lineCount = c.readUInt32LE(8), tokenCount = c.readUInt32LE(version === 100 ? 16 : 12);
210
+ let p = version === 100 ? 20 : 16;
211
+ const identifiers = [];
212
+ for (let i = 0; i < idCount; i++) { const len = c.readUInt32LE(p); p += 4; let s = ''; for (let j = 0; j < len; j++) { const cp = (c[p + j * 4] ^ 0xb6) | ((c[p + j * 4 + 1] ^ 0xb6) << 8) | ((c[p + j * 4 + 2] ^ 0xb6) << 16) | ((c[p + j * 4 + 3] ^ 0xb6) << 24); s += String.fromCodePoint(cp >>> 0); } identifiers.push(s); p += len * 4; }
213
+ const constants = [];
214
+ for (let i = 0; i < constCount; i++) { const r = decodeMarshal(c, p, 4); constants.push(r.value); p = r.end; }
215
+ // Lines and columns of the first token on each new line: newlines and indentation are rebuilt from these.
216
+ const tokLine = new Map(), tokCol = new Map();
217
+ for (let i = 0; i < lineCount; i++) { tokLine.set(c.readUInt32LE(p), c.readUInt32LE(p + 4)); p += 8; }
218
+ for (let i = 0; i < lineCount; i++) { tokCol.set(c.readUInt32LE(p), c.readUInt32LE(p + 4)); p += 8; }
219
+ const TK = version === 101 ? TK4_101 : TK4_BASE;
220
+ const toks = [];
221
+ for (let i = 0; i < tokenCount; i++) {
222
+ const first = c[p];
223
+ let raw, line;
224
+ if (first & 0x80) { raw = c.readUInt32LE(p); line = c.readUInt32LE(p + 4); p += 8; } else { raw = first; line = c.readUInt32LE(p + 1); p += 5; }
225
+ toks.push({ type: raw & 0x7f, data: raw >>> 8, line });
226
+ }
227
+ const parts = [];
228
+ const warnings = [];
229
+ let indent = 0, lastLine = toks[0]?.line ?? 1;
230
+ const stack = [];
231
+ for (let i = 0; i < toks.length; i++) {
232
+ const t = toks[i];
233
+ const name = TK[t.type];
234
+ if (i > 0 && tokLine.has(i)) {
235
+ const col = (tokCol.get(i) ?? 1) - 1;
236
+ const prevIndent = stack.at(-1) ?? 0;
237
+ if (col > prevIndent) stack.push(col); else while (stack.length && col < (stack.at(-1) ?? 0)) stack.pop();
238
+ indent = stack.length;
239
+ const line = tokLine.get(i);
240
+ parts.push({ text: (line - lastLine > 1 ? '\n' : '') + '\n' + '\t'.repeat(indent), kind: 'newline' });
241
+ lastLine = line;
242
+ }
243
+ if (name === undefined) { warnings.push(`token ${t.type} unknown at ${i}`); continue; }
244
+ switch (name) {
245
+ case 'EMPTY': case 'ERROR': case 'EOF': case 'VCS': break;
246
+ case 'ANNOTATION': parts.push({ text: identifiers[t.data] ?? '@?', kind: 'word' }); break;
247
+ case 'IDENTIFIER': parts.push({ text: identifiers[t.data] ?? `_id${t.data}`, kind: 'word' }); break;
248
+ case 'LITERAL': parts.push({ text: constants[t.data] ? variantText(constants[t.data], 4) : 'null', kind: 'word' }); break;
249
+ case 'INDENT': indent++; if (parts.at(-1)?.kind === 'newline') parts.at(-1).text = '\n' + '\t'.repeat(indent); break;
250
+ case 'DEDENT': indent = Math.max(0, indent - 1); if (parts.at(-1)?.kind === 'newline') parts.at(-1).text = '\n' + '\t'.repeat(indent); break;
251
+ case 'NEWLINE': { const blank = t.line - lastLine > 1 ? '\n' : ''; parts.push({ text: blank + '\n' + '\t'.repeat(indent), kind: 'newline' }); break; }
252
+ case 'PI': case 'TAU': case 'INF': parts.push({ text: name, kind: 'word' }); break;
253
+ case 'NAN': parts.push({ text: 'NAN', kind: 'word' }); break;
254
+ case '_': parts.push({ text: '_', kind: 'word' }); break;
255
+ default: {
256
+ const isWord = KEYWORD_RE.test(name);
257
+ const prevName = TK[toks[i - 1]?.type] ?? 'NEWLINE';
258
+ const unary = name === '-' && ['(', '[', ',', '=', 'return', '+', '-', '*', '/', '<', '>', '==', '!=', '<=', '>=', 'NEWLINE', 'INDENT', 'and', 'or', 'not', 'in', ':', '**', '%'].includes(prevName);
259
+ parts.push({ text: name, kind: isWord ? 'word' : ['(', '[', '{'].includes(name) ? 'open' : [')', ']', '}'].includes(name) ? 'close' : 'punct', unary });
260
+ }
261
+ }
262
+ }
263
+ const source = joinTokens(parts).replace(/^\n+/, '').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').replace(/([\w\]\)]):= /g, '$1 := ') + '\n';
264
+ return { engine: 'godot4', bytecodeVersion: version, compressed: decompressedSize !== 0, identifiers: identifiers.length, constants: constants.length, tokens: toks.length, source, warnings };
265
+ }
266
+
267
+ // ------------------------------------------------------------------------------------------------ RSRC binary resources
268
+ const R4 = { 1: 'NIL', 2: 'BOOL', 3: 'INT', 4: 'FLOAT', 5: 'STRING', 10: 'VECTOR2', 11: 'RECT2', 12: 'VECTOR3', 13: 'PLANE', 14: 'QUATERNION', 15: 'AABB', 16: 'BASIS', 17: 'TRANSFORM3D', 18: 'TRANSFORM2D', 20: 'COLOR', 21: 'IMAGE', 22: 'NODE_PATH', 23: 'RID', 24: 'OBJECT', 25: 'INPUT_EVENT', 26: 'DICTIONARY', 30: 'ARRAY', 31: 'PACKED_BYTE_ARRAY', 32: 'PACKED_INT32_ARRAY', 33: 'PACKED_FLOAT32_ARRAY', 34: 'PACKED_STRING_ARRAY', 35: 'PACKED_VECTOR3_ARRAY', 36: 'PACKED_COLOR_ARRAY', 37: 'PACKED_VECTOR2_ARRAY', 40: 'INT64', 41: 'DOUBLE', 42: 'CALLABLE', 43: 'SIGNAL', 44: 'STRING_NAME', 45: 'VECTOR2I', 46: 'RECT2I', 47: 'VECTOR3I', 48: 'PACKED_INT64_ARRAY', 49: 'PACKED_FLOAT64_ARRAY', 50: 'VECTOR4', 51: 'VECTOR4I', 52: 'PROJECTION', 53: 'PACKED_VECTOR4_ARRAY' };
269
+
270
+ export function readRsrc(input) {
271
+ let buf = input;
272
+ if (buf.toString('latin1', 0, 4) === 'RSCC') buf = inflateRscc(buf);
273
+ if (buf.toString('latin1', 0, 4) !== 'RSRC') throw new RPError('BAD_FORMAT', 'Not a Godot binary resource (RSRC/RSCC).');
274
+ const bigEndian = buf.readUInt32LE(4) !== 0;
275
+ if (bigEndian) throw new RPError('UNSUPPORTED', 'big-endian resource files are not supported');
276
+ const real64 = buf.readUInt32LE(8) !== 0;
277
+ const major = buf.readUInt32LE(12), minor = buf.readUInt32LE(16), format = buf.readUInt32LE(20);
278
+ let p = 24;
279
+ const ustr = () => { const n = buf.readUInt32LE(p); const s = buf.toString('utf8', p + 4, p + 4 + n).replace(/\0$/, ''); p += 4 + n; return s; };
280
+ const type = ustr();
281
+ p += 8; // importmd_ofs
282
+ let flags = 0, uid = null, scriptClass = null, namedIds = false;
283
+ if (major >= 4) {
284
+ flags = buf.readUInt32LE(p); p += 4;
285
+ namedIds = !!(flags & 1);
286
+ if (flags & 2) uid = buf.readBigInt64LE(p); p += 8;
287
+ if (flags & 8) scriptClass = ustr();
288
+ p += 11 * 4;
289
+ } else p += 14 * 4;
290
+ const realIsDouble = major >= 4 ? !!(flags & 4) : real64;
291
+ const nStrings = buf.readUInt32LE(p); p += 4;
292
+ const strings = [];
293
+ for (let i = 0; i < nStrings; i++) strings.push(ustr());
294
+ const nExt = buf.readUInt32LE(p); p += 4;
295
+ const ext = [];
296
+ for (let i = 0; i < nExt; i++) { const t = ustr(); const pth = ustr(); let euid = null; if (flags & 2) { euid = buf.readBigInt64LE(p); p += 8; } ext.push({ type: t, path: pth, uid: euid, id: namedIds ? `${i + 1}_${hashId(pth)}` : String(i + 1) }); }
297
+ const nInt = buf.readUInt32LE(p); p += 4;
298
+ const internal = [];
299
+ for (let i = 0; i < nInt; i++) { const pth = ustr(); const off = Number(buf.readBigUInt64LE(p)); p += 8; internal.push({ path: pth, offset: off }); }
300
+ const getString = () => { const id = buf.readUInt32LE(p); p += 4; if (id & 0x80000000) { const n = id & 0x7fffffff; const s = buf.toString('utf8', p, p + n); p += n; return s; } return strings[id] ?? `str${id}`; };
301
+ const real = () => { if (realIsDouble) { const v = buf.readDoubleLE(p); p += 8; return v; } const v = buf.readFloatLE(p); p += 4; return v; };
302
+ const f32 = () => { const v = buf.readFloatLE(p); p += 4; return v; };
303
+ const vec = n => { const o = []; for (let i = 0; i < n; i++) o.push(real()); return o; };
304
+ const parse = (depth = 0) => {
305
+ if (depth > 64) throw new RPError('BAD_FORMAT', 'resource nesting too deep');
306
+ const t = buf.readUInt32LE(p); p += 4;
307
+ const kind = R4[t];
308
+ switch (kind) {
309
+ case 'NIL': return V('null', null);
310
+ case 'BOOL': { const v = buf.readUInt32LE(p) !== 0; p += 4; return V('bool', v); }
311
+ case 'INT': { const v = buf.readInt32LE(p); p += 4; return V('int', v); }
312
+ case 'INT64': { const v = Number(buf.readBigInt64LE(p)); p += 8; return V('int', v); }
313
+ case 'FLOAT': return V('float', f32());
314
+ case 'DOUBLE': { const v = buf.readDoubleLE(p); p += 8; return V('float', v); }
315
+ case 'STRING': return V('string', ustr());
316
+ case 'STRING_NAME': return V('stringname', ustr());
317
+ case 'VECTOR2': return V('Vector2', vec(2));
318
+ case 'VECTOR2I': { const v = [buf.readInt32LE(p), buf.readInt32LE(p + 4)]; p += 8; return V('Vector2i', v); }
319
+ case 'RECT2': return V('Rect2', vec(4));
320
+ case 'RECT2I': { const v = [0, 1, 2, 3].map(i => buf.readInt32LE(p + 4 * i)); p += 16; return V('Rect2i', v); }
321
+ case 'VECTOR3': return V('Vector3', vec(3));
322
+ case 'VECTOR3I': { const v = [0, 1, 2].map(i => buf.readInt32LE(p + 4 * i)); p += 12; return V('Vector3i', v); }
323
+ case 'VECTOR4': return V('Vector4', vec(4));
324
+ case 'VECTOR4I': { const v = [0, 1, 2, 3].map(i => buf.readInt32LE(p + 4 * i)); p += 16; return V('Vector4i', v); }
325
+ case 'PLANE': return V('Plane', vec(4));
326
+ case 'QUATERNION': return V('Quaternion', vec(4));
327
+ case 'AABB': return V('AABB', vec(6));
328
+ case 'TRANSFORM2D': return V('Transform2D', vec(6));
329
+ case 'BASIS': return V('Basis', vec(9));
330
+ case 'TRANSFORM3D': return V('Transform3D', vec(12));
331
+ case 'PROJECTION': return V('Projection', vec(16));
332
+ case 'COLOR': return V('Color', [f32(), f32(), f32(), f32()]);
333
+ case 'NODE_PATH': {
334
+ const nc = buf.readUInt16LE(p); let sc = buf.readUInt16LE(p + 2); p += 4;
335
+ const absolute = !!(sc & 0x8000); sc &= 0x7fff; if (format < 3) sc += 1;
336
+ const names = []; for (let i = 0; i < nc; i++) names.push(getString());
337
+ const subs = []; for (let i = 0; i < sc; i++) subs.push(getString());
338
+ return V('NodePath', `${absolute ? '/' : ''}${names.join('/')}${subs.filter(Boolean).length ? ':' + subs.filter(Boolean).join(':') : ''}`);
339
+ }
340
+ case 'RID': { const v = buf.readUInt32LE(p); p += 4; return V('RID', v); }
341
+ case 'OBJECT': {
342
+ const ot = buf.readUInt32LE(p); p += 4;
343
+ if (ot === 0) return V('null', null);
344
+ if (ot === 2) { const idx = buf.readUInt32LE(p); p += 4; return V('sub', namedIds ? (internal[idx]?.path ?? String(idx)).replace(/^local:\/\//, '').replace(/^.*::/, '') : String(idx + 1)); }
345
+ if (ot === 1) { const et = ustr(); const pth = ustr(); return V('extpath', { type: et, path: pth }); }
346
+ if (ot === 3) { const idx = buf.readUInt32LE(p); p += 4; return V('ext', ext[idx]?.id ?? String(idx)); }
347
+ throw new RPError('BAD_FORMAT', `object kind ${ot}`);
348
+ }
349
+ case 'CALLABLE': case 'SIGNAL': return V('null', null);
350
+ case 'DICTIONARY': { const n = buf.readUInt32LE(p) & 0x7fffffff; p += 4; const out = []; for (let i = 0; i < n; i++) { const k = parse(depth + 1); const v = parse(depth + 1); out.push([k, v]); } return V('dict', out); }
351
+ case 'ARRAY': { const n = buf.readUInt32LE(p) & 0x7fffffff; p += 4; const out = []; for (let i = 0; i < n; i++) out.push(parse(depth + 1)); return V('array', out); }
352
+ case 'PACKED_BYTE_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = [...buf.subarray(p, p + n)]; p += pad4(n); return V('PackedByteArray', v); }
353
+ case 'PACKED_INT32_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) { v.push(buf.readInt32LE(p)); p += 4; } return V('PackedInt32Array', v); }
354
+ case 'PACKED_INT64_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) { v.push(Number(buf.readBigInt64LE(p))); p += 8; } return V('PackedInt64Array', v); }
355
+ case 'PACKED_FLOAT32_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) v.push(f32()); return V('PackedFloat32Array', v); }
356
+ case 'PACKED_FLOAT64_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) { v.push(buf.readDoubleLE(p)); p += 8; } return V('PackedFloat64Array', v); }
357
+ case 'PACKED_STRING_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) v.push(ustr()); return V('PackedStringArray', v); }
358
+ case 'PACKED_VECTOR2_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) v.push([real(), real()]); return V('PackedVector2Array', v); }
359
+ case 'PACKED_VECTOR3_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) v.push([real(), real(), real()]); return V('PackedVector3Array', v); }
360
+ case 'PACKED_VECTOR4_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) v.push([real(), real(), real(), real()]); return V('PackedVector4Array', v); }
361
+ case 'PACKED_COLOR_ARRAY': { const n = buf.readUInt32LE(p); p += 4; const v = []; for (let i = 0; i < n; i++) v.push([f32(), f32(), f32(), f32()]); return V('PackedColorArray', v); }
362
+ default: throw new RPError('BAD_FORMAT', `resource variant ${t} not handled`);
363
+ }
364
+ };
365
+ const resources = [];
366
+ for (const ir of internal) {
367
+ p = ir.offset;
368
+ const rtype = ustr();
369
+ const pc = buf.readUInt32LE(p); p += 4;
370
+ const props = [];
371
+ for (let j = 0; j < pc; j++) { const name = getString(); const value = parse(); props.push([name, value]); }
372
+ resources.push({ path: ir.path, type: rtype, props: props.filter(([k, v]) => !(k === 'script' && v.kind === 'null')), id: namedIds ? ir.path.replace(/^local:\/\//, '').replace(/^.*::/, '') : String(resources.length + 1) });
373
+ }
374
+ return { type, major, minor, format, uid: uid != null ? String(uid) : null, scriptClass, strings: strings.length, ext, resources, namedIds };
375
+ }
376
+ function hashId(s) { let h = 5381; for (const c of s) h = ((h << 5) + h + c.charCodeAt(0)) >>> 0; return (h % 46656).toString(36).padStart(3, '0').slice(-5); }
377
+
378
+ function inflateRscc(buf) {
379
+ // FileAccessCompressed: "RSCC", mode u32, block_size u32, read_total u32, then blocks: for each block size u32 … (block data compressed). Modes: 0 FastLZ, 1 Deflate, 2 zstd, 3 gzip.
380
+ const mode = buf.readUInt32LE(4), blockSize = buf.readUInt32LE(8), total = buf.readUInt32LE(12);
381
+ const blocks = Math.ceil(total / blockSize) || 1;
382
+ let p = 16;
383
+ const sizes = [];
384
+ for (let i = 0; i < blocks; i++) { sizes.push(buf.readUInt32LE(p)); p += 4; }
385
+ const out = [];
386
+ for (let i = 0; i < blocks; i++) {
387
+ const chunk = buf.subarray(p, p + sizes[i]); p += sizes[i];
388
+ if (mode === 1) out.push(zlib.inflateRawSync(chunk));
389
+ else if (mode === 2) { if (typeof zlib.zstdDecompressSync !== 'function') throw new RPError('UNSUPPORTED', 'RSCC zstd needs Node 22.15+'); out.push(zlib.zstdDecompressSync(chunk)); }
390
+ else if (mode === 3) out.push(zlib.gunzipSync(chunk));
391
+ else throw new RPError('UNSUPPORTED', `RSCC compression mode ${mode} (FastLZ) is not supported here`);
392
+ }
393
+ return Buffer.concat(out).subarray(0, total);
394
+ }
395
+
396
+ /** PackedScene _bundled dictionary → node list (name, type, parent, instance, properties, groups) + connections. */
397
+ export function unpackScene(bundled, major) {
398
+ const d = Object.fromEntries(bundled.value.map(([k, v]) => [k.value, v]));
399
+ const names = (d.names?.value ?? []).map(x => (typeof x === 'string' ? x : x.value ?? x));
400
+ const variants = d.variants?.value ?? [];
401
+ const nodesArr = d.nodes?.value ?? [], conns = d.conns?.value ?? [];
402
+ const nodeCount = d.node_count?.value ?? 0, connCount = d.conn_count?.value ?? 0, version = d.version?.value ?? 1;
403
+ const nodePaths = (d.node_paths?.value ?? []).map(x => (typeof x.value === 'string' ? x.value.replace(/^\.\//, '') : x.value));
404
+ const FLAG_MASK = (1 << 24) - 1, ID_IS_PATH = 1 << 30, TYPE_INSTANTIATED = 0x7fffffff, PLACEHOLDER = 1 << 30, NAME_MASK = (1 << 18) - 1;
405
+ const nodes = [];
406
+ let i = 0;
407
+ for (let n = 0; n < nodeCount; n++) {
408
+ const parent = nodesArr[i++], owner = nodesArr[i++], type = nodesArr[i++], nameIdx = nodesArr[i++], instance = nodesArr[i++];
409
+ const pc = nodesArr[i++]; const props = [];
410
+ for (let j = 0; j < pc; j++) { const pn = nodesArr[i++], pv = nodesArr[i++]; props.push({ name: names[pn & (ID_IS_PATH - 1)] ?? `prop${pn}`, value: variants[pv] ?? V('null', null) }); }
411
+ const gc = nodesArr[i++]; const groups = [];
412
+ for (let j = 0; j < gc; j++) groups.push(names[nodesArr[i++]]);
413
+ nodes.push({ index: n, parent: parent < 0 ? null : parent & ID_IS_PATH ? { path: String(nodePaths[parent & FLAG_MASK] ?? `path${parent & FLAG_MASK}`) } : { node: parent & FLAG_MASK }, owner, type: type === TYPE_INSTANTIATED ? null : names[type] ?? `Type${type}`, name: names[nameIdx & NAME_MASK] ?? `Node${n}`, instance: instance >= 0 ? (instance & PLACEHOLDER ? { placeholder: variants[instance & FLAG_MASK] } : { resource: variants[instance & FLAG_MASK] }) : null, props, groups });
414
+ }
415
+ const connections = [];
416
+ i = 0;
417
+ for (let n = 0; n < connCount; n++) {
418
+ const from = conns[i++], to = conns[i++], signal = conns[i++], method = conns[i++], flags = conns[i++], bc = conns[i++];
419
+ const binds = []; for (let j = 0; j < bc; j++) binds.push(variants[conns[i++]]);
420
+ let unbinds = 0; if (version >= 3) unbinds = conns[i++];
421
+ connections.push({ from: from & ID_IS_PATH ? String(nodePaths[from & FLAG_MASK]) : from & FLAG_MASK, to: to & ID_IS_PATH ? String(nodePaths[to & FLAG_MASK]) : to & FLAG_MASK, signal: names[signal], method: names[method], flags, binds, unbinds });
422
+ }
423
+ return { nodes, connections, version, baseScene: d.base_scene?.value ?? null, editableInstances: (d.editable_instances?.value ?? []).map(x => x.value) };
424
+ }
425
+
426
+ /** Text form of a binary resource: .tscn for PackedScene, .tres otherwise. */
427
+ export function rsrcToText(r) {
428
+ const major = r.major >= 4 ? 4 : 3;
429
+ const isScene = r.type === 'PackedScene';
430
+ const lines = [];
431
+ const extLines = r.ext.map(e => (major === 4 ? `[ext_resource type=${qstr(e.type)}${e.uid != null && e.uid >= 0n ? ` uid=${qstr(uidText(e.uid))}` : ''} path=${qstr(e.path)} id=${qstr(e.id)}]` : `[ext_resource path=${qstr(e.path)} type=${qstr(e.type)} id=${e.id}]`));
432
+ const main = r.resources.at(-1);
433
+ const subs = r.resources.slice(0, -1);
434
+ const loadSteps = r.ext.length + subs.length + 1;
435
+ const propLine = (k, v) => `${k} = ${variantText(v, major)}`;
436
+ if (isScene) {
437
+ const bundled = main?.props.find(([k]) => k === '_bundled')?.[1];
438
+ lines.push(`[gd_scene load_steps=${loadSteps} format=${major === 4 ? 3 : 2}${r.uid != null && !r.uid.startsWith('-') ? ` uid=${qstr(uidText(r.uid))}` : ''}]`, '');
439
+ lines.push(...extLines);
440
+ for (const s of subs) { lines.push('', `[sub_resource type=${qstr(s.type)} id=${major === 4 ? qstr(s.id) : s.id}]`, ...s.props.filter(([k]) => !/^resource_(local_to_scene|name)$/.test(k) || true).map(([k, v]) => propLine(k, v))); }
441
+ if (!bundled || bundled.kind !== 'dict') { lines.push('', '# PORT: PackedScene without _bundled data'); return { text: lines.join('\n') + '\n', ext: '.tscn' }; }
442
+ const scene = unpackScene(bundled, major);
443
+ const pathOf = []; // node index → scene path string
444
+ for (const n of scene.nodes) {
445
+ const parentPath = n.parent == null ? null : n.parent.path != null ? n.parent.path : pathOf[n.parent.node];
446
+ const myPath = n.parent == null ? '.' : parentPath === '.' ? n.name : `${parentPath}/${n.name}`;
447
+ pathOf[n.index] = myPath;
448
+ const attrs = [`name=${qstr(n.name)}`];
449
+ if (n.type) attrs.push(`type=${qstr(n.type)}`);
450
+ if (n.parent != null) attrs.push(`parent=${qstr(parentPath ?? '.')}`);
451
+ if (n.instance?.resource) attrs.push(`instance=${variantText(n.instance.resource, major)}`);
452
+ if (n.instance?.placeholder) attrs.push(`instance_placeholder=${variantText(n.instance.placeholder, major)}`);
453
+ if (n.groups.length) attrs.push(`groups=[${n.groups.map(qstr).join(', ')}]`);
454
+ lines.push('', `[node ${attrs.join(' ')}]`);
455
+ for (const pr of n.props) lines.push(propLine(pr.name, pr.value));
456
+ }
457
+ for (const c of scene.connections) lines.push('', `[connection signal=${qstr(c.signal)} from=${qstr(typeof c.from === 'number' ? pathOf[c.from] : c.from)} to=${qstr(typeof c.to === 'number' ? pathOf[c.to] : c.to)} method=${qstr(c.method)}${c.flags && c.flags !== 2 ? ` flags=${c.flags & ~2}` : ''}${c.binds.length ? ` binds=[${c.binds.map(b => variantText(b, major)).join(', ')}]` : ''}${c.unbinds ? ` unbinds=${c.unbinds}` : ''}]`);
458
+ for (const e of scene.editableInstances) lines.push('', `[editable path=${qstr(e)}]`);
459
+ return { text: lines.join('\n') + '\n', ext: '.tscn', nodes: scene.nodes.length, connections: scene.connections.length };
460
+ }
461
+ lines.push(`[gd_resource type=${qstr(r.type)}${r.scriptClass ? ` script_class=${qstr(r.scriptClass)}` : ''} load_steps=${loadSteps} format=${major === 4 ? 3 : 2}${r.uid != null && !r.uid.startsWith('-') ? ` uid=${qstr(uidText(r.uid))}` : ''}]`, '');
462
+ lines.push(...extLines);
463
+ for (const s of subs) { lines.push('', `[sub_resource type=${qstr(s.type)} id=${major === 4 ? qstr(s.id) : s.id}]`, ...s.props.map(([k, v]) => propLine(k, v))); }
464
+ lines.push('', '[resource]', ...(main?.props ?? []).map(([k, v]) => propLine(k, v)));
465
+ return { text: lines.join('\n') + '\n', ext: '.tres' };
466
+ }
467
+ function uidText(uid) {
468
+ // ResourceUID::id_to_text: base-34 over a–y and 0–8 (Godot core/io/resource_uid.cpp).
469
+ const CH = 'abcdefghijklmnopqrstuvwxy012345678';
470
+ let n = BigInt(uid);
471
+ if (n < 0n) return null;
472
+ let s = '';
473
+ do { s = CH[Number(n % 34n)] + s; n /= 34n; } while (n > 0n);
474
+ return `uid://${s}`;
475
+ }
476
+
477
+ // ------------------------------------------------------------------------------------------------ project.binary
478
+ export function decodeProjectBinary(buf) {
479
+ if (buf.toString('latin1', 0, 4) !== 'ECFG') throw new RPError('BAD_FORMAT', 'Not an exported project settings file (ECFG).');
480
+ const count = buf.readUInt32LE(4);
481
+ let p = 8;
482
+ const entries = [];
483
+ for (let i = 0; i < count && p < buf.length; i++) {
484
+ const kl = buf.readUInt32LE(p); p += 4; const key = buf.toString('utf8', p, p + kl); p += kl;
485
+ const vl = buf.readUInt32LE(p); p += 4;
486
+ let value;
487
+ try { value = decodeMarshal(buf, p, 4).value; } catch { try { value = decodeMarshal(buf, p, 3).value; } catch { value = V('string', `<${vl} bytes>`); } }
488
+ p += vl;
489
+ entries.push({ key, value });
490
+ }
491
+ const major = entries.some(e => /(^|\/)config\/features$/.test(e.key) || /_pressed|PackedStringArray|stringname/.test(JSON.stringify(e.value))) ? 4 : 3;
492
+ const sections = new Map();
493
+ for (const e of entries) { const [sec, ...rest] = e.key.includes('/') ? e.key.split('/') : ['', e.key]; const k = rest.join('/'); if (!sections.has(sec)) sections.set(sec, []); sections.get(sec).push([k, e.value]); }
494
+ const lines = ['; Engine configuration file.', '; Rebuilt by ReversePort from project.binary (an export); defaults are not included.', '', `config_version=${major === 4 ? 5 : 4}`, ''];
495
+ for (const [sec, items] of [...sections.entries()].sort((a, b) => (a[0] === '' ? -1 : b[0] === '' ? 1 : a[0].localeCompare(b[0])))) {
496
+ if (sec) lines.push(`[${sec}]`, '');
497
+ for (const [k, v] of items) lines.push(`${k}=${variantText(v, major)}`);
498
+ lines.push('');
499
+ }
500
+ return { major, settings: entries.length, text: lines.join('\n') };
501
+ }
502
+
503
+ // ------------------------------------------------------------------------------------------------ tool
504
+ export async function decodeTool({ path: target, out, limit: max = 2000 } = {}) {
505
+ if (!target) throw new RPError('INVALID_ARGUMENT', 'path is required: a .gdc/.scn/.res/project.binary file, or a folder (an extracted .pck).');
506
+ const abs = path.resolve(target);
507
+ const st = await statOrNull(abs);
508
+ if (!st) throw new RPError('NOT_FOUND', `not found: ${abs}`);
509
+ const files = [];
510
+ let root;
511
+ const remaps = new Map(); // exported file → original path, from the .remap files an export leaves
512
+ if (st.isDirectory()) { root = await requireDir(abs); const w = await walk(root, { maxFiles: 50000, ignore: new Set(['.git', 'node_modules']) });
513
+ for (const f of w.files) if (f.rel.endsWith('.remap')) { const t = await fs.readFile(f.abs, 'utf8').catch(() => ''); const m = t.match(/^path="res:\/\/([^"]+)"/m); if (m) remaps.set(m[1], f.rel.split(path.sep).join('/').replace(/\.remap$/, '')); }
514
+ for (const f of w.files) if (/\.(gdc|gde|scn|res|binary|material|mesh|tres|anim|shader)$/i.test(f.rel) || path.basename(f.rel) === 'project.binary') files.push({ rel: f.rel, abs: f.abs }); }
515
+ else { root = path.dirname(abs); files.push({ rel: path.basename(abs), abs }); }
516
+ const outDir = out ? path.resolve(out) : path.join(await workspaceDir('decoded'), path.basename(root).replace(/[^A-Za-z0-9._-]+/g, '_') || 'project');
517
+ const results = [], skipped = [];
518
+ let written = 0;
519
+ for (const f of files.slice(0, max)) {
520
+ const buf = await fs.readFile(f.abs).catch(() => null);
521
+ if (!buf) continue;
522
+ const magic = buf.toString('latin1', 0, 4);
523
+ try {
524
+ let text, destRel, info = {};
525
+ if (magic === 'GDSC') { const r = decodeGdc(buf); text = r.source; destRel = f.rel.replace(/\.gdc$/i, '.gd'); info = { engine: r.engine, bytecodeVersion: r.bytecodeVersion, tokens: r.tokens, ...(r.compressed ? { compressed: true } : {}), ...(r.warnings.length ? { warnings: r.warnings.slice(0, 5) } : {}) }; }
526
+ else if (magic === 'RSRC' || magic === 'RSCC') { const r = readRsrc(buf); const t = rsrcToText(r); text = t.text; const orig = remaps.get(f.rel.split(path.sep).join('/')); destRel = orig ? orig.replace(/\.(tscn|tres|scn|res)$/i, t.ext) : f.rel.replace(/\.(scn|res)$/i, t.ext); if (destRel === f.rel) destRel += t.ext; info = { type: r.type, godot: `${r.major}.${r.minor}`, format: r.format, ...(t.nodes != null ? { nodes: t.nodes, connections: t.connections } : {}), resources: r.resources.length, external: r.ext.length }; }
527
+ else if (magic === 'ECFG') { const r = decodeProjectBinary(buf); text = r.text; destRel = path.posix.join(path.posix.dirname(f.rel.split(path.sep).join('/')), 'project.godot'); info = { settings: r.settings, godot: r.major }; }
528
+ else if (magic === 'GDPC') { skipped.push({ file: f.rel, reason: 'a pack: extract it with re_archive first, then decode the folder' }); continue; }
529
+ else if (/^\[gd_(scene|resource)|^;|^extends|^@tool|^class_name|^shader_type/.test(buf.toString('utf8', 0, 64))) { skipped.push({ file: f.rel, reason: 'already text' }); continue; }
530
+ else { skipped.push({ file: f.rel, reason: `not a Godot binary format (${JSON.stringify(magic)})` }); continue; }
531
+ const dest = path.join(outDir, destRel);
532
+ await fs.mkdir(path.dirname(dest), { recursive: true });
533
+ await fs.writeFile(dest, text);
534
+ written++;
535
+ results.push({ file: f.rel, decoded: destRel, ...info });
536
+ } catch (e) { skipped.push({ file: f.rel, reason: e.message }); }
537
+ }
538
+ const byKind = {};
539
+ for (const r of results) byKind[r.decoded.split('.').pop()] = (byKind[r.decoded.split('.').pop()] ?? 0) + 1;
540
+ return {
541
+ source: root, out: outDir, decoded: written, byKind, files: results.slice(0, 500), skipped: skipped.slice(0, 200), truncated: files.length > max,
542
+ next: written ? `Open ${outDir} as a Godot project (project.godot was rebuilt if project.binary was present; textures stay .ctex/.stex imports — copy the original .import files or re-import). Comments and formatting are gone; names, constants and structure are exact. Run re_apicheck on the .gd files to confirm the Godot version, then re_graph/port_plan as usual.` : 'Nothing decoded: give a .gdc, .scn, .res or project.binary, or the folder re_archive extracted from the .pck.',
543
+ };
544
+ }