fix(rpgmaker): keep code 101 labels out of dialogue text

- Translate only the inner name while preserving display brackets
- Strip temporary speaker context before writing code 401 dialogue
- Cover malformed bracketed labels and reruns with a regression test
This commit is contained in:
DazedAnon 2026-07-25 14:34:51 -05:00
parent 38ca8e44be
commit d1023178fe
2 changed files with 59 additions and 5 deletions

View file

@ -588,6 +588,22 @@ def _101_name_source(cmd, is_var: bool) -> str:
return ""
def _101_speaker_name(raw_name: str) -> str:
"""Return the translatable name inside a code-101 display wrapper.
Parameter 4 may use ``Name`` as its visible name-window styling. Those
brackets belong in the 101 field, not in the ``[Speaker]:`` transport prefix
temporarily attached to the following 401 dialogue.
"""
name = str(raw_name or "").strip()
name = re.sub(r"^(?:[\\]+[cC]\[\d+\]\s*)+", "", name)
bracketed = re.match(r"^【\s*([^】]+?)\s*】", name)
if bracketed:
return bracketed.group(1).strip()
plain = re.match(r"^([^\\]+)", name)
return plain.group(1).strip() if plain else ""
def _entry_orig(entry) -> dict:
"""Return _original dict on a database entry, or empty dict if absent."""
orig = entry.get("_original") if isinstance(entry, dict) else None
@ -3136,9 +3152,8 @@ def searchCodes(page, pbar, jobList, filename):
# Get Speaker
rawName = _101_name_source(codeList[i], isVar)
match = re.search(r"^(?:[\\]+[cC]\[\d+?\])?([^\\]+)", rawName)
if match:
sourceName = match.group(1)
sourceName = _101_speaker_name(rawName)
if sourceName:
response = getSpeaker(sourceName)
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]

View file

@ -112,7 +112,7 @@ def _resolve_case_command(page_list, entry, marked_cases=None):
return cmd
def _run_search_codes(page, *, preserve_original=True):
def _run_search_codes(page, *, preserve_original=True, speaker_fn=None):
"""Full Pass 1 -> mock translate -> Pass 2 cycle."""
captured = []
@ -121,7 +121,7 @@ def _run_search_codes(page, *, preserve_original=True):
return _mock_translate(text, history, batch)
def speaker(name):
return _mock_speaker(name)
return speaker_fn(name) if speaker_fn is not None else _mock_speaker(name)
orig_t = mvmz.translateAI
orig_s = mvmz.getSpeaker
@ -194,6 +194,45 @@ class TestMVMZSourceOriginal(unittest.TestCase):
self.assertEqual(c122["_original"], "変数テスト")
self.assertIn("EN_TRANSLATED", c122["parameters"][4])
def test_101_display_brackets_do_not_leak_into_401_dialogue(self):
speakers_seen = []
def speaker(name):
speakers_seen.append(name)
return ["Game Description", [0, 0]]
page = {
"list": [
{
"code": 101,
"indent": 0,
"parameters": ["", 0, 0, 2, "【[Game Description]】"],
"_original": "【ゲーム説明】",
},
{
"code": 401,
"indent": 0,
"parameters": ["ここから本編スタートとなります。"],
},
]
}
translated, captured = _run_search_codes(page, speaker_fn=speaker)
cmd101, cmd401 = translated["list"]
self.assertTrue(speakers_seen)
self.assertEqual(set(speakers_seen), {"ゲーム説明"})
self.assertEqual(cmd101["parameters"][4], "【Game Description】")
self.assertEqual(cmd401["parameters"][0], "EN_TRANSLATED")
self.assertNotIn("Game Description", cmd401["parameters"][0])
self.assertTrue(
any(
isinstance(item, str) and item.startswith("[Game Description]: ")
for payload in captured
for item in (payload if isinstance(payload, list) else [payload])
)
)
def test_rerun_uses_original_not_display_text(self):
page1, captured1 = _run_search_codes(_load_map_excerpt())
originals_snapshot = json.dumps(