From 72a3d000dd477ae532e79e745f91c55e99192a93 Mon Sep 17 00:00:00 2001 From: DragonHD Date: Sun, 20 Sep 2026 11:46:04 +0800 Subject: [PATCH] fix: align and shorten tutorial introduction paragraphs --- docs/DEVLOG.md | 9 +++++++++ tools/script_text.py | 15 ++++++++++++--- tools/test_tutorial_intro.py | 34 ++++++++++++++++++++++++++++++++++ tools/tutorial_cn.json | 4 ++-- tools/verify_tutorial_step1.py | 2 +- 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 tools/test_tutorial_intro.py diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 45014f3..5bb7709 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -163,3 +163,12 @@ so centred labels (the "Start New Game" button) were flushed right. Fixed to - Original backup: `E:/DragonHD/backup_game/tutorial-step1-20260920/Tutorial.dsc`; build: `E:/DragonHD/build/tutorial-step1/Tutorial.dsc`. Deployed to the Steam English script directory after checking the game was stopped and the original hash still matched. Re-read deployed file and passed the same byte-span verification. - Git Bash rollback script was executed on a separate translated copy; restored bytes/hash match the original exactly. The deployed tutorial and deliverable remain translated. - In-game acceptance PENDING: start a fresh tutorial, read intro, rotate camera, press numpad 5, zoom with wheel, and check each next-step trigger. Then translate flight in the next small batch. No claim yet about native script loading or rendered layout. + +## 2026-09-20 — Intro paragraph layout correction + +- User confirmed the subsequent tutorial pages work; opening screenshot showed orphan comma and inconsistent alignment. Engine replay at width 400 reproduced the first-line 15px indent from `\f`, unindented wrapped continuation, and centered final paragraph from `\a`. +- Replaced only intro message/mission paragraphs (literal indexes 1 and 2 relative to the previous trial) with concise text and `\b` left alignment. Added explicit `--allow-alignment-changes` build opt-in; default control validation remains strict, opt-in still preserves control order/count and newlines. +- New regression failed on original mapping (x=15, expected 0), then passed at widths 360/400/480. All 32 tests pass. All other script bytes unchanged relative to previous Chinese trial. +- Built and deployed after process and source-hash checks. SHA256: `15a7a24a236d01af4f5f3bcc89901dac5ab0211ed51357dfda5ef78d0026c423`. Backup of previous trial: `E:/DragonHD/backup_game/tutorial-intro-layout-20260920/Tutorial.dsc`. Rollback executed on separate copy and restored previous hash. +- Rebuild: `python tools/script_text.py --source E:/DragonHD/backup_game/tutorial-step1-20260920/Tutorial.dsc --mapping tools/tutorial_cn.json --out E:/DragonHD/build/tutorial-intro-layout/Tutorial.dsc --allow-alignment-changes`. +- Opening page visual recheck pending. Later pages retained exactly as user-tested. diff --git a/tools/script_text.py b/tools/script_text.py index 76589e8..e65c265 100644 --- a/tools/script_text.py +++ b/tools/script_text.py @@ -129,7 +129,7 @@ def encode_literal(text: str) -> bytes: return b'"' + b''.join(ENCODE_ESCAPES.get(b, bytes((b,))) for b in raw) + b'"' -def patch_display_calls(source: bytes, translations: dict[str, str]) -> bytes: +def patch_display_calls(source: bytes, translations: dict[str, str], *, allow_alignment_changes=False) -> bytes: literals = extract_display_literals(source) missing = set(translations) - {literal.text for literal in literals} if missing: @@ -141,7 +141,14 @@ def patch_display_calls(source: bytes, translations: dict[str, str]) -> bytes: translated = translations[literal.text] if not isinstance(translated, str): raise ValueError('translation must be a string') - if CONTROLS.findall(literal.text) != CONTROLS.findall(translated): + original_controls = CONTROLS.findall(literal.text) + translated_controls = CONTROLS.findall(translated) + if allow_alignment_changes: + # Explicit opt-in: replace paragraph alignment/indent controls only. + # Newline count/order and all other controls remain protected. + original_controls = ['ALIGN' if c in '\a\b\f\v' else c for c in original_controls] + translated_controls = ['ALIGN' if c in '\a\b\f\v' else c for c in translated_controls] + if original_controls != translated_controls: raise ValueError('control sequence changed: %r' % literal.text) if FORMAT.findall(literal.text) != FORMAT.findall(translated): raise ValueError('format sequence changed: %r' % literal.text) @@ -157,6 +164,7 @@ def main(): parser.add_argument('--source', required=True, type=Path) parser.add_argument('--mapping', required=True, type=Path) parser.add_argument('--out', required=True, type=Path) + parser.add_argument('--allow-alignment-changes', action='store_true') args = parser.parse_args() if args.source.resolve() == args.out.resolve(): parser.error('output must differ from source') @@ -164,12 +172,13 @@ def main(): mapping = json.loads(args.mapping.read_text(encoding='utf-8')) if not isinstance(mapping, dict): parser.error('mapping must be an object of original text to translated text') - result = patch_display_calls(source, mapping) + result = patch_display_calls(source, mapping, allow_alignment_changes=args.allow_alignment_changes) changed = [literal for literal in extract_display_literals(source) if literal.text in mapping] manifest = {'source': str(args.source.resolve()), 'output': str(args.out.resolve()), 'source_sha256': hashlib.sha256(source).hexdigest(), 'output_sha256': hashlib.sha256(result).hexdigest(), 'mapping_count': len(mapping), 'changed_literals': len(changed), + 'allow_alignment_changes': args.allow_alignment_changes, 'changes': [{'start': lit.start, 'end': lit.end, 'call': lit.call, 'argument': lit.argument} for lit in changed]} args.out.parent.mkdir(parents=True, exist_ok=True) diff --git a/tools/test_tutorial_intro.py b/tools/test_tutorial_intro.py new file mode 100644 index 0000000..5de8d00 --- /dev/null +++ b/tools/test_tutorial_intro.py @@ -0,0 +1,34 @@ +"""Replay tutorial introduction through the deployed engine's layout routine.""" +import json +from pathlib import Path +import unittest +from script_text import patch_display_calls +from test_cjk_layout import layout + + +class IntroTests(unittest.TestCase): + def test_intro_paragraphs_are_left_aligned_without_orphan_punctuation(self): + mapping = json.loads(Path(__file__).with_name('tutorial_cn.json').read_text(encoding='utf-8')) + intros = [v for k, v in mapping.items() if k.startswith('\fNow')] + self.assertEqual(len(intros), 2) + for intro in intros: + for width in (360, 400, 480): + lines, _ = layout(intro, width=width, alignment=0) + for raw, x, y in lines: + self.assertEqual(x, 0) + self.assertNotIn(raw.decode('gb2312')[0], ',。!?、;:”)') + self.assertEqual(len(lines), 4) + + def test_alignment_override_is_explicit_and_preserves_newlines(self): + source = b'SetMissionDescription("\\fA\\n\\aB");' + mapping = {'\fA\n\aB': '\bA\n\bB'} + with self.assertRaises(ValueError): + patch_display_calls(source, mapping) + self.assertEqual(patch_display_calls(source, mapping, allow_alignment_changes=True), + b'SetMissionDescription("\\bA\\n\\bB");') + with self.assertRaises(ValueError): + patch_display_calls(source, {'\fA\n\aB': '\bA\bB'}, allow_alignment_changes=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tutorial_cn.json b/tools/tutorial_cn.json index a0061b5..7677dc7 100644 --- a/tools/tutorial_cn.json +++ b/tools/tutorial_cn.json @@ -1,7 +1,7 @@ { "Tutorial": "操作教程", - "\fNow you have the chance to learn the game controls. Note the \"Next\" button at the top of the screen - click on it after completing each task to move to the next.\n\fYou can get help with the game interface by pressing the \"H\" button.\n\fYou don't have to finish this tutorial (although we recommend doing it once - there are some useful things to learn), you can exit it at any time by pressing the \"Esc\" button.\n\u0007(Note two buttons marked by red arrows: \"Next\" and a button that shows this text)": "\f欢迎进入操作教程。每完成一项练习,点击屏幕上方的“继续教程”按钮,进入下一项。\n\f按 H 键可查看界面帮助。\n\f建议首次游玩时完成教程。随时按 Esc 可退出。\n\u0007红色箭头标出了“继续教程”和“当前任务”按钮。", - "\fNow you have the chance to learn the game controls. Note the \"Next\" button at the top of the screen - click on it after completing each task to move to the next.\n\fYou can get help on the game interface by pressing the \"H\" button.\n\fYou don't have to finish this tutorial (although we recommend doing it once - there are some useful things to learn), you can exit it at any time by pressing \"Esc\" button.\n\u0007(Note two buttons marked by red arrows: \"Next\" and a button that shows this text)": "\f欢迎进入操作教程。每完成一项练习,点击屏幕上方的“继续教程”按钮,进入下一项。\n\f按 H 键可查看界面帮助。\n\f建议首次游玩时完成教程。随时按 Esc 可退出。\n\u0007红色箭头标出了“继续教程”和“当前任务”按钮。", + "\fNow you have the chance to learn the game controls. Note the \"Next\" button at the top of the screen - click on it after completing each task to move to the next.\n\fYou can get help with the game interface by pressing the \"H\" button.\n\fYou don't have to finish this tutorial (although we recommend doing it once - there are some useful things to learn), you can exit it at any time by pressing the \"Esc\" button.\n\u0007(Note two buttons marked by red arrows: \"Next\" and a button that shows this text)": "\b每完成一项练习,点击上方“继续教程”。\n\b按 H 键查看界面帮助。\n\b建议首次游玩时完成教程;按 Esc 可退出。\n\b红箭头指向“继续教程”和“当前任务”按钮。", + "\fNow you have the chance to learn the game controls. Note the \"Next\" button at the top of the screen - click on it after completing each task to move to the next.\n\fYou can get help on the game interface by pressing the \"H\" button.\n\fYou don't have to finish this tutorial (although we recommend doing it once - there are some useful things to learn), you can exit it at any time by pressing \"Esc\" button.\n\u0007(Note two buttons marked by red arrows: \"Next\" and a button that shows this text)": "\b每完成一项练习,点击上方“继续教程”。\n\b按 H 键查看界面帮助。\n\b建议首次游玩时完成教程;按 Esc 可退出。\n\b红箭头指向“继续教程”和“当前任务”按钮。", "Camera rotation": "旋转镜头", "\fMove the mouse cursor to the left or right edge of the screen to rotate the view. Alternatively, use the numeric keypad or hold the center mouse button (wheel) and the Shift key and move the mouse.": "\f将鼠标移到屏幕左、右边缘可旋转视角。也可以使用数字小键盘,或按住 Shift 和鼠标中键(滚轮)并移动鼠标。", "Center the camera": "镜头回正", diff --git a/tools/verify_tutorial_step1.py b/tools/verify_tutorial_step1.py index 038357a..67376c1 100644 --- a/tools/verify_tutorial_step1.py +++ b/tools/verify_tutorial_step1.py @@ -22,7 +22,7 @@ def main(): print(args.mode.upper() + ': original bytes and English tutorial restored; PASS') return mapping = json.loads(Path(__file__).with_name('tutorial_cn.json').read_text(encoding='utf-8')) - assert result == patch_display_calls(source, mapping), 'build mismatch' + assert result == patch_display_calls(source, mapping, allow_alignment_changes=True), 'build mismatch' before = extract_display_literals(source) after = extract_display_literals(result, encoding='gb2312') assert len(before) == len(after) == 55