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.
- package/README.md +71 -26
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/bin/reverseport.mjs +13 -1
- package/dist/generatedPackageMetadata.js +1 -1
- package/licenses/README.md +1 -0
- package/licenses/godot-engine-MIT.txt +20 -0
- package/native/windows/build/manifest.json +1 -1
- package/package.json +1 -1
- package/port/cli.mjs +6 -4
- package/port/knowledge/apis/godot-3-to-4-renames.json +1 -0
- package/port/knowledge/apis/godot-3.json +1 -0
- package/port/knowledge/apis/godot-4.3.json +1 -0
- package/port/knowledge/apis/godot-4.7.json +1 -0
- package/port/src/agent.mjs +1 -1
- package/port/src/agentdisk.mjs +83 -0
- package/port/src/apicheck.mjs +280 -0
- package/port/src/archive.mjs +34 -4
- package/port/src/convert.mjs +3 -1
- package/port/src/dormant.mjs +159 -0
- package/port/src/formats.mjs +1 -0
- package/port/src/gamearchives.mjs +476 -0
- package/port/src/godotbin.mjs +544 -0
- package/port/src/il2cpp.mjs +148 -0
- package/port/src/jsworkspace.mjs +2 -2
- package/port/src/knowledge.mjs +3 -1
- package/port/src/physics.mjs +135 -0
- package/port/src/plan.mjs +94 -11
- package/port/src/porters.mjs +87 -5
- package/port/src/refs.mjs +120 -0
- package/port/src/rpgmaker.mjs +416 -0
- package/port/src/save.mjs +326 -0
- package/port/src/scene.mjs +37 -3
- package/port/src/shader.mjs +385 -0
- package/port/src/strings.mjs +327 -0
- package/port/src/tools.mjs +48 -8
- package/port/src/trace.mjs +302 -0
- package/port/src/unity-anim.mjs +229 -0
- package/port/src/upgrade.mjs +219 -0
- package/skills/build-inspector/SKILL.md +8 -0
- package/skills/godot-port/SKILL.md +2 -0
- package/skills/port-verify/SKILL.md +20 -9
- package/skills/reverse-engineer-anything/SKILL.md +18 -2
- package/skills/reverse-engineer-anything/references/porting-and-rebuilds.md +35 -4
- package/skills/unity-to-godot/SKILL.md +14 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Dormant-logic checks for port_verify: a port can have every function name and still do nothing.
|
|
2
|
+
// - empty bodies: functions whose body is only pass / return / null / a TODO / a thrown "not implemented"
|
|
3
|
+
// - thin bodies: the source function had many statements, the port has very few
|
|
4
|
+
// - unwired: functions the source called (or signals it connected) that the port defines but never calls/connects
|
|
5
|
+
// - suppressions the port added: as any, @ts-ignore, eslint-disable, @warning_ignore, #pragma warning disable, @SuppressWarnings
|
|
6
|
+
// Language-agnostic by design: it works on braces/indentation and statement counts, never on a full parse.
|
|
7
|
+
import { extractSymbols, stripComments, languageOf } from './symbols.mjs';
|
|
8
|
+
import { nameKey } from './util.mjs';
|
|
9
|
+
|
|
10
|
+
const INDENT_LANGS = new Set(['gdscript', 'python']);
|
|
11
|
+
const EMPTY_BODY = /^\s*(?:pass|return(?:\s+(?:null|nil|None|0|false|\{\}|\[\]|""|''|new\s+\w+\(\)|default))?\s*;?|throw\s+new\s+(?:NotImplementedException|Error\(["'][^"']*not implemented[^"']*["']\))\s*\(?\)?\s*;?|print(?:_debug|_rich)?\([^)]*\)|console\.(?:log|warn)\([^)]*\);?|Debug\.Log(?:Warning)?\([^)]*\);?|UE_LOG\([^)]*\);?|\.\.\.|TODO.*|PORT.*|FIXME.*|\/\/.*|#.*)\s*$/;
|
|
12
|
+
const SUPPRESSIONS = [
|
|
13
|
+
{ re: /\bas\s+any\b/g, what: 'as any' }, { re: /@ts-ignore|@ts-nocheck|@ts-expect-error/g, what: '@ts-ignore' }, { re: /eslint-disable/g, what: 'eslint-disable' },
|
|
14
|
+
{ re: /@warning_ignore\s*\(/g, what: '@warning_ignore' }, { re: /#pragma\s+warning\s+disable/g, what: '#pragma warning disable' }, { re: /@SuppressWarnings\s*\(/g, what: '@SuppressWarnings' },
|
|
15
|
+
{ re: /#\s*noqa\b|#\s*type:\s*ignore/g, what: '# noqa / type: ignore' }, { re: /\bunsafe\s*\{/g, what: 'unsafe block' }, { re: /\.unwrap\(\)\s*;?\s*\/\/\s*(?:TODO|PORT|FIXME)/g, what: 'unwrap with a TODO' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** Split code into function bodies by name: { name, line, body, statements }. Best effort, no full parser. */
|
|
19
|
+
export function functionBodies(text, language) {
|
|
20
|
+
const code = stripComments(text, language);
|
|
21
|
+
const sym = extractSymbols(code, language);
|
|
22
|
+
const lines = code.split('\n');
|
|
23
|
+
const lineStart = [0];
|
|
24
|
+
for (let i = 0; i < code.length; i++) if (code[i] === '\n') lineStart.push(i + 1);
|
|
25
|
+
const fns = [...sym.functions].sort((a, b) => a.line - b.line);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (let i = 0; i < fns.length; i++) {
|
|
28
|
+
const start = fns[i].line - 1;
|
|
29
|
+
let body;
|
|
30
|
+
if (INDENT_LANGS.has(language)) {
|
|
31
|
+
const indent = lines[start].match(/^[ \t]*/)[0].length;
|
|
32
|
+
let end = start + 1;
|
|
33
|
+
while (end < lines.length && (!lines[end].trim() || lines[end].match(/^[ \t]*/)[0].length > indent)) end++;
|
|
34
|
+
body = lines.slice(start + 1, end).join('\n');
|
|
35
|
+
const same = lines[start].match(/:\s*(\S.*)$/); // `func f(): return 1`
|
|
36
|
+
if (!body.trim() && same && !/^\s*(?:->|:)/.test(same[1])) body = same[1];
|
|
37
|
+
} else {
|
|
38
|
+
// Brace languages: from the first { after the signature to its matching } (strings skipped), or `=> expr`.
|
|
39
|
+
const from = lineStart[start];
|
|
40
|
+
const nextSig = i + 1 < fns.length ? lineStart[fns[i + 1].line - 1] : code.length;
|
|
41
|
+
const head = code.slice(from, Math.min(code.length, from + 600));
|
|
42
|
+
const arrow = head.match(/\)\s*(?:[\w<>[\],?]+\s+)?=>\s*([^\n;{]+)/);
|
|
43
|
+
const brace = head.indexOf('{');
|
|
44
|
+
if (arrow && (brace < 0 || head.indexOf('=>') < brace)) body = arrow[1];
|
|
45
|
+
else if (brace >= 0) {
|
|
46
|
+
let depth = 0, j = from + brace, quote = null;
|
|
47
|
+
for (; j < code.length; j++) {
|
|
48
|
+
const ch = code[j];
|
|
49
|
+
if (quote) { if (ch === '\\') j++; else if (ch === quote) quote = null; continue; }
|
|
50
|
+
if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
|
|
51
|
+
if (ch === '{') depth++; else if (ch === '}') { depth--; if (depth === 0) break; }
|
|
52
|
+
}
|
|
53
|
+
body = code.slice(from + brace + 1, Math.min(j, nextSig > from + brace ? Math.max(j, 0) : j));
|
|
54
|
+
} else body = '';
|
|
55
|
+
}
|
|
56
|
+
const statements = body.split(/[\n;]/).map(l => l.trim()).filter(l => l && !/^[{}]$/.test(l) && !/^(?:\/\/|#|\*|\/\*)/.test(l));
|
|
57
|
+
out.push({ name: fns[i].name, line: fns[i].line, body, statements: statements.length, empty: statements.length === 0 || (statements.length <= 2 && statements.every(s => EMPTY_BODY.test(s))) });
|
|
58
|
+
}
|
|
59
|
+
return { functions: out, signals: sym.signals ?? [], code };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Names called or connected somewhere in `code` (not at their own definition). */
|
|
63
|
+
export function callSites(code, defined) {
|
|
64
|
+
const calls = new Map();
|
|
65
|
+
for (const name of defined) {
|
|
66
|
+
const key = name.replace(/[^\w]/g, '');
|
|
67
|
+
if (!key) continue;
|
|
68
|
+
const re = new RegExp(`(?<![\\w.])(?:[\\w.]+\\.)?${key}\\s*\\(|["'&]${key}["')]|[(,]\\s*(?:self\\.|this\\.)?${key}\\s*[,)]|[+-]=\\s*(?:self\\.|this\\.)?${key}\\b|Callable\\([^)]*["']${key}["']\\)|nameof\\(${key}\\)`, 'g');
|
|
69
|
+
const defRe = new RegExp(`\\b(?:func|def|function|void|int|bool|float|string|var|let|const|async|public|private|protected|static|override|fn)\\b[^\\n]*\\b${key}\\s*\\(`, 'g');
|
|
70
|
+
const total = (code.match(re) ?? []).length;
|
|
71
|
+
const defs = (code.match(defRe) ?? []).length;
|
|
72
|
+
calls.set(name, Math.max(0, total - defs));
|
|
73
|
+
}
|
|
74
|
+
return calls;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compare one ported unit with its source.
|
|
79
|
+
* Returns findings: [{ kind, name, detail }], with kinds empty-body, thin-body, unwired, suppression.
|
|
80
|
+
*/
|
|
81
|
+
export function dormantFindings({ sourceText, sourceLang, targetText, targetLang, lifecycleGroups = [] }) {
|
|
82
|
+
const findings = [];
|
|
83
|
+
const tgt = functionBodies(targetText, targetLang);
|
|
84
|
+
const src = sourceText && sourceLang ? functionBodies(sourceText, sourceLang) : null;
|
|
85
|
+
const srcByKey = new Map((src?.functions ?? []).map(f => [nameKey(f.name), f]));
|
|
86
|
+
const lifecycleKeys = new Set(lifecycleGroups.flat().map(nameKey));
|
|
87
|
+
|
|
88
|
+
for (const f of tgt.functions) {
|
|
89
|
+
if (f.empty) {
|
|
90
|
+
const s = srcByKey.get(nameKey(f.name)) ?? [...srcByKey.values()].find(x => sameLifecycle(x.name, f.name, lifecycleGroups));
|
|
91
|
+
if (!s || !s.empty) findings.push({ kind: 'empty-body', name: f.name, line: f.line, detail: s ? `source has ${s.statements} statement(s); the port's body is empty or a stub` : 'body is empty or a stub' });
|
|
92
|
+
} else if (src) {
|
|
93
|
+
const s = srcByKey.get(nameKey(f.name)) ?? [...srcByKey.values()].find(x => sameLifecycle(x.name, f.name, lifecycleGroups));
|
|
94
|
+
if (s && s.statements >= 6 && f.statements * 3 < s.statements) findings.push({ kind: 'thin-body', name: f.name, line: f.line, detail: `source has ${s.statements} statement(s), the port ${f.statements}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (src) {
|
|
99
|
+
const srcCalls = callSites(src.code, src.functions.map(f => f.name));
|
|
100
|
+
const tgtCalls = callSites(tgt.code, tgt.functions.map(f => f.name));
|
|
101
|
+
for (const f of tgt.functions) {
|
|
102
|
+
const key = nameKey(f.name);
|
|
103
|
+
if (lifecycleKeys.has(key) || /^_/.test(f.name) && lifecycleKeys.has(key)) continue;
|
|
104
|
+
const s = srcByKey.get(key);
|
|
105
|
+
if (!s) continue;
|
|
106
|
+
const calledInSource = srcCalls.get(s.name) ?? 0;
|
|
107
|
+
const calledInPort = tgtCalls.get(f.name) ?? 0;
|
|
108
|
+
if (calledInSource > 0 && calledInPort === 0) findings.push({ kind: 'unwired', name: f.name, line: f.line, detail: `the source calls/connects it ${calledInSource}×; nothing in the port does` });
|
|
109
|
+
}
|
|
110
|
+
for (const sig of src.signals) {
|
|
111
|
+
const srcUses = (src.code.match(new RegExp(`\\b${sig.name}\\b\\.(?:connect|emit)|emit_signal\\(\\s*["']${sig.name}["']`, 'g')) ?? []).length;
|
|
112
|
+
const variants = [sig.name, sig.name.replace(/_(\w)/g, (_, c) => c.toUpperCase()), sig.name[0].toUpperCase() + sig.name.slice(1).replace(/_(\w)/g, (_, c) => c.toUpperCase())];
|
|
113
|
+
const portUses = variants.reduce((n, v) => n + (tgt.code.match(new RegExp(`\\b${v}\\b\\s*(?:\\?\\.|\\.)?\\s*(?:Invoke|emit|connect|\\()|emit_signal\\(\\s*["']${v}["']|EmitSignal\\(\\s*(?:SignalName\\.)?${v}\\b|\\b${v}\\b\\s*[+-]=|\\.(?:on|addEventListener|emit)\\(\\s*["']${v}["']`, 'g')) ?? []).length, 0);
|
|
114
|
+
if (srcUses > 0 && portUses === 0) findings.push({ kind: 'unwired', name: `signal ${sig.name}`, detail: `emitted/connected ${srcUses}× in the source; the port only declares it (or not at all)` });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const srcSupp = countSuppressions(sourceText ?? '');
|
|
119
|
+
for (const { what, count } of countSuppressions(targetText)) {
|
|
120
|
+
const before = srcSupp.find(s => s.what === what)?.count ?? 0;
|
|
121
|
+
if (count > before) findings.push({ kind: 'suppression', name: what, detail: `${count}× in the port (${before}× in the source): hides errors instead of porting them` });
|
|
122
|
+
}
|
|
123
|
+
return findings;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sameLifecycle(a, b, groups) { return groups.some(g => g.map(nameKey).includes(nameKey(a)) && g.map(nameKey).includes(nameKey(b))); }
|
|
127
|
+
|
|
128
|
+
export function countSuppressions(text) {
|
|
129
|
+
return SUPPRESSIONS.map(s => ({ what: s.what, count: (text.match(s.re) ?? []).length })).filter(s => s.count);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A compact "resume pack" for an agent that lost its context: what the plan is, where it stands, the next unit with
|
|
134
|
+
* the signatures of its already-ported dependencies, and the open findings. Sized for a budget in characters.
|
|
135
|
+
*/
|
|
136
|
+
export function resumePack(plan, { verify = null, budget = 6000 } = {}) {
|
|
137
|
+
const units = plan.units ?? [];
|
|
138
|
+
const done = units.filter(u => u.status === 'done').length;
|
|
139
|
+
const next = units.find(u => u.status === 'in_progress') ?? units.find(u => u.status === 'pending');
|
|
140
|
+
const lines = [
|
|
141
|
+
`# Port ${plan.id}: ${plan.from} → ${plan.to}`,
|
|
142
|
+
`source ${plan.source}`, `output ${plan.output}`,
|
|
143
|
+
`progress ${done}/${units.length} done, ${units.filter(u => u.status === 'partial').length} partial, ${units.filter(u => u.status === 'skipped').length} skipped`,
|
|
144
|
+
];
|
|
145
|
+
if (next) {
|
|
146
|
+
lines.push('', `## Next: ${next.id} ${next.source} → ${next.target} (${next.action})`);
|
|
147
|
+
const deps = (next.dependsOn ?? []).map(d => units.find(u => u.id === d)).filter(Boolean);
|
|
148
|
+
for (const d of deps.slice(0, 12)) lines.push(`- dep ${d.id} ${d.target} [${d.status}]${d.targetSymbols ? `: ${[...(d.targetSymbols.functions ?? d.targetSymbols)].slice(0, 8).map(x => x.name ?? x).join(', ')}` : ''}`);
|
|
149
|
+
}
|
|
150
|
+
if (verify) {
|
|
151
|
+
const n = x => (Array.isArray(x) ? x.length : typeof x === 'number' ? x : 0);
|
|
152
|
+
lines.push('', `## Verify${verify.at ? ` (${verify.at})` : ''}: ${verify.passed ? 'passed' : 'not passed'} (${n(verify.errors)} errors, ${n(verify.warnings)} warnings)`);
|
|
153
|
+
for (const w of (Array.isArray(verify.warnings) ? verify.warnings : []).slice(0, 15)) lines.push(`- ${w.unit ?? ''} ${w.issue}${w.findings ? `: ${w.findings.slice(0, 4).map(f => `${f.kind} ${f.name}`).join(', ')}` : ''}`.trim());
|
|
154
|
+
}
|
|
155
|
+
lines.push('', `## Rules`, `- Port every statement; an empty body or "as any" is not done.`, `- After each unit: port_write, then port_verify before claiming completion.`);
|
|
156
|
+
let text = lines.join('\n');
|
|
157
|
+
if (text.length > budget) text = text.slice(0, budget - 20) + '\n… (truncated)';
|
|
158
|
+
return text;
|
|
159
|
+
}
|
package/port/src/formats.mjs
CHANGED
|
@@ -31,6 +31,7 @@ export const FORMATS = [
|
|
|
31
31
|
{ id: 'il2cpp-metadata', name: 'Unity IL2CPP global-metadata', category: 'engine', test: (b, o) => u32le(b, o) === 0xfab11baf },
|
|
32
32
|
{ id: 'unreal-asset', name: 'Unreal package (.uasset/.umap)', category: 'engine', test: (b, o) => u32le(b, o) === 0x9e2a83c1 },
|
|
33
33
|
{ id: 'gamemaker-data', name: 'GameMaker data (FORM/GEN8)', category: 'engine', test: (b, o) => str(b, o, 'FORM') && str(b, o + 8, 'GEN8') },
|
|
34
|
+
{ id: 'swf', name: 'Flash SWF', category: 'engine', offsetZeroOnly: true, test: (b, o) => o === 0 && ['FWS', 'CWS', 'ZWS'].includes(b.toString('latin1', o, o + 3)) && b[o + 3] >= 1 && b[o + 3] <= 60 },
|
|
34
35
|
{ id: 'renpy-rpa', name: "Ren'Py archive (RPA)", category: 'archive', test: (b, o) => str(b, o, 'RPA-3.0 ') || str(b, o, 'RPA-2.0 ') },
|
|
35
36
|
{ id: 'asar', name: 'Electron archive (asar)', category: 'archive', offsetZeroOnly: true, test: (b, o) => o === 0 && u32le(b, 0) === 4 && str(b, 16, '{"files"') },
|
|
36
37
|
{ id: 'zip', name: 'ZIP archive (also apk/jar/ipa/docx/nupkg)', category: 'archive', test: (b, o) => at(b, o, [0x50, 0x4b, 3, 4]) || at(b, o, [0x50, 0x4b, 5, 6]) },
|