From 34c60491994d16a3b37c0ea38b104dccfae33d16 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 30 Sep 2026 08:56:03 +0900 Subject: [PATCH] zh locale: translate skill names and descriptions skilldesc.txt takes Longju's name, description and condition columns and its affect texts where their format arguments match 40250's; a reviewed overlay fills the gaps and renames the empire languages. Co-Authored-By: Claude Opus 5.5 --- data/zh_40250/README.md | 7 +++++ data/zh_40250/skilldesc_zh_overlay.txt | 19 ++++++++++++ scripts/build_zh_locale.py | 43 ++++++++++++++++++++++++++ scripts/test_build_zh_locale.py | 17 ++++++++++ 4 files changed, 86 insertions(+) create mode 100644 data/zh_40250/skilldesc_zh_overlay.txt diff --git a/data/zh_40250/README.md b/data/zh_40250/README.md index 6b2c6b20..c8053f12 100644 --- a/data/zh_40250/README.md +++ b/data/zh_40250/README.md @@ -31,3 +31,10 @@ - GBK 双字节字符的第二个字节可能是 0x5C(反斜杠),构建时会把它写成两个。 - 查看待译条目:`python3 scripts/build_zh_translate_lua.py todo <目录> --quests <命名空间,...>`。 - 部署到 203:覆盖 `/usr/metin2/server/share/locale/newcibn/translate.lua`,然后重启。英文原件备份在同目录的 `translate.lua.en-20260930`。 + +## 技能名称与说明(skilldesc.txt) + +`scripts/build_zh_locale.py` 以 40250 的 `skilldesc.txt` 为底,逐行取龙驹同 VNUM 的名称、说明和条件列(2–8) +以及效果列(17/20/23)。客户端把效果列当作 `_snprintf` 格式串传入最小值、最大值,所以效果列只有在格式参数与英文一致时才替换。 +`skilldesc_zh_overlay.txt` 按 `VNUM\t列号\t译文` 覆盖龙驹缺的或者不对的格。例如 126–128 在龙驹里叫吴/蜀/魏国方言, +这里改成盛唐语、秦皇语、汉武语;骑马技能的说明补上等级要求。 diff --git a/data/zh_40250/skilldesc_zh_overlay.txt b/data/zh_40250/skilldesc_zh_overlay.txt new file mode 100644 index 00000000..ac6a0d9f --- /dev/null +++ b/data/zh_40250/skilldesc_zh_overlay.txt @@ -0,0 +1,19 @@ +VNUM COLUMN TEXT +1 7 多目标攻击 +2 7 多目标攻击 +17 7 多目标攻击 +19 8 不会被击倒 +20 20 气绝机率 %.0f%% +64 20 对方攻击失败机率增加 +65 20 近身伤害反射 %.1f%% +126 2 盛唐语 +126 5 可以听懂盛唐国的语言 +127 2 秦皇语 +127 5 可以听懂秦皇国的语言 +128 2 汉武语 +128 5 可以听懂汉武国的语言 +137 5 骑马奔跑并攻击周围的敌人(50级起) +138 5 击倒前方拦截的敌人(52级起) +139 5 攻击周围的敌人(55级起) +140 5 攻击拦截的前方敌人(50级起) +152 17 最大生命值 +%.0f%% diff --git a/scripts/build_zh_locale.py b/scripts/build_zh_locale.py index 3520de26..9bee6318 100644 --- a/scripts/build_zh_locale.py +++ b/scripts/build_zh_locale.py @@ -31,6 +31,8 @@ KEYED_FILES = ("locale_game.txt", "locale_interface.txt") # Image folders whose labels are drawn text (the character window's tabs and stat labels). Each is taken # from Longju as a whole: its .sub files carry their own coordinates into the folder's atlas. IMAGE_DIRS = ("ui/windows",) +# skilldesc.txt columns taken from Longju: names, description, conditions (2-8), affect texts (17/20/23). +SKILL_COLUMNS = (2, 3, 4, 5, 6, 7, 8, 17, 20, 23) def keyed_lines(path: Path, encoding: str): @@ -71,6 +73,42 @@ def convert_names(source: Path, destination: Path, overlay: Path = None): return {"converted": converted, "overlaid": len(overlaid), "unrepresentable_vnums": unrepresentable} +def merge_skilldesc(en: Path, zh: Path, destination: Path, overlay: Path = None): + """40250 rows with Longju's text columns; an affect text keeps the English one unless its format + arguments match, since the client passes it to _snprintf with the min/max values.""" + chinese = {} + for line in zh.read_bytes().decode("gb18030").splitlines(): + parts = line.split("\t") + if parts[0].isdigit(): + chinese[parts[0]] = parts + reviewed = {} + if overlay and overlay.is_file(): + for line in overlay.read_text(encoding="utf-8").splitlines()[1:]: + vnum, column, text = line.split("\t") + reviewed[(vnum, int(column))] = text + rows, translated, incompatible = [], 0, [] + for line in en.read_bytes().decode("cp1252").splitlines(): + parts = line.split("\t") + if parts[0].isdigit(): + source = chinese.get(parts[0], []) + for column in SKILL_COLUMNS: + if column >= len(parts): + continue + text = reviewed.get((parts[0], column)) + if text is None and column < len(source) and source[column]: + text = source[column] + if text is None: + continue + if FORMAT.findall(parts[column]) != FORMAT.findall(text): + incompatible.append(f"{parts[0]}:{column}") + continue + parts[column] = text + translated += 1 + rows.append("\t".join(parts)) + destination.write_bytes(("\n".join(rows) + "\n").encode("gbk")) + return {"translated_cells": translated, "incompatible_format_cells": incompatible} + + def tool(name: str) -> Path: for build_dir in BUILD_DIRS: path = REPO / build_dir / "src/port" / name @@ -169,6 +207,11 @@ def build(en: Path, zh: Path, output: Path, client_base: Path, if (zh / filename).is_file(): shutil.copyfile(zh / filename, locale_dir / filename) + if (zh / "skilldesc.txt").is_file(): + report["files"]["skilldesc.txt"] = merge_skilldesc( + en / "skilldesc.txt", zh / "skilldesc.txt", locale_dir / "skilldesc.txt", + names_dir / "skilldesc_zh_overlay.txt" if names_dir else None) + for folder in IMAGE_DIRS: english_files = {path.name for path in (en / folder).iterdir()} chinese_files = {path.name for path in (zh / folder).iterdir()} diff --git a/scripts/test_build_zh_locale.py b/scripts/test_build_zh_locale.py index faa1b233..6aa581a1 100644 --- a/scripts/test_build_zh_locale.py +++ b/scripts/test_build_zh_locale.py @@ -82,6 +82,23 @@ class LocaleBuildTest(unittest.TestCase): self.assertEqual((root / "out.txt").read_bytes().decode("gbk"), "1\t金币\n2\t剑\n") self.assertEqual(result["overlaid"], 1) + def test_skilldesc_keeps_format_arguments(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + blank = [""] * 22 + en_row = ["20", "WARRIOR", "Sword Strike", "Bolt", "Hurl", "Attack.", "Long-Range", "Multi", "Faint"] + blank + en_row[17], en_row[20] = "Attack Power %.0f-%.0f", "Faint Chance %.0f%%" + zh_row = ["20", "WARRIOR", "剑风", "斩气决", "剑气冲霄", "快速挥动 |打击敌人", "远程攻击", "", "击晕"] + blank + zh_row[17] = "攻击力 %.0f-%.0f" + (root / "en.txt").write_bytes(("\t".join(en_row) + "\n").encode("cp1252")) + (root / "zh.txt").write_bytes(("\t".join(zh_row) + "\n").encode("gbk")) + (root / "overlay.txt").write_text("VNUM\tCOLUMN\tTEXT\n20\t7\t多目标攻击\n20\t20\t气绝 %.0f\n") + result = MODULE.merge_skilldesc(root / "en.txt", root / "zh.txt", root / "out.txt", root / "overlay.txt") + row = (root / "out.txt").read_bytes().decode("gbk").rstrip("\n").split("\t") + self.assertEqual(row[2:9], ["剑风", "斩气决", "剑气冲霄", "快速挥动 |打击敌人", "远程攻击", "多目标攻击", "击晕"]) + self.assertEqual((row[17], row[20]), ("攻击力 %.0f-%.0f", "Faint Chance %.0f%%")) + self.assertEqual(result["incompatible_format_cells"], ["20:20"]) + if __name__ == "__main__": unittest.main()