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,219 @@
|
|
|
1
|
+
// port_upgrade: Godot 3 → Godot 4 for a whole project, as a converted COPY plus a list of what still needs a human.
|
|
2
|
+
// It applies the official rename table (classes, builtin types, functions, properties, signals, enums, colors,
|
|
3
|
+
// theme overrides, project settings, shaders) and the syntax changes an agent gets wrong most (export/onready/tool
|
|
4
|
+
// annotations, yield → await, instance → instantiate, string connect/emit_signal → signal calls, Pool*Array,
|
|
5
|
+
// OS time functions → Time). What cannot be converted mechanically (setget, funcref, move_and_slide with
|
|
6
|
+
// arguments, Tween nodes, yield on a coroutine, format-2 scenes that Godot re-saves itself) is reported with
|
|
7
|
+
// file and line. Sources are never modified. Follow with re_apicheck on the copy.
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { RPError, requireDir, walk, limit, toPosix } from './util.mjs';
|
|
11
|
+
import { workspaceDir, isInside } from './workspace.mjs';
|
|
12
|
+
import { renameTables } from './apicheck.mjs';
|
|
13
|
+
|
|
14
|
+
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
|
|
16
|
+
const MANUAL = [
|
|
17
|
+
[/\bsetget\b/, 'setget', 'Godot 4 uses property blocks: var x: T:\n\tset(v): …\n\tget: …'],
|
|
18
|
+
[/\bfuncref\s*\(/, 'funcref', 'use a Callable: Callable(obj, "method") or obj.method'],
|
|
19
|
+
[/\bmove_and_slide\s*\([^)]/, 'move_and_slide(velocity, …)', 'Godot 4: set velocity (a CharacterBody property) then call move_and_slide() without arguments; it returns bool, the velocity stays in velocity'],
|
|
20
|
+
[/\bmove_and_slide_with_snap\s*\(/, 'move_and_slide_with_snap', 'set floor_snap_length and call move_and_slide()'],
|
|
21
|
+
[/\bmove_and_collide\s*\([^)]*,\s*\w+\s*,/, 'move_and_collide(vel, infinite_inertia, …)', 'Godot 4 signature: move_and_collide(motion, test_only=false, safe_margin, recovery_as_collision)'],
|
|
22
|
+
[/\byield\s*\(\s*[^,)]+\s*\)/, 'yield() without a signal', 'a coroutine pause: Godot 4 functions are coroutines when they await; replace with await on the signal or coroutine you wait for'],
|
|
23
|
+
[/\bTween\.new\s*\(|extends\s+Tween\b|type="Tween"/, 'Tween node', 'Godot 4 tweens are created with create_tween(); interpolate_property → tween_property, start() is implicit'],
|
|
24
|
+
[/\binterpolate_property\s*\(/, 'interpolate_property', 'var t := create_tween(); t.tween_property(obj, "prop", final, duration)'],
|
|
25
|
+
[/\bget_node\s*\(\s*"[^"]*"\s*\)\s*\.\s*connect\s*\(\s*"[^"]*"\s*,\s*[^,]+\s*,\s*"[^"]*"\s*,\s*\[/, 'connect with binds', 'bind with Callable.bind(): sig.connect(method.bind(args))'],
|
|
26
|
+
[/\bNavigation2D\b|\bNavigation\b(?!Server|Agent|Region|Polygon|Mesh|Obstacle|Link|Layer|Path)/, 'Navigation node', 'Godot 4 uses NavigationServer + NavigationAgent2D/3D and NavigationRegion2D/3D'],
|
|
27
|
+
[/\bVisualServer\b|\bPhysics2DServer\b|\bPhysicsServer\b(?!2D|3D)/, 'server singletons', 'RenderingServer / PhysicsServer2D / PhysicsServer3D: names converted, but many methods changed'],
|
|
28
|
+
[/\bget_overlapping_(bodies|areas)\s*\(\s*\)/, 'overlap queries', 'converted; in Godot 4 these need monitoring=true and update one physics frame later'],
|
|
29
|
+
[/\bOS\.(window_|set_window_|get_window_|center_window|is_window_|get_screen_)/, 'OS window functions', 'get_window() / DisplayServer in Godot 4'],
|
|
30
|
+
[/\brandomize\s*\(\s*\)/, 'randomize()', 'converted call is fine; Godot 4 seeds the RNG automatically, so remove it or keep it'],
|
|
31
|
+
[/\bexport\s*\(\s*(?:PackedScene|Resource|NodePath|Texture)\b/, 'export(Resource)', 'converted to @export var x: Type where the type is clear; check the type and hints (@export_file, @export_node_path)'],
|
|
32
|
+
[/\bget_tree\(\)\.(set_network_peer|network_peer|is_network_server|get_network_unique_id|network_connected|connected_to_server|server_disconnected)\b/, 'SceneTree networking', 'multiplayer API moved: get_tree().get_multiplayer() / multiplayer.multiplayer_peer, multiplayer.is_server()'],
|
|
33
|
+
[/\brpc_(unreliable|id|unreliable_id)\s*\(|\bremote\s+func|\bmaster\s+func|\bpuppet\s+func/, 'RPC keywords', 'Godot 4: @rpc("any_peer", "unreliable") annotations and rpc()/rpc_id()'],
|
|
34
|
+
[/\bget_tree\(\)\.change_scene\s*\(/, 'change_scene(path)', 'converted to change_scene_to_file; for a PackedScene use change_scene_to_packed'],
|
|
35
|
+
[/\bString\s*\(\s*[\w.]+\s*\)\s*\.format|\.percent_encode\(|\.http_escape\(/, 'string helpers', 'http_escape → uri_encode, percent_encode → uri_encode'],
|
|
36
|
+
[/^\s*var\s+velocity\b/, 'var velocity in a CharacterBody script', 'CharacterBody2D/3D already has velocity: delete this declaration and use the built-in property'],
|
|
37
|
+
[/\bSCREEN_TEXTURE\b/, 'SCREEN_TEXTURE', 'Godot 4 shaders declare it: uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;'],
|
|
38
|
+
[/\bDEPTH_TEXTURE\b/, 'DEPTH_TEXTURE', 'Godot 4 shaders declare it: uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap;'],
|
|
39
|
+
[/\bNode\.pause_mode\b|\bpause_mode\s*=\s*(?:PAUSE_MODE_|\d)/, 'pause_mode', 'process_mode with PROCESS_MODE_PAUSABLE/WHEN_PAUSED/ALWAYS/DISABLED/INHERIT'],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function convertGdscript(text, R, localFuncs) {
|
|
43
|
+
let out = text;
|
|
44
|
+
const notes = [];
|
|
45
|
+
const rep = (re, to) => { out = out.replace(re, to); };
|
|
46
|
+
// Annotations.
|
|
47
|
+
rep(/^(\s*)tool\s*$/gm, '$1@tool');
|
|
48
|
+
rep(/^(\s*)onready\s+var\b/gm, '$1@onready var');
|
|
49
|
+
rep(/^(\s*)export\s*\(\s*(int|float|bool|String|Vector2|Vector3|Color|NodePath|Array|Dictionary|PackedScene|Resource|Texture2D|Texture|AudioStream|Font|Curve|Gradient|StyleBox|Shader|Material|Mesh)\s*\)\s+var\s+(\w+)\s*(?:=\s*(.+))?$/gm,
|
|
50
|
+
(m, ind, type, name, val) => `${ind}@export var ${name}: ${R.classes.get(type) ?? type}${val !== undefined ? ` = ${val}` : ''}`);
|
|
51
|
+
rep(/^(\s*)export\s*\(\s*(?:int|float)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)(?:\s*,\s*([-\d.]+))?\s*\)\s+var\b/gm, (m, ind, a, b, step) => `${ind}@export_range(${a}, ${b}${step ? `, ${step}` : ''}) var`);
|
|
52
|
+
rep(/^(\s*)export\s*\(\s*String\s*,\s*FILE(?:\s*,\s*("[^"]*"))?\s*\)\s+var\b/gm, (m, ind, f) => `${ind}@export_file${f ? `(${f})` : ''} var`);
|
|
53
|
+
rep(/^(\s*)export\s*\(\s*String\s*,\s*MULTILINE\s*\)\s+var\b/gm, '$1@export_multiline var');
|
|
54
|
+
rep(/^(\s*)export\s*\(\s*(?:int|String)\s*,\s*"([^"]*(?:"\s*,\s*"[^"]*)*)"\s*\)\s+var\b/gm, (m, ind, names) => `${ind}@export_enum("${names}") var`);
|
|
55
|
+
rep(/^(\s*)export\s*\(\s*int\s*,\s*FLAGS\s*(?:,\s*("[^"]*(?:"\s*,\s*"[^"]*)*"))?\s*\)\s+var\b/gm, (m, ind, names) => `${ind}@export_flags(${names ?? ''}) var`);
|
|
56
|
+
rep(/^(\s*)export\s*\(\s*int\s*,\s*LAYERS_2D_PHYSICS\s*\)\s+var\b/gm, '$1@export_flags_2d_physics var');
|
|
57
|
+
rep(/^(\s*)export\s*\(\s*int\s*,\s*LAYERS_3D_PHYSICS\s*\)\s+var\b/gm, '$1@export_flags_3d_physics var');
|
|
58
|
+
rep(/^(\s*)export\s*\(\s*Array\s*,\s*(\w+)\s*\)\s+var\s+(\w+)/gm, (m, ind, t, name) => `${ind}@export var ${name}: Array[${R.classes.get(t) ?? t}]`);
|
|
59
|
+
rep(/^(\s*)export\s+var\b/gm, '$1@export var');
|
|
60
|
+
rep(/^(\s*)export\s*\([^)]*\)\s+var\b/gm, (m, ind) => { notes.push('export hint kept as plain @export; check the hint'); return `${ind}@export var`; });
|
|
61
|
+
// yield(obj, "signal") → await obj.signal ; yield(coroutine_call) → await coroutine_call
|
|
62
|
+
const SIG = { idle_frame: 'process_frame', ...Object.fromEntries(R.signals) };
|
|
63
|
+
rep(/\byield\s*\(\s*((?:[^,()]|\((?:[^()]|\([^()]*\))*\))+?)\s*,\s*"(\w+)"\s*\)/g, (m, obj, sig) => `await ${obj.trim()}.${SIG[sig] ?? sig}`);
|
|
64
|
+
rep(/\byield\s*\(\s*([\w.]+\([^()]*\))\s*\)/g, 'await $1');
|
|
65
|
+
// emit_signal("sig", a, b) → sig.emit(a, b)
|
|
66
|
+
rep(/\bemit_signal\s*\(\s*"(\w+)"\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/g, (m, sig, args) => `${R.signals.get(sig) ?? sig}.emit(${args?.trim() ?? ''})`);
|
|
67
|
+
// connect("sig", obj, "method" [, binds]) → sig.connect(obj.method[.bind(...)]) ; obj.connect(...) → obj.sig.connect(...)
|
|
68
|
+
rep(/(?:(\$[\w/%]+|\w+(?:\([^()]*\))?(?:\.\w+(?:\([^()]*\))?)*)\.)?\bconnect\s*\(\s*"(\w+)"\s*,\s*([\w.$]+(?:\([^()]*\))?)\s*,\s*"(\w+)"\s*(?:,\s*(\[[^\]]*\]))?\s*(?:,\s*(\w+))?\s*\)/g,
|
|
69
|
+
(m, recv, sig, target, method, binds, flags) => {
|
|
70
|
+
const s = R.signals.get(sig) ?? sig;
|
|
71
|
+
const callable = `${target === 'self' ? '' : target + '.'}${method}${binds ? `.bind(${binds.slice(1, -1).trim()})` : ''}`;
|
|
72
|
+
return `${recv ? recv + '.' : ''}${s}.connect(${callable}${flags ? `, ${flags}` : ''})`;
|
|
73
|
+
});
|
|
74
|
+
rep(/\bdisconnect\s*\(\s*"(\w+)"\s*,\s*([\w.$]+)\s*,\s*"(\w+)"\s*\)/g, (m, sig, target, method) => `${R.signals.get(sig) ?? sig}.disconnect(${target === 'self' ? '' : target + '.'}${method})`);
|
|
75
|
+
rep(/\bis_connected\s*\(\s*"(\w+)"\s*,\s*([\w.$]+)\s*,\s*"(\w+)"\s*\)/g, (m, sig, target, method) => `${R.signals.get(sig) ?? sig}.is_connected(${target === 'self' ? '' : target + '.'}${method})`);
|
|
76
|
+
// instance() → instantiate(), OS time → Time, builtin types, classes.
|
|
77
|
+
rep(/\.instance\s*\(\s*\)/g, '.instantiate()');
|
|
78
|
+
rep(/\bOS\.get_ticks_msec\(\)/g, 'Time.get_ticks_msec()');
|
|
79
|
+
rep(/\bOS\.get_ticks_usec\(\)/g, 'Time.get_ticks_usec()');
|
|
80
|
+
rep(/\bOS\.get_unix_time\(\)/g, 'Time.get_unix_time_from_system()');
|
|
81
|
+
rep(/\bOS\.get_datetime\(\)/g, 'Time.get_datetime_dict_from_system()');
|
|
82
|
+
rep(/\bOS\.get_system_time_msecs\(\)/g, 'Time.get_ticks_msec()');
|
|
83
|
+
rep(/\bEngine\.editor_hint\b/g, 'Engine.is_editor_hint()');
|
|
84
|
+
rep(/\.empty\(\)/g, '.is_empty()');
|
|
85
|
+
rep(/\.invert\(\)/g, '.reverse()');
|
|
86
|
+
rep(/\bget_tree\(\)\.change_scene\s*\(/g, 'get_tree().change_scene_to_file(');
|
|
87
|
+
rep(/\bparse_json\s*\(/g, 'JSON.parse_string(');
|
|
88
|
+
rep(/\bto_json\s*\(/g, 'JSON.stringify(');
|
|
89
|
+
rep(/\bColor\.(\w+)\b/g, (m, c) => `Color.${R.colors.get(c) ?? c}`);
|
|
90
|
+
for (const [a, b] of R.builtins) rep(new RegExp(`\\b${esc(a)}\\b`, 'g'), b);
|
|
91
|
+
for (const [a, b] of R.classes) rep(new RegExp(`(?<![\\w."'/])${esc(a)}\\b(?!["'])`, 'g'), b);
|
|
92
|
+
// Functions: `name(` not preceded by a word char that would make it another identifier; skip locally defined funcs.
|
|
93
|
+
for (const [a, b] of R.functions) { if (a === 'yield' || localFuncs.has(a) || !/^\w+$/.test(b)) continue; rep(new RegExp(`(?<![\\w])${esc(a)}(?=\\s*\\()`, 'g'), b); }
|
|
94
|
+
// Properties: `.name` not followed by `(`; and bare `name =`/`name.` for self properties of common nodes.
|
|
95
|
+
for (const [a, b] of R.properties) rep(new RegExp(`\\.${esc(a)}\\b(?!\\s*\\()`, 'g'), `.${b}`);
|
|
96
|
+
for (const [a, b] of R.properties) rep(new RegExp(`^(\\s*)${esc(a)}(?=\\s*(?:=|\\.|\\+=|-=|\\*=|/=))`, 'gm'), `$1${b}`);
|
|
97
|
+
// Enums/constants.
|
|
98
|
+
for (const [a, b] of R.enums) rep(new RegExp(`\\b${esc(a)}\\b`, 'g'), b);
|
|
99
|
+
// Theme overrides in code: get("custom_colors/font_color") etc.
|
|
100
|
+
for (const [a, b] of R.themeOverrides) rep(new RegExp(esc(a), 'g'), b);
|
|
101
|
+
return { out, notes };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function convertScene(text, R) {
|
|
105
|
+
let out = text;
|
|
106
|
+
const rep = (re, to) => { out = out.replace(re, to); };
|
|
107
|
+
rep(/(\btype=")(\w+)(")/g, (m, a, t, c) => `${a}${R.classes.get(t) ?? t}${c}`);
|
|
108
|
+
rep(/(\[sub_resource type=")(\w+)/g, (m, a, t) => `${a}${R.classes.get(t) ?? t}`);
|
|
109
|
+
rep(/(\[ext_resource [^\]]*type=")(\w+)/g, (m, a, t) => `${a}${R.classes.get(t) ?? t}`);
|
|
110
|
+
for (const [a, b] of R.themeOverrides) rep(new RegExp(`^${esc(a)}`, 'gm'), b);
|
|
111
|
+
for (const [a, b] of R.properties) rep(new RegExp(`^${esc(a)}(?=\\s*=)`, 'gm'), b);
|
|
112
|
+
for (const [a, b] of R.builtins) rep(new RegExp(`\\b${esc(a)}\\s*\\(`, 'g'), `${b}(`);
|
|
113
|
+
rep(/\[connection signal="(\w+)" from="([^"]*)" to="([^"]*)" method="(\w+)"/g, (m, sig, from, to, method) => `[connection signal="${R.signals.get(sig) ?? sig}" from="${from}" to="${to}" method="${method}"`);
|
|
114
|
+
rep(/^(\[gd_scene[^\]]*?)format=2/gm, '$1format=3');
|
|
115
|
+
rep(/^(\[gd_resource[^\]]*?)format=2/gm, '$1format=3');
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function convertProject(text, R) {
|
|
120
|
+
let out = text.replace(/^config_version=4[ \t]*$/m, 'config_version=5');
|
|
121
|
+
for (const [a, b] of R.projectGodot) out = out.replace(new RegExp(`^${esc(a)}=`, 'gm'), `${b}=`);
|
|
122
|
+
for (const [a, b] of R.projectSettings) out = out.replace(new RegExp(`^${esc(a)}=`, 'gm'), `${b}=`);
|
|
123
|
+
for (const [a, b] of R.inputMap) out = out.split(a).join(b);
|
|
124
|
+
out = out.replace(/,"command":(?:true|false)/g, '');
|
|
125
|
+
if (/^\[application\]/m.test(out) && !/config\/features=/.test(out)) out = out.replace(/^\[application\][ \t]*$/m, '[application]\n\nconfig/features=PackedStringArray("4.3", "Forward Plus")');
|
|
126
|
+
if (/^\[rendering\]/m.test(out)) out = out.replace(/^environment\/default_environment=/m, 'environment/defaults/default_environment=');
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function convertShader(text, R) {
|
|
131
|
+
let out = text;
|
|
132
|
+
for (const [a, b] of R.shaders) out = out.replace(new RegExp(`\\b${esc(a)}\\b`, 'g'), b);
|
|
133
|
+
return out.replace(/\bhint_albedo\b/g, 'source_color').replace(/\bhint_color\b/g, 'source_color').replace(/\bhint_white\b/g, 'hint_default_white').replace(/\bhint_black\b/g, 'hint_default_black').replace(/\bhint_normal\b/g, 'hint_normal');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function diffCount(a, b) {
|
|
137
|
+
const la = a.split('\n'), lb = b.split('\n');
|
|
138
|
+
let n = 0;
|
|
139
|
+
for (let i = 0; i < Math.max(la.length, lb.length); i++) if (la[i] !== lb[i]) n++;
|
|
140
|
+
return n;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function manualFindings(text, rel) {
|
|
144
|
+
const found = [];
|
|
145
|
+
text.split('\n').forEach((l, i) => { for (const [re, what, how] of MANUAL) if (re.test(l)) found.push({ file: rel, line: i + 1, what, how }); });
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function upgradeTool({ path: target, action = 'check', out, limit: max = 300 } = {}) {
|
|
150
|
+
if (!target) throw new RPError('INVALID_ARGUMENT', 'path is required: a Godot 3 project folder.');
|
|
151
|
+
const root = await requireDir(target);
|
|
152
|
+
const pg = await fs.readFile(path.join(root, 'project.godot'), 'utf8').catch(() => null);
|
|
153
|
+
if (pg === null) throw new RPError('NOT_GODOT', `${root} has no project.godot.`);
|
|
154
|
+
if (/config_version=5/.test(pg)) throw new RPError('INVALID_ARGUMENT', 'This is already a Godot 4 project (config_version=5). Use re_apicheck to find leftovers.');
|
|
155
|
+
const rt = renameTables();
|
|
156
|
+
const raw = JSON.parse(await fs.readFile(new URL('../knowledge/apis/godot-3-to-4-renames.json', import.meta.url), 'utf8')).tables;
|
|
157
|
+
const R = { ...rt, colors: new Map(raw.color_renames), themeOverrides: raw.theme_override_renames, projectGodot: raw.project_godot_renames, projectSettings: raw.project_settings_renames, inputMap: raw.input_map_renames };
|
|
158
|
+
|
|
159
|
+
let outAbs = null;
|
|
160
|
+
if (action === 'apply') {
|
|
161
|
+
outAbs = out ? path.resolve(out) : path.join(await workspaceDir('upgrades'), path.basename(root).replace(/[^A-Za-z0-9._-]+/g, '_') + '-godot4');
|
|
162
|
+
if (isInside(outAbs, root) || isInside(root, outAbs)) throw new RPError('INVALID_ARGUMENT', 'out must be outside the source project.');
|
|
163
|
+
const existing = await fs.readdir(outAbs).catch(() => []);
|
|
164
|
+
if (existing.length) throw new RPError('OUTPUT_NOT_EMPTY', `out is not empty: ${outAbs}`);
|
|
165
|
+
} else if (action !== 'check') throw new RPError('INVALID_ARGUMENT', 'action must be check or apply.');
|
|
166
|
+
|
|
167
|
+
const { files, truncated } = await walk(root, { maxFiles: 50000 });
|
|
168
|
+
const changed = [], manual = [], renamedFiles = [];
|
|
169
|
+
let converted = 0, copied = 0;
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const ext = f.ext;
|
|
172
|
+
const rel = f.rel;
|
|
173
|
+
let text = null, after = null, destRel = rel;
|
|
174
|
+
if (['.gd', '.tscn', '.tres', '.godot', '.gdshader', '.shader', '.cfg', '.import'].includes(ext) || path.basename(rel) === 'project.godot') {
|
|
175
|
+
text = await fs.readFile(f.abs, 'utf8').catch(() => null);
|
|
176
|
+
}
|
|
177
|
+
if (text !== null) {
|
|
178
|
+
if (ext === '.gd') {
|
|
179
|
+
const localFuncs = new Set([...text.matchAll(/^\s*(?:static\s+)?func\s+(\w+)/gm)].map(m => m[1]));
|
|
180
|
+
after = convertGdscript(text, R, localFuncs).out;
|
|
181
|
+
manual.push(...manualFindings(after, rel).filter(x => x.what !== 'var velocity in a CharacterBody script' || /extends\s+CharacterBody[23]D/.test(after)));
|
|
182
|
+
} else if (ext === '.tscn' || ext === '.tres') { after = convertScene(text, R); manual.push(...manualFindings(after, rel).filter(x => x.what === 'Tween node')); }
|
|
183
|
+
else if (path.basename(rel) === 'project.godot') after = convertProject(text, R);
|
|
184
|
+
else if (ext === '.gdshader' || ext === '.shader') { after = convertShader(text, R); manual.push(...manualFindings(after, rel).filter(x => /TEXTURE/.test(x.what))); if (ext === '.shader') { destRel = rel.slice(0, -7) + '.gdshader'; renamedFiles.push({ from: rel, to: destRel }); } }
|
|
185
|
+
else if (ext === '.import') { after = null; destRel = null; } // Godot 4 regenerates .import files.
|
|
186
|
+
else after = text;
|
|
187
|
+
}
|
|
188
|
+
if (destRel === null) continue;
|
|
189
|
+
const lines = after !== null && after !== text ? diffCount(text, after) : 0;
|
|
190
|
+
if (lines) { converted++; changed.push({ file: rel, lines }); }
|
|
191
|
+
if (action === 'apply') {
|
|
192
|
+
const dest = path.join(outAbs, destRel);
|
|
193
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
194
|
+
if (after !== null) await fs.writeFile(dest, after); else await fs.copyFile(f.abs, dest);
|
|
195
|
+
copied++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Rename references to renamed .shader files.
|
|
199
|
+
if (action === 'apply' && renamedFiles.length) {
|
|
200
|
+
for (const f of files) if (['.tscn', '.tres', '.gd'].includes(f.ext)) {
|
|
201
|
+
const dest = path.join(outAbs, f.rel);
|
|
202
|
+
let t = await fs.readFile(dest, 'utf8').catch(() => null);
|
|
203
|
+
if (t === null) continue;
|
|
204
|
+
let changedRef = false;
|
|
205
|
+
for (const r of renamedFiles) { const re = new RegExp(esc(toPosix(r.from)), 'g'); if (re.test(t)) { t = t.replace(re, toPosix(r.to)); changedRef = true; } }
|
|
206
|
+
if (changedRef) await fs.writeFile(dest, t);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
changed.sort((a, b) => b.lines - a.lines);
|
|
210
|
+
const byWhat = {};
|
|
211
|
+
for (const m of manual) byWhat[m.what] = (byWhat[m.what] ?? 0) + 1;
|
|
212
|
+
return {
|
|
213
|
+
source: root, action, ...(outAbs ? { out: outAbs, filesWritten: copied } : {}), filesScanned: files.length, filesConverted: converted, truncated,
|
|
214
|
+
changed: limit(changed, max), renamedFiles: limit(renamedFiles, 100), manual: limit(manual, max), manualByKind: byWhat,
|
|
215
|
+
next: action === 'check'
|
|
216
|
+
? `port_upgrade action:"apply" writes the converted copy (default under the workspace, or out). ${manual.length} place(s) need a human afterwards; they are listed in manual.`
|
|
217
|
+
: `Fix the ${manual.length} manual item(s) in ${outAbs}, run re_apicheck {path:"${toPosix(outAbs)}", engine:"godot@4.3"} and open the folder in Godot 4 (it re-saves scenes to format 3 and regenerates .import files). Renamed .shader files have been updated in scenes.`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -21,6 +21,14 @@ Ren'Py (`.rpa`), RPG Maker, Electron (`app.asar`), LÖVE.
|
|
|
21
21
|
re_archive {file:"Game.pck"} → entries, sizes, types, Godot version, pack format
|
|
22
22
|
re_archive {file:"Game.exe"} → same, when the pack is embedded in the executable
|
|
23
23
|
re_archive {file, action:"extract", match:"*.gd"} → extract (MD5-checked) into the workspace or out
|
|
24
|
+
re_il2cpp {file:"…/Metadata/global-metadata.dat"} → Unity IL2CPP: assemblies, classes, method names, literals, C# skeleton
|
|
25
|
+
re_decode {path:"<extracted pack folder>"} → .gdc → .gd, .scn/.res → .tscn/.tres, project.binary → project.godot
|
|
26
|
+
re_archive {file:"data.win"} → GameMaker: game name/version, sprite/object/room names, textures + audio
|
|
27
|
+
re_archive {file:"archive.rpa"} → Ren'Py: scripts/images/audio (.rpyc = compiled AST, no source)
|
|
28
|
+
re_archive {file:"Master.bank"} / {file:"Init.bnk"} → FMOD events + FSB5 samples / Wwise media (.wem) + object counts
|
|
29
|
+
re_archive {file:"Game.pak"} → Unreal: entries, compression, stored/Zlib extract; encrypted = listed only
|
|
30
|
+
re_archive {file:"Build/game.data.br"} → Unity WebGL: data files, StreamingAssets, IL2CPP metadata; Emscripten .data via its loader .js
|
|
31
|
+
re_archive {file:"game.swf"} → Flash: stage, AS2/AS3, classes, symbols, images/sounds/ABC extract
|
|
24
32
|
re_binary {file:"<Game>_Data/Managed/Assembly-CSharp.dll"} → Unity Mono build: game classes and methods
|
|
25
33
|
re_binary {file:"Game.exe"} → arch, imports (graphics API, Steam, FMOD/Wwise…), hints
|
|
26
34
|
```
|
|
@@ -9,6 +9,8 @@ Use the port-project skill for the loop; this skill holds the Godot-specific jud
|
|
|
9
9
|
|
|
10
10
|
## Before planning
|
|
11
11
|
|
|
12
|
+
- Godot 3 source (`config_version=4` in project.godot)? Upgrade first: `port_upgrade {path, action:"apply"}`
|
|
13
|
+
writes a Godot 4 copy and lists what needs a human; fix those, `re_apicheck` the copy, then port from the copy.
|
|
12
14
|
- `re_scene {file:"project.godot"}` → main scene, autoloads (singletons), input actions, physics tick rate.
|
|
13
15
|
Autoloads become singletons (Unity: DontDestroyOnLoad MonoBehaviour; Unreal: GameInstanceSubsystem; web: module
|
|
14
16
|
singleton). Input actions must be recreated by name in the target.
|
|
@@ -11,21 +11,32 @@ port_verify {planId}
|
|
|
11
11
|
|
|
12
12
|
`passed:true` requires: every unit done or deliberately skipped, no missing output files, no source-engine API
|
|
13
13
|
left in ported code (`leftovers`), every source function and signal present in the port (`missingSymbols`; lifecycle
|
|
14
|
-
renames like `_ready` → `Awake` are understood),
|
|
14
|
+
renames like `_ready` → `Awake` are understood), no units marked partial, and no dormant logic (`dormant`): a name
|
|
15
|
+
that exists but has an empty or stub body, a function the source called that nothing in the port calls, a signal the
|
|
16
|
+
port only declares, or a suppression (`as any`, `@warning_ignore`, `#pragma warning disable`) the port added.
|
|
17
|
+
|
|
18
|
+
Lost your context? `port_verify` and `port_status` return `resume`: the plan, where it stands, the next unit with its
|
|
19
|
+
ported dependencies, and the open findings. Read it before continuing.
|
|
15
20
|
|
|
16
21
|
## Fix loop
|
|
17
22
|
|
|
18
|
-
| Finding | Fix
|
|
19
|
-
| ------------------------------------------- |
|
|
20
|
-
| `not ported yet` | `port_unit {planId, unit}` and port it, or `port_write {status:"skipped", notes}` with a reason the user accepts.
|
|
21
|
-
| `source-engine API left in the port` | Replace each listed pattern with the `use` it names, `port_write` again.
|
|
22
|
-
| `functions/signals not found` | Port them, or rename to the matching idiom. If the function truly became unnecessary, say why in notes.
|
|
23
|
-
| `lifecycle callbacks without a counterpart` | Fine when the work moved (e.g. connections replaced by OnTriggerEnter) — check it did.
|
|
24
|
-
| `marked partial` | Finish it or explain what is missing and who needs to do it.
|
|
25
|
-
| `PORT:` markers | Each is an open item: resolve it or list it in the hand-over.
|
|
23
|
+
| Finding | Fix |
|
|
24
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `not ported yet` | `port_unit {planId, unit}` and port it, or `port_write {status:"skipped", notes}` with a reason the user accepts. |
|
|
26
|
+
| `source-engine API left in the port` | Replace each listed pattern with the `use` it names, `port_write` again. |
|
|
27
|
+
| `functions/signals not found` | Port them, or rename to the matching idiom. If the function truly became unnecessary, say why in notes. |
|
|
28
|
+
| `lifecycle callbacks without a counterpart` | Fine when the work moved (e.g. connections replaced by OnTriggerEnter) — check it did. |
|
|
29
|
+
| `marked partial` | Finish it or explain what is missing and who needs to do it. |
|
|
30
|
+
| `PORT:` markers | Each is an open item: resolve it or list it in the hand-over. |
|
|
31
|
+
| `behaviour parity` | `tick-moved`: put motion back in the physics callback. `delta-dropped/added`: match the source's delta use. `seed-missing`: seed the same way. `order-dropped`: map execution order/deferral. `timescale-dropped`: set the equivalent setting. Prove it with `port_trace`. |
|
|
32
|
+
| `dormant logic` | `empty-body`/`thin-body`: port the statements. `unwired`: call or connect it where the source did. `suppression`: remove it and port the real fix. |
|
|
26
33
|
|
|
27
34
|
Run `port_verify` again until it passes (or the remaining items are explained to the user).
|
|
28
35
|
|
|
36
|
+
Godot output: also `re_apicheck {path: <output folder>, engine: "godot@4.x"}`. Every `renamed-*` is a Godot 3 name
|
|
37
|
+
(replace with the given Godot 4 name), every `unknown-*` an invented or misspelled API (use the suggestion or the
|
|
38
|
+
real API), `too-new` a class the target version does not have yet.
|
|
39
|
+
|
|
29
40
|
## Hand-over (short and factual)
|
|
30
41
|
|
|
31
42
|
- Where the output is and how to open/run it in the target engine.
|
|
@@ -34,7 +34,7 @@ original task after repair. Client approval, shell PATH, registration and engine
|
|
|
34
34
|
availability have different remedies; avoid repeating setup for all of them.
|
|
35
35
|
|
|
36
36
|
Prefer an existing `reverseport` CLI. When none is installed and ReversePort package acquisition
|
|
37
|
-
is within the authorized scope, use `npx -y reverseport@1.
|
|
37
|
+
is within the authorized scope, use `npx -y reverseport@1.8.0` in its place.
|
|
38
38
|
Configuration repair starts with a read-only setup plan; show paths, backups and
|
|
39
39
|
skill replacement before applying authorized changes. Preserve existing approval
|
|
40
40
|
and target-execution boundaries. ReversePort setup never installs or upgrades unrelated
|
|
@@ -49,6 +49,21 @@ These tools run without an analysis engine or provider:
|
|
|
49
49
|
- Quick look at an executable, pack or unknown file: `re_binary`, `re_archive`
|
|
50
50
|
(Godot `.pck` also inside an `.exe`, zip/apk/jar, tar.gz, asar), `re_probe`,
|
|
51
51
|
`re_extract`. Two builds: `re_compare`. Write-up: `re_report`.
|
|
52
|
+
- Unity project with pink materials, "missing script" components or GUID
|
|
53
|
+
trouble after moving files: `re_refs` (reports only; fix in Unity).
|
|
54
|
+
- Localisation: `re_strings` extracts user-facing text (Godot, Unity, RPG
|
|
55
|
+
Maker, Ren'Py, i18n JSON) to .pot/.csv with stable ids; `action:"apply"`
|
|
56
|
+
writes a translated copy and checks placeholders. Never edit strings by hand.
|
|
57
|
+
- Godot code you or a model wrote: `re_apicheck {path, engine:"godot@4.3"}`
|
|
58
|
+
before opening Godot. Fix every renamed-* with its replacement and every
|
|
59
|
+
unknown-* with a real API; never invent members.
|
|
60
|
+
- Godot 3 project → Godot 4: `port_upgrade {path, action:"apply"}` writes a
|
|
61
|
+
converted copy and lists what needs a human (setget, Tween nodes,
|
|
62
|
+
move_and_slide arguments…); fix those, then `re_apicheck` on the copy.
|
|
63
|
+
- Save files: `re_save {file, compare}` peels gzip/base64/LZ-String/JSON/
|
|
64
|
+
INI/store_var, names the fields by role, verifies checksums and diffs two
|
|
65
|
+
saves; `action:"schema"` writes SaveData.gd/.cs to port the format.
|
|
66
|
+
Encrypted or obfuscated saves are reported, never decoded.
|
|
52
67
|
- No files, only a name, store page, website or public repository: `re_lookup`,
|
|
53
68
|
then `re_blueprint` for a clean-room rebuild spec measured against the original.
|
|
54
69
|
- Port between engines or languages: `port_map` → `port_plan` → `port_unit` /
|
|
@@ -67,7 +82,8 @@ explicit path or endpoint and do not need it.
|
|
|
67
82
|
|
|
68
83
|
- ASAR or extracted JavaScript/Electron tree:
|
|
69
84
|
`analyze_javascript_application`.
|
|
70
|
-
- Archive or pack members (zip/apk/jar/asar, Godot .pck
|
|
85
|
+
- Archive or pack members (zip/apk/jar/asar, Godot .pck, Ren'Py .rpa, GameMaker
|
|
86
|
+
data.win, FMOD/Wwise banks, Unreal .pak, Flash .swf): `re_archive` to list
|
|
71
87
|
or extract. IPA/MSIX/AppX/DMG, or when you need Evidence IDs and integrity
|
|
72
88
|
findings: `open_binary` with the local path, then `inspect_artifact`.
|
|
73
89
|
- Android APK code, classes, methods, or incoming references:
|
|
@@ -32,11 +32,26 @@ routes in the main skill and cite their Evidence.
|
|
|
32
32
|
|
|
33
33
|
- Godot packs can sit inside the `.exe`: `re_archive {file}` lists them; add
|
|
34
34
|
`action: "extract", match: "*.gd"` to extract. Encrypted packs are reported,
|
|
35
|
-
never opened.
|
|
35
|
+
never opened. Exported packs hold .gdc, binary .scn/.res and project.binary:
|
|
36
|
+
`re_decode {path: <extracted folder>}` writes an editable project back.
|
|
36
37
|
- `re_binary` reads PE (with .NET types and methods), ELF, Mach-O, wasm, Java
|
|
37
38
|
class and dex files; `Assembly-CSharp.dll` shows a Unity game's scripts.
|
|
38
39
|
- `re_compare {a, b}` diffs folders, packs or binaries. Report changes grouped
|
|
39
40
|
(scripts, scenes, textures, audio, engine binary), biggest first.
|
|
41
|
+
- `re_refs {path}` audits a Unity project's GUID references: dangling (missing
|
|
42
|
+
scripts, pink materials), duplicate `.meta` GUIDs, assets without `.meta`,
|
|
43
|
+
`.meta` without asset. Run it before `port_plan` from Unity: a dangling
|
|
44
|
+
reference is a component the port would silently lose.
|
|
45
|
+
|
|
46
|
+
## Save files
|
|
47
|
+
|
|
48
|
+
`re_save {file}` lists the container layers (gzip, base64, LZ-String, JSON,
|
|
49
|
+
XML, INI, Godot store_var), every field with a role guess, and whether a
|
|
50
|
+
checksum field is a plain hash of the rest. `compare` with a second save shows
|
|
51
|
+
which fields the action in between changed. `action: "schema"` writes a JSON
|
|
52
|
+
schema and SaveData.gd/.cs with the same field names so a port keeps loading
|
|
53
|
+
old saves; reproduce the layer order when writing. Encrypted, XOR-obfuscated,
|
|
54
|
+
BinaryFormatter and pickle saves are reported, not decoded.
|
|
40
55
|
|
|
41
56
|
## Recon without files
|
|
42
57
|
|
|
@@ -75,15 +90,31 @@ Unity → Unreal, or any language.
|
|
|
75
90
|
PORT-PLAN.md. `port_copy_assets {planId}` copies the assets.
|
|
76
91
|
3. Loop in dependency order: `port_unit {planId}` returns the source, API hints
|
|
77
92
|
with line numbers, conventions and the ported names of dependencies.
|
|
78
|
-
Scenes
|
|
93
|
+
Scenes, uGUI, prefab variants, `.anim` and `.controller`: `port_scene {planId, unit}`. Code: write the port, then
|
|
79
94
|
`port_write {planId, unit, files}` and fix every leftover and
|
|
80
95
|
missing function it reports before moving on.
|
|
96
|
+
Shaders: `port_shader {file, to}` gives a marked first draft (uniforms,
|
|
97
|
+
intrinsics, built-ins); resolve its leftovers and `PORT:` markers, then
|
|
98
|
+
`port_write` it as the unit's file.
|
|
81
99
|
4. Translate behaviour, not syntax: events instead of signals, serialized
|
|
82
100
|
references instead of node paths, timers or coroutines instead of awaits.
|
|
83
101
|
Convert every position, direction, rotation, speed and distance with
|
|
84
102
|
`port_convert` (Unreal: centimeters, Z-up; Unity mirrors Z; three.js matches
|
|
85
|
-
Godot).
|
|
86
|
-
|
|
103
|
+
Godot). Physics: `port_convert {from, to, physics: {gravity, fixedStep,
|
|
104
|
+
linearDamp, friction, bounce, jumpVelocity, layers}}` gives the target
|
|
105
|
+
settings, converted numbers and the rules that differ (fixed step, damping
|
|
106
|
+
defaults, friction/bounce combination, per-body masks vs a matrix). Leave
|
|
107
|
+
`PORT:` markers instead of silent gaps.
|
|
108
|
+
5. `port_verify {planId}` until it passes; `port_status` shows progress. Both
|
|
109
|
+
return `resume`: read it first after a context reset. Verify also flags
|
|
110
|
+
dormant logic (empty or thin bodies, unwired functions and signals, added
|
|
111
|
+
suppressions) and behaviour parity (motion moved between frame and physics
|
|
112
|
+
tick, delta dropped, RNG unseeded, execution order lost). To prove behaviour:
|
|
113
|
+
`port_trace {action:"harness", engine}` in both projects, then
|
|
114
|
+
`port_trace {action:"compare", a, b}` shows the first divergence per
|
|
115
|
+
property and whether the error grows. Godot targets: also
|
|
116
|
+
`re_apicheck {path: <output>, engine}` so no Godot 3 names or invented
|
|
117
|
+
members survive.
|
|
87
118
|
6. Tell the user how to open the result. Unity: copy `Assets/` in, then Tools ▸
|
|
88
119
|
ReversePort ▸ Build prefabs and scenes from specs. Godot: open the folder.
|
|
89
120
|
three.js: `npm install && npm run dev`. Unreal: Generate project files, build.
|
|
@@ -11,7 +11,8 @@ Use the port-project skill for the loop; this skill holds the Unity-specific jud
|
|
|
11
11
|
|
|
12
12
|
- Scenes and prefabs must be text: Unity → Project Settings → Editor → Asset Serialization = **Force Text**. Without
|
|
13
13
|
it `re_scene` cannot read them.
|
|
14
|
-
- Keep the `.meta` files: they map GUIDs to scripts and prefabs.
|
|
14
|
+
- Keep the `.meta` files: they map GUIDs to scripts and prefabs. Run `re_refs` on the project first: a dangling
|
|
15
|
+
GUID (missing script, pink material) is a component the port would silently lose; fix it in Unity before planning.
|
|
15
16
|
- GDScript or C#? Large, idiomatic C# codebases port faster to **godot-csharp** (only engine API calls change);
|
|
16
17
|
smaller or designer-facing projects read better as GDScript (`to:"godot"`).
|
|
17
18
|
|
|
@@ -21,10 +22,21 @@ Use the port-project skill for the loop; this skill holds the Unity-specific jud
|
|
|
21
22
|
AnimatableBody3D), CharacterController → CharacterBody3D, trigger colliders → Area3D, other colliders →
|
|
22
23
|
StaticBody3D, Box/Sphere/Capsule colliders → CollisionShape3D, built-in primitives → BoxMesh/SphereMesh/…, Camera,
|
|
23
24
|
Light, AudioSource, tags → groups, prefab instances → instanced scenes, scripts → the ported script. Transforms are
|
|
24
|
-
mirrored on Z.
|
|
25
|
+
mirrored on Z. Prefab variants become inherited scenes (`instance=`) with `m_IsActive` applied and other
|
|
26
|
+
overrides listed. uGUI becomes Control nodes: Canvas → full-rect Control, Image/RawImage → TextureRect, Text and
|
|
27
|
+
TextMeshPro → Label (size, colour, alignment), Button → Button (label text, OnClick listed), Slider/Toggle/
|
|
28
|
+
ScrollRect/InputField/layout groups → HSlider/CheckBox/ScrollContainer/LineEdit/Box- and GridContainer, with
|
|
29
|
+
RectTransform anchors and offsets converted (Y flipped). Animator → an `AnimationPlayer` whose library holds the
|
|
30
|
+
ported clips plus an `Animator` child running the ported controller script. `.anim` files port to Godot
|
|
31
|
+
`Animation` resources (position/rotation/scale/property/sprite-swap/event tracks); `.controller` files port to a
|
|
32
|
+
GDScript state machine with the same states, parameters and transition conditions (blend trees and extra layers
|
|
33
|
+
are listed for an AnimationTree by hand). Model meshes and materials stay flagged as `metadata/reverseport_todo`.
|
|
25
34
|
|
|
26
35
|
## Code: the usual translations
|
|
27
36
|
|
|
37
|
+
After writing each script, `re_apicheck {path, engine: "godot@4.x"}`: models mix Godot 3 (`KinematicBody2D`,
|
|
38
|
+
`instance()`, `yield`, string `connect`) into Godot 4 code and invent members; the tool names the replacement.
|
|
39
|
+
|
|
28
40
|
- One GameObject with several MonoBehaviours → one node per behaviour (child nodes) or merge them into one script.
|
|
29
41
|
- `GetComponent<T>()` → the node itself or a child (`$Child`); components are nodes in Godot.
|
|
30
42
|
- `[SerializeField]` → `@export`; prefab fields (`GameObject`) → `PackedScene`.
|