Cnass commited on
Commit
6de14d3
·
verified ·
1 Parent(s): 1c9a2bb

Upload 3 files

Browse files
app/rpgmaker/extract.py CHANGED
@@ -3,15 +3,16 @@
3
  Deliberately narrow. RPG Maker stores executable script, plugin bindings,
4
  asset filenames and engine identifiers in the same JSON arrays as dialogue,
5
  and translating any of those breaks the game rather than mistranslating it -
6
- so this reads exactly three things:
7
 
8
  401 / 405 Show Text / Show Scrolling Text - the dialogue itself
9
  102 / 402 Show Choices / When[choice] - the buttons under that dialogue
10
  System.json terms - the menus wrapped around both
 
11
 
12
- Everything else is left alone, including the database files (item and skill
13
- names), the MZ speaker-name field on 101, name-change commands (320/324/325),
14
- plugin commands (356/357) and Script blocks (355/655).
15
 
16
  Consecutive 401 commands are one message box split across lines, not separate
17
  sentences, so a run of them is merged into a single unit and translated as one
@@ -54,6 +55,31 @@ _SYSTEM_TYPE_LISTS = (
54
  "weaponTypes",
55
  )
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  @dataclass
59
  class Slot:
@@ -61,10 +87,16 @@ class Slot:
61
 
62
  `paths` is a list because a merged run of 401 commands writes one line per
63
  original command: the box keeps the same number of lines it had.
 
 
 
 
 
64
  """
65
 
66
  file: str
67
  paths: list[list] = field(default_factory=list)
 
68
 
69
 
70
  @dataclass
@@ -173,20 +205,40 @@ def _extract_system(data: dict, sink) -> None:
173
  if isinstance(values, list):
174
  for index, value in enumerate(values):
175
  if isinstance(value, str) and value.strip():
176
- sink(value, Slot(file=name, paths=[["terms", key, index]]))
177
 
178
  messages = terms.get("messages")
179
  if isinstance(messages, dict):
180
  for key, value in messages.items():
181
  if isinstance(value, str) and value.strip():
182
- sink(value, Slot(file=name, paths=[["terms", "messages", key]]))
183
 
184
  for key in _SYSTEM_TYPE_LISTS:
185
  values = data.get(key)
186
  if isinstance(values, list):
187
  for index, value in enumerate(values):
188
  if isinstance(value, str) and value.strip():
189
- sink(value, Slot(file=name, paths=[[key, index]]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
 
192
  def _extract_file(name: str, data, sink) -> None:
@@ -213,6 +265,9 @@ def _extract_file(name: str, data, sink) -> None:
213
  elif name == "System.json" and isinstance(data, dict):
214
  _extract_system(data, sink)
215
 
 
 
 
216
 
217
  def detect_data_root(root: Path) -> tuple[Path, str] | None:
218
  """Locate the data folder in an unpacked upload.
 
3
  Deliberately narrow. RPG Maker stores executable script, plugin bindings,
4
  asset filenames and engine identifiers in the same JSON arrays as dialogue,
5
  and translating any of those breaks the game rather than mistranslating it -
6
+ so this reads exactly four things:
7
 
8
  401 / 405 Show Text / Show Scrolling Text - the dialogue itself
9
  102 / 402 Show Choices / When[choice] - the buttons under that dialogue
10
  System.json terms - the menus wrapped around both
11
+ database name/description/message fields - items, skills, enemies, states
12
 
13
+ Everything else is left alone, including the MZ speaker-name field on 101,
14
+ name-change commands (320/324/325), plugin commands (356/357) and Script
15
+ blocks (355/655).
16
 
17
  Consecutive 401 commands are one message box split across lines, not separate
18
  sentences, so a run of them is merged into a single unit and translated as one
 
55
  "weaponTypes",
56
  )
57
 
58
+ # Database files: flat arrays of entries, index 0 always null (RPG Maker's ids
59
+ # are 1-based). Only the fields the player actually reads are listed.
60
+ #
61
+ # `message1`..`message4` are battle-log templates ("%1 takes damage!") whose %1
62
+ # and %2 are filled in by the engine; Hy-MT2 carries them through the same way
63
+ # it carries \\C[2], so they are translated like any other line.
64
+ #
65
+ # Deliberately absent:
66
+ # note - stores plugin notetags (<tag:value>) mixed with free text.
67
+ # Translating one would silently change plugin behaviour, and
68
+ # splitting text from tags is guesswork, so the whole field is
69
+ # left alone.
70
+ # Actors - names, nicknames and profiles are the character names this
71
+ # tool does not translate.
72
+ # Classes - class names read as character labels next to actor names, so
73
+ # they follow the same rule.
74
+ _DATABASE_FIELDS: dict[str, tuple[str, ...]] = {
75
+ "Items.json": ("name", "description"),
76
+ "Weapons.json": ("name", "description"),
77
+ "Armors.json": ("name", "description"),
78
+ "Skills.json": ("name", "description", "message1", "message2"),
79
+ "Enemies.json": ("name",),
80
+ "States.json": ("name", "message1", "message2", "message3", "message4"),
81
+ }
82
+
83
 
84
  @dataclass
85
  class Slot:
 
87
 
88
  `paths` is a list because a merged run of 401 commands writes one line per
89
  original command: the box keeps the same number of lines it had.
