diff --git a/dev/ysbin_tool.py b/dev/ysbin_tool.py index 80eac14..614d82e 100644 --- a/dev/ysbin_tool.py +++ b/dev/ysbin_tool.py @@ -355,11 +355,13 @@ def extract_dialogue(raw): # ── pack ───────────────────────────────────────────────────────────────────── -def patch_ystb(raw, translations): +def patch_ystb(raw, translations, wrap_nl_bytes=b'\x0a'): """ Patch translated text into a YSTB binary. translations : {src_text: tl_text} + wrap_nl_bytes : bytes to insert as line-break separator (default b'\x0a'). + YU-RIS native newline is b'\xef\xf0' (two-byte control code). Returns patched bytes, or None if nothing changed (no matches found). Strategy: @@ -411,6 +413,9 @@ def patch_ystb(raw, translations): new_bytes = tl.encode('cp932') except Exception: new_bytes = tl.encode('utf-8') + # Replace sentinel 0x01 with the requested newline bytes + if wrap_nl_bytes != b'\x0a': + new_bytes = new_bytes.replace(b'\x01', wrap_nl_bytes) attr_replacements[attr_idx] = new_bytes elif a_type == 3: @@ -824,6 +829,25 @@ def patch_font_sizes(raw, style_sizes): return bytes(new_header) + instr_enc + new_attrs_enc + new_vals_enc + tail +# ── word-wrap helper ──────────────────────────────────────────────────────────── + +def _wrap_text(text, width): + """ + Word-wrap `text` to at most `width` characters per line. + Returns the wrapped string with '\n' (0x0a) as the line separator. + Preserves any manual '\n' already present in the text. + """ + import textwrap + paragraphs = text.split('\n') + lines = [] + for para in paragraphs: + if len(para) <= width: + lines.append(para) + else: + lines.extend(textwrap.wrap(para, width=width) or ['']) + return '\n'.join(lines) + + # ── CLI commands ────────────────────────────────────────────────────────────── def cmd_extract(args): @@ -882,6 +906,12 @@ def cmd_extract(args): def cmd_pack(args): json_dir = args.json_dir ysbin_dir = args.ysbin_dir + wrap_width = getattr(args, 'wrap', 0) or 0 + wrap_nl_str = getattr(args, 'wrap_newline', 'eff0') or 'eff0' + try: + wrap_nl_bytes = bytes.fromhex(wrap_nl_str.replace(' ', '').replace('0x', '')) + except ValueError: + wrap_nl_bytes = b'\xef\xf0' patched = 0 skipped = 0 @@ -948,6 +978,14 @@ def cmd_pack(args): if not e.get('src') or not e.get('tl') or e['tl'] == e['src']: continue tl = e['tl'] + # Word-wrap English speech text if requested. + # Only wrap if the translation looks like English (no CJK). + if wrap_width and tl and not any(0x3000 <= ord(c) <= 0x9fff for c in tl): + tl = _wrap_text(tl, wrap_width) + # If a non-default newline byte is requested, store a sentinel + # that will be replaced after cp932 encoding below. + if wrap_nl_bytes != b'\x0a': + tl = tl.replace('\n', '\x01') # temp sentinel (0x01 unused in text) key = e.get('_raw', e['src']) # Re-attach the ORIGINAL speaker prefix from _raw so the engine # continues to recognise the name in its es.CHAR.NAME registry. @@ -974,7 +1012,7 @@ def cmd_pack(args): with open(ybn_path, 'rb') as f: raw = f.read() - result = patch_ystb(raw, translations) + result = patch_ystb(raw, translations, wrap_nl_bytes=wrap_nl_bytes) if result is None: print(f' SKIP {fname}: no matching strings found in binary') skipped += 1 @@ -1375,6 +1413,10 @@ def main(): p_pack = sub.add_parser('pack', help='JSON → .ybn (patch in-place)') p_pack.add_argument('--ysbin-dir', default='data/ysbin') p_pack.add_argument('--json-dir', default='json') + p_pack.add_argument('--wrap', type=int, default=0, metavar='N', + help='Auto word-wrap English lines to N chars (0 = off, try 50)') + p_pack.add_argument('--wrap-newline', default='eff0', metavar='HEX', + help='Hex bytes for line separator (default: eff0 = YU-RIS native newline 0xEF 0xF0)') # repack-ypf p_ypf = sub.add_parser('repack-ypf', help='data/ysbin/*.ybn → patched .ypf')