zh locale: pack locale_zh and add reviewed Chinese names
eterpack_pack writes locale packs in the original format (COMPRESS entries, LZO+TEA index), and build_zh_locale.py packs locale/zh into locale_zh.eix/.epk registered in pack/Index instead of leaving loose files. The reviewed translations for names and keys still in English are kept apart from the server snapshot in data/zh_40250/*_zh_overlay.txt (1,607 items, 231 mobs/NPCs, 45 locale_game keys) and override it at build time. Only YMIR, the coordinate-named warp markers and the empty TOOLTIP_UNSEAL_LEFT_TIME remain English. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fd051cc3ed
commit
a1ae69aa7a
@@ -11,5 +11,8 @@
|
||||
| `mob_names_zh.txt` | 1,334 | `fdbfe4b3f3367d5842c0e9bbe0b5e5ea2dbe99fb6e566ef5789f65d3794eab5c` |
|
||||
|
||||
线上字段里的 1,608 个物品名及 346 个怪物/NPC 名不含汉字,本快照原样保留。
|
||||
它们的审核译文(2026-09-28 确认)单独放在 `item_names_zh_overlay.txt`、
|
||||
`mob_names_zh_overlay.txt`,界面回退键译文在 `locale_game_zh_overlay.txt`;
|
||||
`script/build_zh_locale.py` 构建时以 overlay 覆盖快照同 VNUM / 同键的值。
|
||||
旧的龙驹中文表与线上表有 21 个相同 VNUM 不同名;不把旧表覆盖到服务端。
|
||||
服务端名称若再更新,需重新只读导出并核对差异后更新本快照。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
KEY VALUE
|
||||
CANNOT_USE 无法使用。
|
||||
CHANNEL_NOTIFY_FULL 该频道已满!请选择其他频道。
|
||||
DRAGON_SOUL_CANNOT_REFINE 该龙魂石无法强化。
|
||||
DRAGON_SOUL_CANNOT_REFINE_MORE 该龙魂石已达到最高等级。
|
||||
DRAGON_SOUL_EQUIP_WARNING1 龙魂石一旦装备便很难取下。
|
||||
DRAGON_SOUL_EQUIP_WARNING2 确定要继续吗?
|
||||
DRAGON_SOUL_EXPIRED 已过期的龙魂石
|
||||
DRAGON_SOUL_INVALID_DRAGON_SOUL 龙魂石不正确。
|
||||
DRAGON_SOUL_IS_NOT_DRAGON_SOUL 这不是龙魂石。
|
||||
DRAGON_SOUL_NOT_DRAGON_SOUL_REFINE_STONE 这不是正确的龙魂石。
|
||||
DRAGON_SOUL_NOT_ENOUGH_MATERIAL 强化材料不足。
|
||||
DRAGON_SOUL_NOT_MATCHED_SLOT 该龙魂石不适合这个栏位。
|
||||
DRAGON_SOUL_STEP_LEVEL1 阶段:最低级,
|
||||
DRAGON_SOUL_STEP_LEVEL2 阶段:低级,
|
||||
DRAGON_SOUL_STEP_LEVEL3 阶段:中级,
|
||||
DRAGON_SOUL_STEP_LEVEL4 阶段:高级,
|
||||
DRAGON_SOUL_STEP_LEVEL5 阶段:最高级,
|
||||
DRAGON_SOUL_STRENGTH 强化:+%d
|
||||
DRAGON_SOUL_UNEQUIP_WARNING1 取下时龙魂石可能会损坏。
|
||||
DRAGON_SOUL_UNEQUIP_WARNING2 确定要继续吗?
|
||||
DRAGON_SOUL_UNMATCHED_SLOT 栏位不正确。
|
||||
FISHING_NOTIFY1 好像钓到了%s。
|
||||
FISHING_NOTIFY2 %s好像上钩了。
|
||||
FISHING_SUCCESS1 钓到了%s!
|
||||
FISHING_SUCCESS2 把%s从水里拉上来了!
|
||||
GUILD_MARK_NOT_ENOUGH_LEVEL 帮会等级需达到3级
|
||||
HOW_MANY_ITEM_DO_YOU_DROP2 确定要扔掉%s %d个吗?
|
||||
LOGIN_FAILURE_BLOCK_LOGIN 请选择其他服务器!
|
||||
LOGIN_FAILURE_UNKNOWN 因未知原因无法登录。
|
||||
REFINE_FAILURE_CAN_NOT_ATTACH %s无法镶嵌到该物品上。
|
||||
REFINE_FAILURE_NO_GOLD_SOCKET 没有可镶嵌%s的金色插槽。
|
||||
REFINE_FAILURE_NO_SOCKET0 没有可镶嵌%s的插槽。
|
||||
TOOLTIP_ATT_SPEED 攻击速度 : +%d%%
|
||||
TOOLTIP_COSTUME_ATTR_BONUS 时装属性 %d%%
|
||||
TOOLTIP_DRAGON_SOUL_DECK1 地
|
||||
TOOLTIP_DRAGON_SOUL_DECK2 天
|
||||
TOOLTIP_HP_REGEN 生命恢复量 : +%d%%
|
||||
TOOLTIP_MOV_SPEED 移动速度 : %d%%
|
||||
TOOLTIP_RESIST_DARK 黑暗抗性 +%d%%
|
||||
TOOLTIP_RESIST_EARTH 大地抗性 +%d%%
|
||||
TOOLTIP_RESIST_ICE 冰冻抗性 +%d%%
|
||||
TOOLTIP_SKILL_DURATION 持续时间 : %d秒
|
||||
TOOLTIP_SP_REGEN 精力恢复量 : +%d%%
|
||||
TOOLTIP_TIME_CHARGER_FIX 时间充能 %d秒
|
||||
TOOLTIP_TIME_CHARGER_PER 时间充能 %d%%
|
||||
@@ -0,0 +1,232 @@
|
||||
VNUM LOCALE_NAME
|
||||
101 幼狼
|
||||
1096 亡灵战神
|
||||
1308 真狼神守护者
|
||||
1309 真狼神守护者
|
||||
1310 狼神守护者
|
||||
1904 真三尾银狐
|
||||
1905 真三尾银狐
|
||||
1906 真三尾银狐
|
||||
6001 火精
|
||||
6002 炎精
|
||||
6003 极热精
|
||||
6004 血火精
|
||||
6005 火魔
|
||||
6006 炎魔
|
||||
6007 火怪
|
||||
6008 炎怪
|
||||
6009 超热怪
|
||||
6051 真超热怪
|
||||
6091 夜摩天
|
||||
6101 冰精
|
||||
6102 冷精
|
||||
6103 极冰精
|
||||
6104 寒冰精
|
||||
6105 冰傀魔
|
||||
6106 冷魔
|
||||
6107 冰怪
|
||||
6108 冷怪
|
||||
6109 超冷怪
|
||||
6151 真超冷怪
|
||||
6191 寒魔
|
||||
7029 冰魔
|
||||
7030 冰魔鲸
|
||||
7031 冰异虫
|
||||
7032 雪地兽
|
||||
7033 冰魔人
|
||||
7034 雪魔兽人
|
||||
7035 冰雕魔
|
||||
7036 冰魔
|
||||
7037 冰魔鲸
|
||||
7038 冰异虫
|
||||
7039 雪地兽
|
||||
7040 冰魔人
|
||||
7041 雪魔兽人
|
||||
7042 冰雕魔
|
||||
7043 冰魔
|
||||
7044 冰魔鲸
|
||||
7045 冰异虫
|
||||
7046 雪地兽
|
||||
7047 冰魔人
|
||||
7048 雪魔兽人
|
||||
7049 冰雕魔
|
||||
7050 破坏者
|
||||
7051 蝎人王
|
||||
7052 蝎人
|
||||
7053 蝎人弓箭手
|
||||
7054 半兽刀兵
|
||||
7055 半兽弓箭手
|
||||
7056 裁决者
|
||||
7057 破坏者
|
||||
7058 蝎人王
|
||||
7059 蝎人
|
||||
7060 蝎人弓箭手
|
||||
7061 半兽刀兵
|
||||
7062 半兽弓箭手
|
||||
7063 裁决者
|
||||
7064 破坏者
|
||||
7065 蝎人王
|
||||
7066 蝎人
|
||||
7067 蝎人弓箭手
|
||||
7068 半兽刀兵
|
||||
7069 半兽弓箭手
|
||||
7070 裁决者
|
||||
7071 火云虎
|
||||
7072 火灵
|
||||
7073 焰赤虎
|
||||
7074 夜魔
|
||||
7075 赤皇武夫
|
||||
7076 火云虎
|
||||
7077 火灵
|
||||
7078 焰赤虎
|
||||
7079 夜魔
|
||||
7080 赤皇武夫
|
||||
7081 火云虎
|
||||
7082 火灵
|
||||
7083 焰赤虎
|
||||
7084 夜魔
|
||||
7085 赤皇武夫
|
||||
7086 天刹鬼木
|
||||
7087 青叶妖姬
|
||||
7088 狂暴树鬼
|
||||
7089 邪魔树鬼
|
||||
7090 天刹鬼木
|
||||
7091 青叶妖姬
|
||||
7092 狂暴树鬼
|
||||
7093 邪魔树鬼
|
||||
7094 天刹鬼木
|
||||
7095 青叶妖姬
|
||||
7096 狂暴树鬼
|
||||
8041 复活节陨石
|
||||
8042 复活节陨石
|
||||
8043 复活节陨石
|
||||
8044 复活节陨石
|
||||
8045 复活节陨石
|
||||
8046 复活节陨石
|
||||
8047 复活节陨石
|
||||
8048 复活节陨石
|
||||
8049 复活节陨石
|
||||
8050 复活节陨石
|
||||
8051 魂魔石
|
||||
8052 鬼魔石
|
||||
8053 暴魔石
|
||||
8054 龙魔石
|
||||
8055 地魔石
|
||||
8056 亡魔石
|
||||
8057 炼狱火魔石
|
||||
8101 鱼跃陨石
|
||||
8102 蝴仙陨石
|
||||
8103 鹿鸣陨石
|
||||
8104 鹰神陨石
|
||||
8105 盘蛇陨石
|
||||
8106 虎啸陨石
|
||||
8107 狮吼陨石
|
||||
8108 兽目陨石
|
||||
8109 凤凰陨石
|
||||
8110 麒麟陨石
|
||||
8111 蛟龙陨石
|
||||
8600 真鬼族守护神
|
||||
8601 真鬼族守护神
|
||||
8602 真秘宗教主
|
||||
8603 真秘宗教主
|
||||
8604 真亡灵战神
|
||||
8605 真亡灵战神
|
||||
8606 真死神
|
||||
8607 真死神
|
||||
8608 真三尾银狐
|
||||
8609 真三尾银狐
|
||||
8610 真狼神守护者
|
||||
8611 真狼神守护者
|
||||
8612 真圣母蛛
|
||||
8613 真圣母蛛
|
||||
8614 真龟龙王
|
||||
8615 真龟龙王
|
||||
20035 战旗
|
||||
20036 战旗
|
||||
20037 战旗
|
||||
20110 野猪
|
||||
20201 野猪坐骑
|
||||
20202 狼坐骑
|
||||
20203 虎坐骑
|
||||
20204 狮子坐骑
|
||||
20205 战甲野猪
|
||||
20206 战甲狼
|
||||
20207 战甲虎
|
||||
20208 战甲狮子
|
||||
20209 战甲野猪
|
||||
20210 战甲狼
|
||||
20211 战甲虎
|
||||
20212 战甲狮子
|
||||
20213 公驯鹿
|
||||
20214 战甲公驯鹿
|
||||
20215 战甲公驯鹿
|
||||
20216 母驯鹿
|
||||
20217 战甲母驯鹿
|
||||
20218 战甲母驯鹿
|
||||
20219 万圣黑马
|
||||
20367 墓穴守卫
|
||||
20368 铁算盘
|
||||
20369 李忠
|
||||
20370 中毒的士兵
|
||||
20371 古代石碑
|
||||
20372 残旧石碑
|
||||
20373 护卫
|
||||
20374 卫兵
|
||||
20375 木栅
|
||||
20376 蔡基雄
|
||||
20377 药还
|
||||
20378 警备队长岳穆尔
|
||||
20379 受伤的士兵
|
||||
20380 学者
|
||||
20381 村长
|
||||
20382 石花
|
||||
20383 工匠莫雷汉
|
||||
20384 圣诞树
|
||||
20385 赤龙城碑
|
||||
20386 赤龙封印碑
|
||||
20387 命运之锁
|
||||
20388 赤龙结界门
|
||||
20389 夜摩天石
|
||||
20390 赤龙封印石
|
||||
20391 太阳草
|
||||
20392 阵亡的士兵
|
||||
20393 负伤的士兵
|
||||
20394 火焰之地卫兵
|
||||
20395 冰原守卫
|
||||
30101 库德石像
|
||||
30102 玄武岩方尖碑
|
||||
30103 龟岩
|
||||
30104 符文桩
|
||||
30111 第一道地狱之门
|
||||
30112 第二道地狱之门
|
||||
30113 第三道地狱之门
|
||||
30114 第四道地狱之门
|
||||
30115 第五道地狱之门
|
||||
30116 第六道地狱之门
|
||||
30117 第七道地狱之门
|
||||
30118 第八道地狱之门
|
||||
30119 第九道地狱之门
|
||||
30120 药草袋
|
||||
30121 修罗之魂
|
||||
30122 猛将之魂
|
||||
30123 暗黑祭坛
|
||||
30124 修罗骸骨1
|
||||
30125 修罗骸骨2
|
||||
30126 修罗骸骨3
|
||||
30127 修罗骸骨4
|
||||
30128 修罗骸骨5
|
||||
30129 复活节兔子
|
||||
30130 墓碑
|
||||
30301 龙牙矿脉
|
||||
30302 红玉矿脉
|
||||
30303 碧玉矿脉
|
||||
30304 翡翠矿脉
|
||||
30305 青玉矿脉
|
||||
33001 修罗骸骨
|
||||
33007 饥民
|
||||
33008 南瓜杰克
|
||||
33009 龙牙炼圣
|
||||
33010 红玉炼圣
|
||||
33011 碧玉炼圣
|
||||
33012 翡翠炼圣
|
||||
33013 青玉炼圣
|
||||
+45
-20
@@ -1,22 +1,42 @@
|
||||
# 40250 简体中文适配(进行中)
|
||||
# 40250 简体中文适配
|
||||
|
||||
## 当前可运行原型
|
||||
|
||||
先运行 `cmake --build build-release --target locale_proto_patch`,再运行
|
||||
`python3 script/build_zh_locale.py`,在 `build/zh-locale/Client` 生成独立的测试客户端目录。
|
||||
它复制 40250 `Eternexus/locale_en/locale/en` 的文件结构,配置 `locale.cfg` 为
|
||||
`10002 936 zh`,并将中文内容按键合入 `locale_game.txt`、`locale_interface.txt`。
|
||||
旧龙驹语言包中的占位符与 40250 不一致时保留英文原文;报告在
|
||||
`build/zh-locale/zh-locale-report.json`。职业与帝国说明采用龙驹中文文本。
|
||||
|
||||
生成目录通过符号链接共用仓库 `Client` 的 pack 等资源,仅用于本机验证;
|
||||
不会修改原始 40250 客户端,也不是可分发包。可用以下命令启动:
|
||||
## 语言包(2026-09-28 起为正式 pack)
|
||||
|
||||
```sh
|
||||
build-release/native_render/mt_native_render \
|
||||
cmake --build build-native --target locale_proto_patch eterpack_pack
|
||||
python3 script/build_zh_locale.py
|
||||
```
|
||||
|
||||
生成器复制 40250 `Eternexus/locale_en/locale/en` 的文件结构到暂存目录
|
||||
`build/zh-locale/locale-src/zh`,将中文内容按键合入 `locale_game.txt`、`locale_interface.txt`。
|
||||
旧龙驹语言包缺失或占位符与 40250 不一致的键,改用 `data/zh_40250/locale_game_zh_overlay.txt`
|
||||
中经审核的译文(同样校验占位符);报告在
|
||||
`build/zh-locale/zh-locale-report.json`。职业与帝国说明采用龙驹中文文本。
|
||||
|
||||
暂存目录随后由 `eterpack_pack`(`extension/tests/eterpack_pack.cpp`,直接调用移植的 40250
|
||||
`CEterPack::Put` / `EncryptIndexFile`)打成 `locale_zh.eix/.epk`,格式与原版 `locale_en` 等
|
||||
相同:231 个文件全部为 COMPRESS(LZO),索引经 LZO + TEA 加密。工具写完后以只读方式
|
||||
重新打开 pack,逐个经 `CEterPack::Get` 读回并与源文件比对。用同一工具重打原版
|
||||
`locale_en` 源文件,文件名、类型、CRC 和解压内容与原版 pack 逐项一致。
|
||||
|
||||
输出客户端 `build/zh-locale/Client`:
|
||||
|
||||
- `pack/`:原版 pack 逐个符号链接,加上 `locale_zh.eix/.epk`,以及在 `locale/en/` 之前
|
||||
注册 `locale/zh/` → `locale_zh` 的 `Index`(CRLF,与原版一致)。
|
||||
- `locale.cfg` 与 `locale_zh.cfg`:`10002 936 zh`。
|
||||
- 不留散装的 `locale/zh`:客户端默认先查散装文件,留着会绕过 pack。
|
||||
|
||||
仓库 `Client/` 是 `audit/assets/40250-client.json` 校验的原版资源,不做修改。
|
||||
macOS 启动:
|
||||
|
||||
```sh
|
||||
build-native/native_render/mt_native_render \
|
||||
--live-client build/zh-locale/Client --login-screen --width 1024 --height 768
|
||||
```
|
||||
|
||||
Android:`./push-android-client.sh --locale zh`(默认推送 `build/zh-locale/Client`,符号链接按
|
||||
目标文件复制,并以 `locale_zh.cfg` 写入设备上的 `locale.cfg`)。
|
||||
|
||||
物品与怪物原型从 40250 原版解压,按线上 40250 服务端 `gb2312name` 的 VNUM 快照
|
||||
只替换 `szLocaleName` 字段后按原格式
|
||||
重新压缩加密;生成器验证记录数、结构大小、除名称外所有字节不变,以及写出后的
|
||||
@@ -25,11 +45,15 @@ build-release/native_render/mt_native_render \
|
||||
|
||||
## 已核对的差异
|
||||
|
||||
- 40250 `locale_game.txt` 有 817 个键;当前 771 个安全替换为中文,46 个保留英文。
|
||||
其中 12 个在龙驹文件中的格式占位符不一致,需逐条重译或验证。
|
||||
- 40250 `locale_game.txt` 有 817 个键;771 个来自龙驹,45 个来自审核译文,
|
||||
仅 `TOOLTIP_UNSEAL_LEFT_TIME` 保留英文原文(原文为空,40250 脚本未使用)。
|
||||
- 40250 `locale_interface.txt` 的 326 个键均找到中文对应项。
|
||||
- 40250 客户端物品原型 5,743 条、怪物/NPC 原型 1,334 条都已按 VNUM 写入
|
||||
`data/zh_40250/` 中的服务端名称快照;其中分别有 1,608 和 346 个名称仍不含汉字。
|
||||
`data/zh_40250/` 中的服务端名称快照;快照里分别有 1,608 和 346 个名称不含汉字。
|
||||
2026-09-28 审核确认的译文放在 `item_names_zh_overlay.txt`(1,607 条)和
|
||||
`mob_names_zh_overlay.txt`(231 条),构建时覆盖快照同 VNUM 的名称;快照本身保持服务端原样。
|
||||
剩余英文仅为 vnum 2 占位物品 `YMIR` 和 115 个名称为地图坐标的传送点标记(游戏内不显示)。
|
||||
客户端显示译名,服务端数据库仍为英文,服务端发出的含名称消息会显示英文。
|
||||
- 40250 服务端(`192.168.21.203`,2026-09-27 只读 SSH 核对)当前数据库配置为
|
||||
`LOCALE=newcibn`、`DB_NAME_COLUMN=gb2312name`,有 `newcibn` 语言目录。
|
||||
因此客户端采用 CP936;服务端广播字典已在 2026-09-28 完成首轮补齐,名称与任务文本仍需核对。
|
||||
@@ -40,12 +64,13 @@ build-release/native_render/mt_native_render \
|
||||
|
||||
## 后续验收
|
||||
|
||||
1. 根据线上 40250 VNUM 及用途逐条翻译仍为英文的 1,608 个物品、346 个怪物/NPC;
|
||||
同步更新服务端名称和客户端快照。`itemdesc`、`skilldesc` 也需按 40250 键和语义逐项合并。
|
||||
2. 补齐 46 个英文回退键,复核 12 个格式不兼容键。
|
||||
1. 物品/怪物译名和界面回退键已在客户端补齐(overlay);是否同步写入服务端
|
||||
`gb2312name` 需另行核对后决定。`itemdesc`、`skilldesc` 仍需按 40250 键和语义逐项合并。
|
||||
2. (已完成)46 个英文回退键已补齐。
|
||||
3. 在游戏窗口验证中文字符、字体、输入法、换行和 UI 宽度,再联机检查登录、背包、
|
||||
商店、任务、公告及聊天。
|
||||
4. 完成后制作注册到 `pack/Index` 的中文 pack,并在 macOS 和 Android 上分别测试。
|
||||
4. 中文 pack 已注册到 `pack/Index`(见上);macOS 登录界面已确认从 pack 读取中文,
|
||||
Android 真机/模拟器待测。
|
||||
|
||||
## 服务端适配进度(2026-09-28)
|
||||
|
||||
|
||||
@@ -238,6 +238,9 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
|
||||
add_executable(locale_proto_patch ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/locale_proto_patch.cpp)
|
||||
target_link_libraries(locale_proto_patch PRIVATE port_platform)
|
||||
# Locale packs in the original format (script/build_zh_locale.py packs locale/zh with it).
|
||||
add_executable(eterpack_pack ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/eterpack_pack.cpp)
|
||||
target_link_libraries(eterpack_pack PRIVATE port_platform)
|
||||
# 2V2-e: the same fake server as a process, for the Godot-driven render run
|
||||
# (script/python_game_render_test.sh).
|
||||
add_executable(port_fake_login_server ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_fake_login_server_main.cpp
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// eterpack_pack — build a 40250 pack (<db>.eix/.epk) from a directory with the ported CEterPack, the way
|
||||
// the original locale packs are made: every file COMPRESS (LZO), index LZO + TEA encrypted.
|
||||
//
|
||||
// eterpack_pack <db path without extension> <source dir> <pack prefix, e.g. locale/zh/>
|
||||
//
|
||||
// Pack names are <prefix><path relative to the source dir>, lowercased with '/' (inlineConvertPackFilename).
|
||||
// An existing <db>.eix/.epk is replaced. The result is reopened read-only and every file read back
|
||||
// through CEterPack::Get and compared with its source.
|
||||
#include "EterPack/StdAfx.h"
|
||||
#include "EterPack/EterPack.h"
|
||||
#include "EterPack/EterPackManager.h"
|
||||
#include "EterBase/MappedFile.h"
|
||||
#include "../src/platform/UserInterface/UserInterface.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::vector<char> read_file(const fs::path& path)
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc != 4)
|
||||
{
|
||||
std::fprintf(stderr, "usage: %s <db> <source dir> <pack prefix>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const std::string db = argv[1];
|
||||
const fs::path source = argv[2];
|
||||
std::string prefix = argv[3];
|
||||
if (!prefix.empty() && prefix.back() != '/')
|
||||
prefix += '/';
|
||||
|
||||
std::vector<std::pair<std::string, fs::path>> files;
|
||||
for (const auto& e : fs::recursive_directory_iterator(source))
|
||||
if (e.is_regular_file())
|
||||
files.emplace_back(prefix + e.path().lexically_relative(source).generic_string(), e.path());
|
||||
std::sort(files.begin(), files.end());
|
||||
if (files.empty())
|
||||
{
|
||||
std::fprintf(stderr, "no files under %s\n", source.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
PackSingletons();
|
||||
std::error_code ec;
|
||||
fs::remove(db + ".eix", ec);
|
||||
fs::remove(db + ".epk", ec);
|
||||
{
|
||||
CEterFileDict dict;
|
||||
CEterPack pack;
|
||||
if (!pack.Create(dict, db.c_str(), "", false))
|
||||
{
|
||||
std::fprintf(stderr, "cannot create %s.eix/.epk\n", db.c_str());
|
||||
return 1;
|
||||
}
|
||||
for (const auto& [name, path] : files)
|
||||
{
|
||||
const std::vector<char> data = read_file(path);
|
||||
if (name.size() > FILENAME_MAX_LEN || !pack.Put(name.c_str(), data.data(), long(data.size()), COMPRESSED_TYPE_COMPRESS))
|
||||
{
|
||||
std::fprintf(stderr, "cannot put %s\n", name.c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (!pack.EncryptIndexFile())
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Read back as the client does.
|
||||
CEterFileDict dict;
|
||||
CEterPack pack;
|
||||
if (!pack.Create(dict, db.c_str(), prefix.c_str(), true))
|
||||
{
|
||||
std::fprintf(stderr, "cannot reopen %s\n", db.c_str());
|
||||
return 1;
|
||||
}
|
||||
int failures = 0;
|
||||
for (const auto& [name, path] : files)
|
||||
{
|
||||
const std::vector<char> want = read_file(path);
|
||||
CMappedFile mapped;
|
||||
LPCVOID data = nullptr;
|
||||
if (!pack.Get(mapped, name.c_str(), &data) || mapped.Size() != want.size() ||
|
||||
(!want.empty() && std::memcmp(data, want.data(), want.size()) != 0))
|
||||
{
|
||||
std::fprintf(stderr, "read-back mismatch: %s\n", name.c_str());
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
if (failures)
|
||||
return 1;
|
||||
std::printf("%s: %zu files, COMPRESS, encrypted index\n", db.c_str(), files.size());
|
||||
return 0;
|
||||
}
|
||||
+97
-21
@@ -3,6 +3,12 @@
|
||||
|
||||
The Longju locale is an older client version. Only keyed UI text with compatible
|
||||
format arguments is overlaid; 40250 proto, scripts, and UI layouts stay intact.
|
||||
|
||||
The files are staged in build/zh-locale/locale-src/zh and packed like the original
|
||||
locale packs (eterpack_pack: COMPRESS entries, encrypted index) into locale_zh.eix/.epk.
|
||||
The output Client has a pack/ of its own: links to the original packs, the zh pack and
|
||||
an Index registering locale/zh/ -> locale_zh. No loose locale/zh is left next to it,
|
||||
since the client searches loose files first.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -17,7 +23,8 @@ REPO = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_EN = REPO.parent / "40250/Server Client TMP4/Client/Eternexus/locale_en/locale/en"
|
||||
DEFAULT_ZH = REPO.parent / "server_extracted/client_locale/longju/locale/taiwan"
|
||||
DEFAULT_OUTPUT = REPO / "build/zh-locale/Client"
|
||||
DEFAULT_PROTO_TOOL = REPO / "build-release/extension/src/port/locale_proto_patch"
|
||||
BUILD_DIRS = ("build-release", "build-native")
|
||||
LOCALE_CFG = "10002 936 zh\n"
|
||||
DEFAULT_NAMES = REPO / "data/zh_40250"
|
||||
FORMAT = re.compile(r"%(?:\([^)]*\))?[-+#0 ]*\d*(?:\.\d+)?[diouxXeEfFgGcrs%]")
|
||||
KEYED_FILES = ("locale_game.txt", "locale_interface.txt")
|
||||
@@ -33,35 +40,99 @@ def keyed_lines(path: Path, encoding: str):
|
||||
return lines, values
|
||||
|
||||
|
||||
def convert_names(source: Path, destination: Path):
|
||||
def name_rows(path: Path):
|
||||
rows = {}
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
parts = line.split("\t", 1)
|
||||
if len(parts) == 2 and parts[0].isdigit():
|
||||
rows[parts[0]] = parts[1]
|
||||
return rows
|
||||
|
||||
|
||||
def convert_names(source: Path, destination: Path, overlay: Path = None):
|
||||
"""The server name snapshot, with the reviewed translations of its English names laid over it."""
|
||||
names = name_rows(source)
|
||||
overlaid = {vnum: name for vnum, name in name_rows(overlay).items() if vnum in names} if overlay else {}
|
||||
names.update(overlaid)
|
||||
converted = 0
|
||||
unrepresentable = []
|
||||
rows = []
|
||||
for line in source.read_text(encoding="utf-8").splitlines():
|
||||
parts = line.split("\t", 1)
|
||||
if len(parts) != 2 or not parts[0].isdigit():
|
||||
continue
|
||||
for vnum, name in names.items():
|
||||
try:
|
||||
rows.append(f"{parts[0]}\t{parts[1]}\n".encode("gbk"))
|
||||
rows.append(f"{vnum}\t{name}\n".encode("gbk"))
|
||||
converted += 1
|
||||
except UnicodeEncodeError:
|
||||
unrepresentable.append(parts[0])
|
||||
unrepresentable.append(vnum)
|
||||
destination.write_bytes(b"".join(rows))
|
||||
return {"converted": converted, "unrepresentable_vnums": unrepresentable}
|
||||
return {"converted": converted, "overlaid": len(overlaid), "unrepresentable_vnums": unrepresentable}
|
||||
|
||||
|
||||
def tool(name: str) -> Path:
|
||||
for build_dir in BUILD_DIRS:
|
||||
path = REPO / build_dir / "extension/src/port" / name
|
||||
if path.is_file():
|
||||
return path
|
||||
return REPO / BUILD_DIRS[0] / "extension/src/port" / name
|
||||
|
||||
|
||||
def register(index: bytes) -> bytes:
|
||||
"""pack/Index with locale/zh/ -> locale_zh added before the locale/en/ pair (CRLF, as the original)."""
|
||||
lines = index.split(b"\r\n")
|
||||
if b"locale/zh/" in lines:
|
||||
return index
|
||||
at = lines.index(b"locale/en/")
|
||||
if at % 2 != 1:
|
||||
raise ValueError("pack/Index: locale/en/ is not a folder line")
|
||||
lines[at:at] = [b"locale/zh/", b"locale_zh"]
|
||||
return b"\r\n".join(lines)
|
||||
|
||||
|
||||
def make_pack_dir(client_base: Path, pack_dir: Path, locale_dir: Path, pack_tool: Path):
|
||||
source = client_base / "pack"
|
||||
if pack_dir.is_symlink():
|
||||
pack_dir.unlink() # earlier builds linked the whole directory
|
||||
pack_dir.mkdir(parents=True, exist_ok=True)
|
||||
for entry in source.iterdir():
|
||||
if entry.name == "Index" or entry.name.lower().startswith("locale_zh."):
|
||||
continue
|
||||
target = pack_dir / entry.name
|
||||
if target.is_symlink() or target.exists():
|
||||
target.unlink()
|
||||
target.symlink_to(entry.resolve())
|
||||
(pack_dir / "Index").write_bytes(register((source / "Index").read_bytes()))
|
||||
result = subprocess.run([str(pack_tool), str(pack_dir / "locale_zh"), str(locale_dir), "locale/zh/"],
|
||||
check=True, capture_output=True, text=True)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def build(en: Path, zh: Path, output: Path, client_base: Path,
|
||||
proto_tool: Path = None, names_dir: Path = None):
|
||||
proto_tool: Path = None, names_dir: Path = None, pack_tool: Path = None):
|
||||
"""names_dir holds the server name snapshot and the reviewed *_zh_overlay.txt translations."""
|
||||
if not (en / "locale_game.txt").is_file() or not (zh / "locale_game.txt").is_file():
|
||||
raise ValueError("English 40250 and Chinese Longju locale directories are required")
|
||||
if pack_tool is None or not pack_tool.is_file():
|
||||
raise ValueError("build eterpack_pack (cmake --build <build dir> --target eterpack_pack)")
|
||||
|
||||
locale_dir = output / "locale/zh"
|
||||
shutil.copytree(en, locale_dir, dirs_exist_ok=True)
|
||||
locale_dir = output.parent / "locale-src/zh"
|
||||
if locale_dir.exists():
|
||||
shutil.rmtree(locale_dir)
|
||||
shutil.copytree(en, locale_dir)
|
||||
loose = output / "locale/zh"
|
||||
if loose.exists():
|
||||
shutil.rmtree(loose) # earlier builds ran the locale from loose files
|
||||
if not any((output / "locale").iterdir()):
|
||||
(output / "locale").rmdir()
|
||||
report = {"english_source": str(en), "chinese_source": str(zh), "files": {}}
|
||||
|
||||
for filename in KEYED_FILES:
|
||||
english_lines, english = keyed_lines(en / filename, "cp1252")
|
||||
_, chinese = keyed_lines(zh / filename, "gbk")
|
||||
# Reviewed translations of the keys Longju lacks or formats differently; they win over Longju.
|
||||
overlay = names_dir / (Path(filename).stem + "_zh_overlay.txt") if names_dir else None
|
||||
if overlay and overlay.is_file():
|
||||
_, reviewed = keyed_lines(overlay, "utf-8")
|
||||
chinese.update(reviewed)
|
||||
translated = []
|
||||
incompatible = []
|
||||
merged = []
|
||||
@@ -100,8 +171,10 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
|
||||
raise ValueError("build locale_proto_patch and provide the Chinese name tables")
|
||||
item_names = output.parent / "item_names_gbk.txt"
|
||||
mob_names = output.parent / "mob_names_gbk.txt"
|
||||
item_conversion = convert_names(names_dir / "item_names_zh.txt", item_names)
|
||||
mob_conversion = convert_names(names_dir / "mob_names_zh.txt", mob_names)
|
||||
item_conversion = convert_names(names_dir / "item_names_zh.txt", item_names,
|
||||
names_dir / "item_names_zh_overlay.txt")
|
||||
mob_conversion = convert_names(names_dir / "mob_names_zh.txt", mob_names,
|
||||
names_dir / "mob_names_zh_overlay.txt")
|
||||
result = subprocess.run(
|
||||
[str(proto_tool), str(en / "item_proto"), str(en / "mob_proto"),
|
||||
str(item_names), str(mob_names), str(locale_dir / "item_proto"),
|
||||
@@ -114,10 +187,12 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
|
||||
"patch_result": result.stdout.strip().splitlines(),
|
||||
}
|
||||
|
||||
(output / "locale.cfg").write_text("10002 936 zh\n", encoding="ascii")
|
||||
# A local test checkout can share the original 40250 packs without copying
|
||||
# gigabytes of assets. Distribution builds must package these separately.
|
||||
for name in ("pack", "sound", "BGM", "miles", "lib", "channel.inf"):
|
||||
report["pack"] = make_pack_dir(client_base, output / "pack", locale_dir, pack_tool)
|
||||
(output / "locale.cfg").write_text(LOCALE_CFG, encoding="ascii")
|
||||
(output / "locale_zh.cfg").write_text(LOCALE_CFG, encoding="ascii")
|
||||
# A local test checkout shares the original 40250 assets through symbolic links
|
||||
# (pack/ holds one per original pack); push-android-client.sh copies their targets.
|
||||
for name in ("sound", "BGM", "miles", "lib", "channel.inf"):
|
||||
source = client_base / name
|
||||
target = output / name
|
||||
if not source.exists():
|
||||
@@ -141,12 +216,13 @@ def main():
|
||||
parser.add_argument("--chinese", type=Path, default=DEFAULT_ZH)
|
||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||
parser.add_argument("--client-base", type=Path, default=REPO / "Client")
|
||||
parser.add_argument("--proto-tool", type=Path, default=DEFAULT_PROTO_TOOL)
|
||||
parser.add_argument("--proto-tool", type=Path, default=tool("locale_proto_patch"))
|
||||
parser.add_argument("--pack-tool", type=Path, default=tool("eterpack_pack"))
|
||||
parser.add_argument("--name-tables", type=Path, default=DEFAULT_NAMES)
|
||||
args = parser.parse_args()
|
||||
report_path = build(args.english, args.chinese, args.output, args.client_base,
|
||||
args.proto_tool, args.name_tables)
|
||||
print(f"Built {args.output / 'locale/zh'}")
|
||||
args.proto_tool, args.name_tables, args.pack_tool)
|
||||
print(f"Built {args.output / 'pack/locale_zh.eix'} (.epk), registered in pack/Index")
|
||||
print(f"Report: {report_path}")
|
||||
|
||||
|
||||
|
||||
@@ -27,18 +27,55 @@ class LocaleBuildTest(unittest.TestCase):
|
||||
(en / "item_proto").write_bytes(b"40250-proto")
|
||||
(zh / "item_proto").write_bytes(b"old-proto")
|
||||
(client / "pack").mkdir()
|
||||
(client / "pack/Index").write_bytes(b"PACK\r\nlocale/de/\r\nlocale_de\r\nlocale/en/\r\nlocale_en\r\n")
|
||||
(client / "pack/locale_en.eix").write_bytes(b"en")
|
||||
# Stands in for eterpack_pack: records its arguments as the .eix.
|
||||
pack_tool = root / "eterpack_pack"
|
||||
pack_tool.write_text("#!/bin/sh\necho \"$2 $3\" > \"$1.eix\"\n")
|
||||
pack_tool.chmod(0o755)
|
||||
|
||||
report_path = MODULE.build(en, zh, output, client)
|
||||
merged = (output / "locale/zh/locale_game.txt").read_bytes().decode("gbk")
|
||||
report_path = MODULE.build(en, zh, output, client, pack_tool=pack_tool)
|
||||
staged = output.parent / "locale-src/zh"
|
||||
merged = (staged / "locale_game.txt").read_bytes().decode("gbk")
|
||||
self.assertIn("GOOD\t中文 %d\tSA", merged)
|
||||
self.assertIn("BAD\t%s at %d\tSA", merged)
|
||||
self.assertIn("ONLY_EN\tFallback", merged)
|
||||
self.assertEqual((output / "locale/zh/item_proto").read_bytes(), b"40250-proto")
|
||||
self.assertEqual((staged / "item_proto").read_bytes(), b"40250-proto")
|
||||
self.assertEqual((output / "locale.cfg").read_text(), "10002 936 zh\n")
|
||||
self.assertTrue((output / "pack").is_symlink())
|
||||
self.assertEqual((output / "locale_zh.cfg").read_text(), "10002 936 zh\n")
|
||||
# A pack/ of its own: original packs linked, locale_zh packed from the staging dir and registered.
|
||||
self.assertFalse((output / "pack").is_symlink())
|
||||
self.assertFalse((output / "locale/zh").exists())
|
||||
self.assertTrue((output / "pack/locale_en.eix").is_symlink())
|
||||
self.assertEqual((output / "pack/locale_zh.eix").read_text(), f"{staged} locale/zh/\n")
|
||||
self.assertEqual((output / "pack/Index").read_bytes(),
|
||||
b"PACK\r\nlocale/de/\r\nlocale_de\r\nlocale/zh/\r\nlocale_zh\r\n"
|
||||
b"locale/en/\r\nlocale_en\r\n")
|
||||
self.assertEqual((client / "pack/Index").read_bytes()[:4], b"PACK")
|
||||
self.assertFalse((client / "pack/locale_zh.eix").exists())
|
||||
report = json.loads(report_path.read_text())
|
||||
self.assertEqual(report["files"]["locale_game.txt"]["incompatible_format_keys"], ["BAD"])
|
||||
|
||||
# Reviewed overlay: fills an English-only key and replaces a format-incompatible Longju value.
|
||||
names = root / "names"
|
||||
names.mkdir()
|
||||
(names / "locale_game_zh_overlay.txt").write_text("KEY\tVALUE\nBAD\t%s 于 %d\nONLY_EN\t备用\n")
|
||||
report_path = MODULE.build(en, zh, output, client, names_dir=names, pack_tool=pack_tool)
|
||||
merged = (staged / "locale_game.txt").read_bytes().decode("gbk")
|
||||
self.assertIn("BAD\t%s 于 %d\tSA", merged)
|
||||
self.assertIn("ONLY_EN\t备用", merged)
|
||||
game = json.loads(report_path.read_text())["files"]["locale_game.txt"]
|
||||
self.assertEqual((game["english_fallback_keys"], game["incompatible_format_keys"]), ([], []))
|
||||
|
||||
def test_name_overlay_replaces_snapshot_rows(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "names.txt").write_text("VNUM\tLOCALE_NAME\n1\tYang\n2\t剑\n")
|
||||
(root / "overlay.txt").write_text("VNUM\tLOCALE_NAME\n1\t金币\n99\t不存在\n")
|
||||
result = MODULE.convert_names(root / "names.txt", root / "out.txt", root / "overlay.txt")
|
||||
self.assertEqual((root / "out.txt").read_bytes().decode("gbk"), "1\t金币\n2\t剑\n")
|
||||
self.assertEqual(result["overlaid"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user