90
+
91
+ `raw` marks a slot that owns a whole field rather than one line of a
92
+ message box. Those are written exactly as the model returned them - a
93
+ two-line item description has to stay two lines, where the line-per-command
94
+ slots get re-flowed to match the box they came from.
95
  """
96
 
97
  file: str
98
  paths: list[list] = field(default_factory=list)
99
+ raw: bool = False
100
 
101
 
102
  @dataclass
 
205
  if isinstance(values, list):
206
  for index, value in enumerate(values):
207
  if isinstance(value, str) and value.strip():
208
+ sink(value, Slot(file=name, paths=[["terms", key, index]], raw=True))
209
 
210
  messages = terms.get("messages")
211
  if isinstance(messages, dict):
212
  for key, value in messages.items():
213
  if isinstance(value, str) and value.strip():
214
+ sink(value, Slot(file=name, paths=[["terms", "messages", key]], raw=True))
215
 
216
  for key in _SYSTEM_TYPE_LISTS:
217
  values = data.get(key)
218
  if isinstance(values, list):
219
  for index, value in enumerate(values):
220
  if isinstance(value, str) and value.strip():
221
+ sink(value, Slot(file=name, paths=[[key, index]], raw=True))
222
+
223
+
224
+ def _extract_database(name: str, data: list, fields: tuple[str, ...], sink) -> None:
225
+ """Read the player-visible fields out of one database file.
226
+
227
+ Index 0 of these arrays is null and entries can carry empty strings for
228
+ fields the designer never filled in, so both are skipped: writing a
229
+ translation into a blank description would put text on an item that is
230
+ meant to show none.
231
+ """
232
+ for entry_index, entry in enumerate(data):
233
+ if not isinstance(entry, dict):
234
+ continue
235
+ for key in fields:
236
+ value = entry.get(key)
237
+ if isinstance(value, str) and value.strip():
238
+ sink(
239
+ value,
240
+ Slot(file=name, paths=[[entry_index, key]], raw=True),
241
+ )
242
 
243
 
244
  def _extract_file(name: str, data, sink) -> None:
 
265
  elif name == "System.json" and isinstance(data, dict):
266
  _extract_system(data, sink)
267
 
268
+ elif name in _DATABASE_FIELDS and isinstance(data, list):
269
+ _extract_database(name, data, _DATABASE_FIELDS[name], sink)
270
+
271
 
272
  def detect_data_root(root: Path) -> tuple[Path, str] | None:
273
  """Locate the data folder in an unpacked upload.
app/rpgmaker/inject.py CHANGED
@@ -85,13 +85,15 @@ def apply(
85
  Groups writes by file so each file is parsed and serialised once, however
86
  many strings land in it. Returns (written_slots, skipped_slots).
87
  """
88
- slots_by_file: dict[str, list[tuple[list[list], str]]] = {}
89
  for unit in units:
90
  translated = translations.get(unit.source)
91
  if translated is None:
92
  continue
93
  for slot in unit.slots:
94
- slots_by_file.setdefault(slot.file, []).append((slot.paths, translated))
 
 
95
 
96
  written = skipped = 0
97
  for file_name, entries in slots_by_file.items():
@@ -99,11 +101,16 @@ def apply(
99
  try:
100
  data = json.loads(json_path.read_text(encoding="utf-8"))
101
  except (OSError, json.JSONDecodeError):
102
- skipped += sum(len(paths) for paths, _ in entries)
103
  continue
104
 
105
- for paths, translated in entries:
106
- for line, path in zip(rewrap(translated, len(paths)), paths):
 
 
 
 
 
107
  if _assign(data, path, line):
108
  written += 1
109
  else:
 
85
  Groups writes by file so each file is parsed and serialised once, however
86
  many strings land in it. Returns (written_slots, skipped_slots).
87
  """
88
+ slots_by_file: dict[str, list[tuple[list[list], str, bool]]] = {}
89
  for unit in units:
90
  translated = translations.get(unit.source)
91
  if translated is None:
92
  continue
93
  for slot in unit.slots:
94
+ slots_by_file.setdefault(slot.file, []).append(
95
+ (slot.paths, translated, slot.raw)
96
+ )
97
 
98
  written = skipped = 0
99
  for file_name, entries in slots_by_file.items():
 
101
  try:
102
  data = json.loads(json_path.read_text(encoding="utf-8"))
103
  except (OSError, json.JSONDecodeError):
104
+ skipped += sum(len(paths) for paths, _, _ in entries)
105
  continue
106
 
107
+ for paths, translated, raw in entries:
108
+ # A raw slot is one whole field (an item description, a menu term),
109
+ # so it keeps whatever line breaks the model produced. Everything
110
+ # else is a message box and has to come back with the line count it
111
+ # went in with.
112
+ lines = [translated] if raw else rewrap(translated, len(paths))
113
+ for line, path in zip(lines, paths):
114
  if _assign(data, path, line):
115
  written += 1
116
  else:
app/rpgmaker/store.py CHANGED
@@ -65,7 +65,12 @@ def load_units(project_id: str) -> list[Unit]:
65
  return [
66
  Unit(
67
  source=item["source"],
68
- slots=[Slot(file=s["file"], paths=s["paths"]) for s in item["slots"]],
 
 
 
 
 
69
  )
70
  for item in raw
71
  ]
 
65
  return [
66
  Unit(
67
  source=item["source"],
68
+ slots=[
69
+ # .get for `raw`: projects indexed before raw slots existed have
70
+ # no such key, and a resumed job must still read its own units.
71
+ Slot(file=s["file"], paths=s["paths"], raw=s.get("raw", False))
72
+ for s in item["slots"]
73
+ ],
74
  )
75
  for item in raw
76
  ]