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,416 @@
|
|
|
1
|
+
// port_rpgmaker: an RPG Maker MV/MZ project (data/*.json + img/ + audio/) → a runnable Godot 4 project.
|
|
2
|
+
// Maps become scenes whose tile layers are drawn from the original tilesets (B–E and A5 exactly, autotiles with
|
|
3
|
+
// their interior piece), events become nodes with their pages kept as data, and a small interpreter runs the most
|
|
4
|
+
// used event commands (text, choices, switches, variables, conditional branches, transfer, wait, self switches,
|
|
5
|
+
// common events). The database (actors, items, system …) is loaded by an autoload. Everything the interpreter does
|
|
6
|
+
// not run is listed per command code, so the remaining work is explicit. The source is only read.
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { RPError, requireDir, walk, limit } from './util.mjs';
|
|
10
|
+
import { workspaceDir, isInside } from './workspace.mjs';
|
|
11
|
+
|
|
12
|
+
const SUPPORTED = new Map([[0, 'end'], [101, 'Show Text'], [401, 'text line'], [102, 'Show Choices'], [402, 'When [choice]'], [403, 'When Cancel'], [404, 'end choices'], [108, 'Comment'], [408, 'comment line'], [111, 'Conditional Branch (switch/variable/self switch)'], [411, 'Else'], [412, 'end branch'], [117, 'Common Event'], [121, 'Control Switches'], [122, 'Control Variables (constant/variable/random)'], [123, 'Control Self Switch'], [201, 'Transfer Player'], [230, 'Wait'], [250, 'Play SE'], [241, 'Play BGM'], [115, 'Exit Event Processing'], [112, 'Loop'], [413, 'Repeat Above'], [113, 'Break Loop']]);
|
|
13
|
+
const NAMES = { 103: 'Input Number', 104: 'Select Item', 105: 'Show Scrolling Text', 118: 'Label', 119: 'Jump to Label', 124: 'Control Timer', 125: 'Change Gold', 126: 'Change Items', 127: 'Change Weapons', 128: 'Change Armors', 129: 'Change Party Member', 132: 'Change Battle BGM', 133: 'Change Victory ME', 134: 'Change Save Access', 135: 'Change Menu Access', 136: 'Change Encounter', 137: 'Change Formation Access', 138: 'Change Window Color', 202: 'Set Vehicle Location', 203: 'Set Event Location', 204: 'Scroll Map', 205: 'Set Movement Route', 206: 'Get on/off Vehicle', 211: 'Change Transparency', 212: 'Show Animation', 213: 'Show Balloon Icon', 214: 'Erase Event', 216: 'Change Player Followers', 217: 'Gather Followers', 221: 'Fadeout Screen', 222: 'Fadein Screen', 223: 'Tint Screen', 224: 'Flash Screen', 225: 'Shake Screen', 231: 'Show Picture', 232: 'Move Picture', 233: 'Rotate Picture', 234: 'Tint Picture', 235: 'Erase Picture', 236: 'Set Weather Effect', 242: 'Fadeout BGM', 245: 'Play BGS', 249: 'Play ME', 261: 'Play Movie', 281: 'Change Map Name Display', 282: 'Change Tileset', 283: 'Change Battle Back', 284: 'Change Parallax', 285: 'Get Location Info', 301: 'Battle Processing', 302: 'Shop Processing', 303: 'Name Input Processing', 311: 'Change HP', 312: 'Change MP', 313: 'Change State', 314: 'Recover All', 315: 'Change EXP', 316: 'Change Level', 317: 'Change Parameter', 318: 'Change Skill', 319: 'Change Equipment', 320: 'Change Name', 321: 'Change Class', 322: 'Change Actor Images', 323: 'Change Vehicle Image', 324: 'Change Nickname', 325: 'Change Profile', 351: 'Open Menu Screen', 352: 'Open Save Screen', 353: 'Game Over', 354: 'Return to Title Screen', 355: 'Script', 655: 'script line', 356: 'Plugin Command (MV)', 357: 'Plugin Command (MZ)', 657: 'plugin args', 505: 'move route step', 601: 'If Win', 602: 'If Escape', 603: 'If Lose', 604: 'end battle branch' };
|
|
14
|
+
|
|
15
|
+
const RUNTIME = {
|
|
16
|
+
'rpg/rpg_database.gd': `extends Node
|
|
17
|
+
## Autoload "RpgDb": the RPG Maker database (data/*.json) plus game state (switches, variables, self switches).
|
|
18
|
+
## Ported by ReversePort. JSON files are the original data, loaded as-is.
|
|
19
|
+
|
|
20
|
+
signal message(text: String, face: String)
|
|
21
|
+
signal choices_requested(options: PackedStringArray)
|
|
22
|
+
|
|
23
|
+
var system: Dictionary = {}
|
|
24
|
+
var map_infos: Array = []
|
|
25
|
+
var common_events: Array = []
|
|
26
|
+
var tilesets: Array = []
|
|
27
|
+
var actors: Array = []
|
|
28
|
+
var items: Array = []
|
|
29
|
+
var switches: Dictionary = {}
|
|
30
|
+
var variables: Dictionary = {}
|
|
31
|
+
var self_switches: Dictionary = {}
|
|
32
|
+
var choice_result := -1
|
|
33
|
+
|
|
34
|
+
func _ready() -> void:
|
|
35
|
+
\tsystem = _load("System")
|
|
36
|
+
\tmap_infos = _load("MapInfos")
|
|
37
|
+
\tcommon_events = _load("CommonEvents")
|
|
38
|
+
\ttilesets = _load("Tilesets")
|
|
39
|
+
\tactors = _load("Actors")
|
|
40
|
+
\titems = _load("Items")
|
|
41
|
+
|
|
42
|
+
func _load(name: String):
|
|
43
|
+
\tvar path := "res://data/%s.json" % name
|
|
44
|
+
\tif not FileAccess.file_exists(path):
|
|
45
|
+
\t\treturn [] if name != "System" else {}
|
|
46
|
+
\treturn JSON.parse_string(FileAccess.get_file_as_string(path))
|
|
47
|
+
|
|
48
|
+
func load_map(id: int) -> Dictionary:
|
|
49
|
+
\treturn _load("Map%03d" % id)
|
|
50
|
+
|
|
51
|
+
func map_scene(id: int) -> String:
|
|
52
|
+
\treturn "res://maps/map_%03d.tscn" % id
|
|
53
|
+
|
|
54
|
+
func get_switch(id: int) -> bool:
|
|
55
|
+
\treturn switches.get(id, false)
|
|
56
|
+
|
|
57
|
+
func set_switch(id: int, value: bool) -> void:
|
|
58
|
+
\tswitches[id] = value
|
|
59
|
+
|
|
60
|
+
func get_variable(id: int) -> int:
|
|
61
|
+
\treturn variables.get(id, 0)
|
|
62
|
+
|
|
63
|
+
func set_variable(id: int, value: int) -> void:
|
|
64
|
+
\tvariables[id] = value
|
|
65
|
+
|
|
66
|
+
func get_self_switch(map_id: int, event_id: int, letter: String) -> bool:
|
|
67
|
+
\treturn self_switches.get("%d,%d,%s" % [map_id, event_id, letter], false)
|
|
68
|
+
|
|
69
|
+
func set_self_switch(map_id: int, event_id: int, letter: String, value: bool) -> void:
|
|
70
|
+
\tself_switches["%d,%d,%s" % [map_id, event_id, letter]] = value
|
|
71
|
+
`,
|
|
72
|
+
'rpg/rpg_map.gd': `extends Node2D
|
|
73
|
+
## A ported RPG Maker map: draws its tile layers from the original tileset images.
|
|
74
|
+
## B–E and A5 tiles are drawn exactly; autotiles (A1–A4) are drawn with their interior piece (edges approximated).
|
|
75
|
+
|
|
76
|
+
@export var map_id := 1
|
|
77
|
+
@export var tile_size := 48
|
|
78
|
+
var data: Dictionary = {}
|
|
79
|
+
var sheets: Array[Texture2D] = []
|
|
80
|
+
|
|
81
|
+
func _ready() -> void:
|
|
82
|
+
\tdata = RpgDb.load_map(map_id)
|
|
83
|
+
\tvar ts_id: int = data.get("tilesetId", 1)
|
|
84
|
+
\tvar ts = RpgDb.tilesets[ts_id] if ts_id < RpgDb.tilesets.size() else null
|
|
85
|
+
\tsheets.clear()
|
|
86
|
+
\tif ts:
|
|
87
|
+
\t\tfor name in ts["tilesetNames"]:
|
|
88
|
+
\t\t\tvar p := "res://img/tilesets/%s.png" % name
|
|
89
|
+
\t\t\tsheets.append(load(p) if name != "" and ResourceLoader.exists(p) else null)
|
|
90
|
+
\tqueue_redraw()
|
|
91
|
+
|
|
92
|
+
func _draw() -> void:
|
|
93
|
+
\tif data.is_empty():
|
|
94
|
+
\t\treturn
|
|
95
|
+
\tvar w: int = data["width"]
|
|
96
|
+
\tvar h: int = data["height"]
|
|
97
|
+
\tvar tiles: Array = data["data"]
|
|
98
|
+
\tfor z in range(4):
|
|
99
|
+
\t\tfor y in range(h):
|
|
100
|
+
\t\t\tfor x in range(w):
|
|
101
|
+
\t\t\t\tvar id: int = tiles[(z * h + y) * w + x]
|
|
102
|
+
\t\t\t\tif id > 0:
|
|
103
|
+
\t\t\t\t\t_draw_tile(id, Vector2(x * tile_size, y * tile_size))
|
|
104
|
+
|
|
105
|
+
func _draw_tile(id: int, at: Vector2) -> void:
|
|
106
|
+
\tvar s := tile_size
|
|
107
|
+
\tif id < 2048:
|
|
108
|
+
\t\tvar set_number := 4 if id >= 1536 else 5 + id / 256
|
|
109
|
+
\t\tvar tex: Texture2D = sheets[set_number] if set_number < sheets.size() else null
|
|
110
|
+
\t\tif tex == null:
|
|
111
|
+
\t\t\treturn
|
|
112
|
+
\t\tvar sx := ((id / 128) % 2 * 8 + id % 8) * s
|
|
113
|
+
\t\tvar sy := ((id % 256) / 8 % 16) * s
|
|
114
|
+
\t\tdraw_texture_rect_region(tex, Rect2(at, Vector2(s, s)), Rect2(sx, sy, s, s))
|
|
115
|
+
\t\treturn
|
|
116
|
+
\tvar kind := (id - 2048) / 48
|
|
117
|
+
\tvar set_number := 0 if id < 2816 else 1 if id < 4352 else 2 if id < 5888 else 3
|
|
118
|
+
\tvar tex2: Texture2D = sheets[set_number] if set_number < sheets.size() else null
|
|
119
|
+
\tif tex2 == null:
|
|
120
|
+
\t\treturn
|
|
121
|
+
\tvar tx := kind % 8
|
|
122
|
+
\tvar ty := kind / 8
|
|
123
|
+
\tvar bx := 0
|
|
124
|
+
\tvar by := 0
|
|
125
|
+
\tif set_number == 0:
|
|
126
|
+
\t\tbx = [0, 0, 6, 6][kind] if kind < 4 else (tx / 4) * 8 + (2 if kind % 2 == 1 else 0)
|
|
127
|
+
\t\tby = [0, 3, 0, 3][kind] if kind < 4 else ty * 6 + (tx / 2) % 2 * 3
|
|
128
|
+
\telif set_number == 1:
|
|
129
|
+
\t\tbx = tx * 2
|
|
130
|
+
\t\tby = (ty - 2) * 3
|
|
131
|
+
\telif set_number == 2:
|
|
132
|
+
\t\tbx = tx * 2
|
|
133
|
+
\t\tby = (ty - 6) * 2
|
|
134
|
+
\telse:
|
|
135
|
+
\t\tbx = tx * 2
|
|
136
|
+
\t\tby = int(floor((ty - 10) * 2.5 + (0.5 if ty % 2 == 1 else 0.0)))
|
|
137
|
+
\t# Interior piece: the four inner quarters of the autotile block.
|
|
138
|
+
\tvar half := s / 2
|
|
139
|
+
\tvar quarters := [[2, 4], [1, 4], [2, 3], [1, 3]]
|
|
140
|
+
\tfor i in range(4):
|
|
141
|
+
\t\tvar q: Array = quarters[i]
|
|
142
|
+
\t\tvar src := Rect2((bx * 2 + q[0]) * half, (by * 2 + q[1]) * half, half, half)
|
|
143
|
+
\t\tvar dst := Rect2(at + Vector2((i % 2) * half, (i / 2) * half), Vector2(half, half))
|
|
144
|
+
\t\tdraw_texture_rect_region(tex2, dst, src)
|
|
145
|
+
`,
|
|
146
|
+
'rpg/rpg_event.gd': `extends Node2D
|
|
147
|
+
## A ported RPG Maker event: pages, conditions and a list interpreter for the common commands.
|
|
148
|
+
## Commands the interpreter does not run are reported once with push_warning (code and name), see PORT-NOTES.md.
|
|
149
|
+
|
|
150
|
+
@export var map_id := 1
|
|
151
|
+
@export var event_id := 1
|
|
152
|
+
@export var trigger_override := -1
|
|
153
|
+
var pages: Array = []
|
|
154
|
+
var running := false
|
|
155
|
+
|
|
156
|
+
func _ready() -> void:
|
|
157
|
+
\tvar map := RpgDb.load_map(map_id)
|
|
158
|
+
\tvar ev = map["events"][event_id] if event_id < map["events"].size() else null
|
|
159
|
+
\tif ev:
|
|
160
|
+
\t\tpages = ev["pages"]
|
|
161
|
+
\tvar page: Variant = active_page()
|
|
162
|
+
\tif page != null and int(page["trigger"]) == 3:
|
|
163
|
+
\t\trun()
|
|
164
|
+
|
|
165
|
+
func active_page() -> Variant:
|
|
166
|
+
\tfor i in range(pages.size() - 1, -1, -1):
|
|
167
|
+
\t\tvar c: Dictionary = pages[i]["conditions"]
|
|
168
|
+
\t\tif c["switch1Valid"] and not RpgDb.get_switch(c["switch1Id"]):
|
|
169
|
+
\t\t\tcontinue
|
|
170
|
+
\t\tif c["switch2Valid"] and not RpgDb.get_switch(c["switch2Id"]):
|
|
171
|
+
\t\t\tcontinue
|
|
172
|
+
\t\tif c["variableValid"] and RpgDb.get_variable(c["variableId"]) < int(c["variableValue"]):
|
|
173
|
+
\t\t\tcontinue
|
|
174
|
+
\t\tif c["selfSwitchValid"] and not RpgDb.get_self_switch(map_id, event_id, c["selfSwitchCh"]):
|
|
175
|
+
\t\t\tcontinue
|
|
176
|
+
\t\treturn pages[i]
|
|
177
|
+
\treturn null
|
|
178
|
+
|
|
179
|
+
func run() -> void:
|
|
180
|
+
\tif running:
|
|
181
|
+
\t\treturn
|
|
182
|
+
\tvar page: Variant = active_page()
|
|
183
|
+
\tif page == null:
|
|
184
|
+
\t\treturn
|
|
185
|
+
\trunning = true
|
|
186
|
+
\tawait run_list(page["list"])
|
|
187
|
+
\trunning = false
|
|
188
|
+
|
|
189
|
+
func run_list(list: Array) -> void:
|
|
190
|
+
\tvar i := 0
|
|
191
|
+
\twhile i < list.size():
|
|
192
|
+
\t\tvar cmd: Dictionary = list[i]
|
|
193
|
+
\t\tvar code: int = cmd["code"]
|
|
194
|
+
\t\tvar p: Array = cmd["parameters"]
|
|
195
|
+
\t\tmatch code:
|
|
196
|
+
\t\t\t0, 108, 408, 412, 404:
|
|
197
|
+
\t\t\t\tpass
|
|
198
|
+
\t\t\t101:
|
|
199
|
+
\t\t\t\tvar text := PackedStringArray()
|
|
200
|
+
\t\t\t\twhile i + 1 < list.size() and int(list[i + 1]["code"]) == 401:
|
|
201
|
+
\t\t\t\t\ti += 1
|
|
202
|
+
\t\t\t\t\ttext.append(list[i]["parameters"][0])
|
|
203
|
+
\t\t\t\tRpgDb.message.emit("\\n".join(text), str(p[0]))
|
|
204
|
+
\t\t\t\tawait get_tree().process_frame
|
|
205
|
+
\t\t\t102:
|
|
206
|
+
\t\t\t\tRpgDb.choice_result = -1
|
|
207
|
+
\t\t\t\tRpgDb.choices_requested.emit(PackedStringArray(p[0]))
|
|
208
|
+
\t\t\t\twhile RpgDb.choice_result < 0:
|
|
209
|
+
\t\t\t\t\tawait get_tree().process_frame
|
|
210
|
+
\t\t\t\ti = _skip_to_choice(list, i, RpgDb.choice_result, cmd["indent"])
|
|
211
|
+
\t\t\t\tcontinue
|
|
212
|
+
\t\t\t402, 403:
|
|
213
|
+
\t\t\t\ti = _skip_block(list, i, cmd["indent"], [404])
|
|
214
|
+
\t\t\t\tcontinue
|
|
215
|
+
\t\t\t111:
|
|
216
|
+
\t\t\t\tif not _condition(p):
|
|
217
|
+
\t\t\t\t\ti = _skip_block(list, i, cmd["indent"], [411, 412]) + 1
|
|
218
|
+
\t\t\t\t\tcontinue
|
|
219
|
+
\t\t\t411:
|
|
220
|
+
\t\t\t\ti = _skip_block(list, i, cmd["indent"], [412])
|
|
221
|
+
\t\t\t\tcontinue
|
|
222
|
+
\t\t\t112, 413:
|
|
223
|
+
\t\t\t\tif code == 413:
|
|
224
|
+
\t\t\t\t\ti = _loop_start(list, i, cmd["indent"])
|
|
225
|
+
\t\t\t\t\tcontinue
|
|
226
|
+
\t\t\t113:
|
|
227
|
+
\t\t\t\ti = _skip_block(list, _loop_end(list, i, cmd["indent"]), cmd["indent"] - 1, [413]) + 1
|
|
228
|
+
\t\t\t\tcontinue
|
|
229
|
+
\t\t\t115:
|
|
230
|
+
\t\t\t\treturn
|
|
231
|
+
\t\t\t117:
|
|
232
|
+
\t\t\t\tvar ce = RpgDb.common_events[p[0]] if int(p[0]) < RpgDb.common_events.size() else null
|
|
233
|
+
\t\t\t\tif ce:
|
|
234
|
+
\t\t\t\t\tawait run_list(ce["list"])
|
|
235
|
+
\t\t\t121:
|
|
236
|
+
\t\t\t\tfor id in range(int(p[0]), int(p[1]) + 1):
|
|
237
|
+
\t\t\t\t\tRpgDb.set_switch(id, int(p[2]) == 0)
|
|
238
|
+
\t\t\t122:
|
|
239
|
+
\t\t\t\tvar value := 0
|
|
240
|
+
\t\t\t\tmatch int(p[3]):
|
|
241
|
+
\t\t\t\t\t0: value = int(p[4])
|
|
242
|
+
\t\t\t\t\t1: value = RpgDb.get_variable(int(p[4]))
|
|
243
|
+
\t\t\t\t\t2: value = randi_range(int(p[4]), int(p[5]))
|
|
244
|
+
\t\t\t\t\t_: push_warning("PORT: Control Variables operand %d" % int(p[3]))
|
|
245
|
+
\t\t\t\tfor id in range(int(p[0]), int(p[1]) + 1):
|
|
246
|
+
\t\t\t\t\tvar old := RpgDb.get_variable(id)
|
|
247
|
+
\t\t\t\t\tvar ops := [value, old + value, old - value, old * value, old / value if value != 0 else old, old % value if value != 0 else old]
|
|
248
|
+
\t\t\t\t\tRpgDb.set_variable(id, ops[int(p[2])])
|
|
249
|
+
\t\t\t123:
|
|
250
|
+
\t\t\t\tRpgDb.set_self_switch(map_id, event_id, p[0], int(p[1]) == 0)
|
|
251
|
+
\t\t\t201:
|
|
252
|
+
\t\t\t\tif int(p[0]) == 0:
|
|
253
|
+
\t\t\t\t\tget_tree().change_scene_to_file(RpgDb.map_scene(int(p[1])))
|
|
254
|
+
\t\t\t\t\treturn
|
|
255
|
+
\t\t\t\tpush_warning("PORT: Transfer Player with variables")
|
|
256
|
+
\t\t\t230:
|
|
257
|
+
\t\t\t\tawait get_tree().create_timer(int(p[0]) / 60.0).timeout
|
|
258
|
+
\t\t\t241, 250:
|
|
259
|
+
\t\t\t\tvar folder := "bgm" if code == 241 else "se"
|
|
260
|
+
\t\t\t\tvar base := "res://audio/%s/%s" % [folder, p[0]["name"]]
|
|
261
|
+
\t\t\t\tfor ext in [".ogg", ".m4a", ".wav"]:
|
|
262
|
+
\t\t\t\t\tif ResourceLoader.exists(base + ext):
|
|
263
|
+
\t\t\t\t\t\tvar player := AudioStreamPlayer.new()
|
|
264
|
+
\t\t\t\t\t\tplayer.stream = load(base + ext)
|
|
265
|
+
\t\t\t\t\t\tadd_child(player)
|
|
266
|
+
\t\t\t\t\t\tplayer.play()
|
|
267
|
+
\t\t\t\t\t\tbreak
|
|
268
|
+
\t\t\t_:
|
|
269
|
+
\t\t\t\tpush_warning("PORT: event command %d not ported (map %d, event %d)" % [code, map_id, event_id])
|
|
270
|
+
\t\ti += 1
|
|
271
|
+
|
|
272
|
+
func _condition(p: Array) -> bool:
|
|
273
|
+
\tmatch int(p[0]):
|
|
274
|
+
\t\t0: return RpgDb.get_switch(int(p[1])) == (int(p[2]) == 0)
|
|
275
|
+
\t\t1:
|
|
276
|
+
\t\t\tvar a := RpgDb.get_variable(int(p[1]))
|
|
277
|
+
\t\t\tvar b := int(p[3]) if int(p[2]) == 0 else RpgDb.get_variable(int(p[3]))
|
|
278
|
+
\t\t\treturn [a == b, a >= b, a <= b, a > b, a < b, a != b][int(p[4])]
|
|
279
|
+
\t\t2: return RpgDb.get_self_switch(map_id, event_id, p[1]) == (int(p[2]) == 0)
|
|
280
|
+
\tpush_warning("PORT: Conditional Branch type %d" % int(p[0]))
|
|
281
|
+
\treturn false
|
|
282
|
+
|
|
283
|
+
func _skip_block(list: Array, i: int, indent: int, stop_codes: Array) -> int:
|
|
284
|
+
\tvar j := i + 1
|
|
285
|
+
\twhile j < list.size():
|
|
286
|
+
\t\tif int(list[j]["indent"]) == indent and stop_codes.has(int(list[j]["code"])):
|
|
287
|
+
\t\t\treturn j
|
|
288
|
+
\t\tj += 1
|
|
289
|
+
\treturn j
|
|
290
|
+
|
|
291
|
+
func _skip_to_choice(list: Array, i: int, choice: int, indent: int) -> int:
|
|
292
|
+
\tvar j := i + 1
|
|
293
|
+
\twhile j < list.size():
|
|
294
|
+
\t\tvar c: Dictionary = list[j]
|
|
295
|
+
\t\tif int(c["indent"]) == indent and int(c["code"]) == 402 and int(c["parameters"][0]) == choice:
|
|
296
|
+
\t\t\treturn j + 1
|
|
297
|
+
\t\tif int(c["indent"]) == indent and int(c["code"]) == 404:
|
|
298
|
+
\t\t\treturn j
|
|
299
|
+
\t\tj += 1
|
|
300
|
+
\treturn j
|
|
301
|
+
|
|
302
|
+
func _loop_start(list: Array, i: int, indent: int) -> int:
|
|
303
|
+
\tvar j := i - 1
|
|
304
|
+
\twhile j >= 0 and not (int(list[j]["code"]) == 112 and int(list[j]["indent"]) == indent):
|
|
305
|
+
\t\tj -= 1
|
|
306
|
+
\treturn j + 1
|
|
307
|
+
|
|
308
|
+
func _loop_end(list: Array, i: int, indent: int) -> int:
|
|
309
|
+
\tvar j := i
|
|
310
|
+
\twhile j < list.size() and not (int(list[j]["code"]) == 413):
|
|
311
|
+
\t\tj += 1
|
|
312
|
+
\treturn j
|
|
313
|
+
`,
|
|
314
|
+
'rpg/rpg_message_box.gd': `extends CanvasLayer
|
|
315
|
+
## Minimal message window and choice list for ported events (replace with your own UI).
|
|
316
|
+
|
|
317
|
+
var label: Label
|
|
318
|
+
var box: VBoxContainer
|
|
319
|
+
|
|
320
|
+
func _ready() -> void:
|
|
321
|
+
\tlabel = Label.new()
|
|
322
|
+
\tlabel.position = Vector2(24, 460)
|
|
323
|
+
\tlabel.visible = false
|
|
324
|
+
\tadd_child(label)
|
|
325
|
+
\tbox = VBoxContainer.new()
|
|
326
|
+
\tbox.position = Vector2(560, 300)
|
|
327
|
+
\tadd_child(box)
|
|
328
|
+
\tRpgDb.message.connect(_on_message)
|
|
329
|
+
\tRpgDb.choices_requested.connect(_on_choices)
|
|
330
|
+
|
|
331
|
+
func _on_message(text: String, _face: String) -> void:
|
|
332
|
+
\tlabel.text = text
|
|
333
|
+
\tlabel.visible = true
|
|
334
|
+
|
|
335
|
+
func _on_choices(options: PackedStringArray) -> void:
|
|
336
|
+
\tfor c in box.get_children():
|
|
337
|
+
\t\tc.queue_free()
|
|
338
|
+
\tfor i in range(options.size()):
|
|
339
|
+
\t\tvar b := Button.new()
|
|
340
|
+
\t\tb.text = options[i]
|
|
341
|
+
\t\tb.pressed.connect(func(): RpgDb.choice_result = i; for c in box.get_children(): c.queue_free())
|
|
342
|
+
\t\tbox.add_child(b)
|
|
343
|
+
`,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export async function portRpgMakerTool({ path: source, out, limit: max = 300 } = {}) {
|
|
347
|
+
const root = await requireDir(source, 'RPG Maker project');
|
|
348
|
+
const dataDir = (await fs.stat(path.join(root, 'data')).catch(() => null)) ? path.join(root, 'data') : (await fs.stat(path.join(root, 'www', 'data')).catch(() => null)) ? path.join(root, 'www', 'data') : null;
|
|
349
|
+
if (!dataDir) throw new RPError('NOT_RPGMAKER', 'No data/ folder with System.json found (RPG Maker MV/MZ project or www/ of a deployed game).');
|
|
350
|
+
const base = path.dirname(dataDir);
|
|
351
|
+
const readJson = async f => JSON.parse(await fs.readFile(path.join(dataDir, f), 'utf8'));
|
|
352
|
+
const system = await readJson('System.json').catch(() => { throw new RPError('NOT_RPGMAKER', 'data/System.json is missing or unreadable.'); });
|
|
353
|
+
const encrypted = !!(system.hasEncryptedImages || system.hasEncryptedAudio);
|
|
354
|
+
const mz = await fs.stat(path.join(base, 'js', 'rmmz_core.js')).then(() => true).catch(() => false);
|
|
355
|
+
const mapInfos = await readJson('MapInfos.json').catch(() => []);
|
|
356
|
+
const outDir = out ? path.resolve(out) : path.join(await workspaceDir('ports'), `${path.basename(root).replace(/[^A-Za-z0-9._-]+/g, '_')}-godot`);
|
|
357
|
+
if (isInside(outDir, root) || isInside(root, outDir)) throw new RPError('INVALID_ARGUMENT', 'out must be outside the source project.');
|
|
358
|
+
if ((await fs.readdir(outDir).catch(() => [])).length) throw new RPError('OUTPUT_NOT_EMPTY', `out is not empty: ${outDir}`);
|
|
359
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
360
|
+
|
|
361
|
+
// Copy data, images and audio as they are (encrypted .rpgmvp/.png_ files are copied but cannot be used).
|
|
362
|
+
const { files } = await walk(base, { maxFiles: 100000 });
|
|
363
|
+
let copied = 0;
|
|
364
|
+
for (const f of files) {
|
|
365
|
+
const rel = f.rel.split(path.sep).join('/');
|
|
366
|
+
if (!/^(data|img|audio|fonts|movies)\//.test(rel)) continue;
|
|
367
|
+
const dest = path.join(outDir, rel);
|
|
368
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
369
|
+
await fs.copyFile(f.abs, dest);
|
|
370
|
+
copied++;
|
|
371
|
+
}
|
|
372
|
+
for (const [rel, text] of Object.entries(RUNTIME)) { const d = path.join(outDir, rel); await fs.mkdir(path.dirname(d), { recursive: true }); await fs.writeFile(d, text); }
|
|
373
|
+
|
|
374
|
+
// Maps → scenes, and a census of event commands.
|
|
375
|
+
const tile = mz ? (system.tileSize ?? 48) : 48;
|
|
376
|
+
const census = new Map();
|
|
377
|
+
const maps = [];
|
|
378
|
+
const count = list => { for (const c of list ?? []) census.set(c.code, (census.get(c.code) ?? 0) + 1); };
|
|
379
|
+
for (const ce of await readJson('CommonEvents.json').catch(() => [])) if (ce) count(ce.list);
|
|
380
|
+
for (const info of mapInfos) {
|
|
381
|
+
if (!info) continue;
|
|
382
|
+
const id = info.id;
|
|
383
|
+
const map = await readJson(`Map${String(id).padStart(3, '0')}.json`).catch(() => null);
|
|
384
|
+
if (!map) continue;
|
|
385
|
+
const lines = [`[gd_scene load_steps=3 format=3]`, '', '[ext_resource type="Script" path="res://rpg/rpg_map.gd" id="1_map"]', '[ext_resource type="Script" path="res://rpg/rpg_event.gd" id="2_event"]', '', `[node name="Map${String(id).padStart(3, '0')}" type="Node2D"]`, 'script = ExtResource("1_map")', `map_id = ${id}`, `tile_size = ${tile}`, `metadata/display_name = ${JSON.stringify(map.displayName || info.name || '')}`];
|
|
386
|
+
let events = 0;
|
|
387
|
+
for (const ev of map.events ?? []) {
|
|
388
|
+
if (!ev) continue;
|
|
389
|
+
for (const pg of ev.pages ?? []) count(pg.list);
|
|
390
|
+
const name = `EV${String(ev.id).padStart(3, '0')}_${String(ev.name || 'event').replace(/[^A-Za-z0-9_]+/g, '_')}`;
|
|
391
|
+
lines.push('', `[node name="${name}" type="Node2D" parent="."]`, `position = Vector2(${ev.x * tile + tile / 2}, ${ev.y * tile + tile / 2})`, 'script = ExtResource("2_event")', `map_id = ${id}`, `event_id = ${ev.id}`);
|
|
392
|
+
events++;
|
|
393
|
+
}
|
|
394
|
+
const sceneRel = `maps/map_${String(id).padStart(3, '0')}.tscn`;
|
|
395
|
+
await fs.mkdir(path.join(outDir, 'maps'), { recursive: true });
|
|
396
|
+
await fs.writeFile(path.join(outDir, sceneRel), lines.join('\n') + '\n');
|
|
397
|
+
maps.push({ id, name: info.name, size: `${map.width}x${map.height}`, events, scene: sceneRel });
|
|
398
|
+
}
|
|
399
|
+
// Start scene: the start map with the player marker and the message box.
|
|
400
|
+
const startMap = system.startMapId ?? maps[0]?.id ?? 1;
|
|
401
|
+
const startScene = `res://maps/map_${String(startMap).padStart(3, '0')}.tscn`;
|
|
402
|
+
await fs.writeFile(path.join(outDir, 'main.tscn'), `[gd_scene load_steps=3 format=3]\n\n[ext_resource type="PackedScene" path="${startScene}" id="1_map"]\n[ext_resource type="Script" path="res://rpg/rpg_message_box.gd" id="2_msg"]\n\n[node name="Main" type="Node2D"]\n\n[node name="Map" parent="." instance=ExtResource("1_map")]\n\n[node name="Player" type="Marker2D" parent="."]\nposition = Vector2(${(system.startX ?? 0) * tile + tile / 2}, ${(system.startY ?? 0) * tile + tile / 2})\n\n[node name="MessageBox" type="CanvasLayer" parent="."]\nscript = ExtResource("2_msg")\n`);
|
|
403
|
+
const width = mz ? (system.advanced?.screenWidth ?? 816) : 816, height = mz ? (system.advanced?.screenHeight ?? 624) : 624;
|
|
404
|
+
await fs.writeFile(path.join(outDir, 'project.godot'), `; Engine configuration file.\n; Ported by ReversePort from an RPG Maker ${mz ? 'MZ' : 'MV'} project.\n\nconfig_version=5\n\n[application]\n\nconfig/name=${JSON.stringify(system.gameTitle || path.basename(root))}\nrun/main_scene="res://main.tscn"\nconfig/features=PackedStringArray("4.3")\n\n[autoload]\n\nRpgDb="*res://rpg/rpg_database.gd"\n\n[display]\n\nwindow/size/viewport_width=${width}\nwindow/size/viewport_height=${height}\n\n[rendering]\n\ntextures/canvas_textures/default_texture_filter=0\n`);
|
|
405
|
+
|
|
406
|
+
const commands = [...census.entries()].sort((a, b) => b[1] - a[1]).map(([code, n]) => ({ code, name: SUPPORTED.get(code) ?? NAMES[code] ?? `code ${code}`, count: n, ported: SUPPORTED.has(code) }));
|
|
407
|
+
const missing = commands.filter(c => !c.ported);
|
|
408
|
+
const notes = ['# Port notes', '', `Ported by ReversePort from RPG Maker ${mz ? 'MZ' : 'MV'}: ${maps.length} maps, ${copied} data/asset files.`, '', '## Event commands not run by rpg_event.gd', '', ...(missing.length ? missing.map(c => `- ${c.code} ${c.name}: ${c.count}×`) : ['- none']), '', '## Also by hand', '', '- Player movement, collision (tileset flags in data/Tilesets.json), followers and vehicles.', '- Menus, battle system, shops and save/load.', '- Autotile edges (A1–A4 are drawn with their interior piece), animated water and parallax.', '- Plugins (js/plugins.js): each needs its own port.', ...(encrypted ? ['- Images/audio are encrypted in the source (.rpgmvp/.png_ etc.); they are copied but not decrypted. Use the unencrypted project.'] : [])];
|
|
409
|
+
await fs.writeFile(path.join(outDir, 'PORT-NOTES.md'), notes.join('\n') + '\n');
|
|
410
|
+
return {
|
|
411
|
+
source: root, out: outDir, engine: mz ? 'RPG Maker MZ' : 'RPG Maker MV', maps: limit(maps, max), copied, tileSize: tile, startMap,
|
|
412
|
+
commands: { total: commands.reduce((s, c) => s + c.count, 0), ported: commands.filter(c => c.ported).reduce((s, c) => s + c.count, 0), missing: limit(missing, 60) },
|
|
413
|
+
...(encrypted ? { encrypted: 'images/audio are encrypted in this project: copied, never decrypted; use the unencrypted project' } : {}),
|
|
414
|
+
next: `Open ${outDir} in Godot 4.3+ and run: the start map draws and its events run. PORT-NOTES.md lists the event commands and systems still to port (movement, menus, battles first). Text: re_strings on the source for translation.`,
|
|
415
|
+
};
|
|
416
|
+
}
|