fix: align and shorten tutorial introduction paragraphs

This commit is contained in:
DragonHD
2026-09-20 11:46:04 +08:00
parent 2e214efeb8
commit 72a3d000dd
5 changed files with 58 additions and 6 deletions
+12 -3
View File
@@ -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)
+34
View File
@@ -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()
+2 -2
View File
@@ -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": "镜头回正",
+1 -1
View File
@@ -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