layout: restructure into src/ tests/ android/ scripts/ tools/
- extension/src/{port,platform,codepage} -> src/; native_render -> src/host
(+ dxt, shaders/); libgr2 -> src/gr2; extension/third_party -> third_party
- extension/tests -> tests/port, libgr2/tests -> tests/gr2
- android-native -> android (build.sh, push-client.sh moved in)
- script -> scripts; tools/40250 -> tools/server; oracle -> tools/granny-oracle;
perf tools -> tools/perf
- all build trees under build/ (native, release, android, port-gate)
- xrender:: CMake aliases -> mt::; port-map ledger impl paths rewritten
No code changes. ctest 15/15, port_gate macos+android PASS, port_map check 0 errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
70710477cf
commit
a46093104c
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a CP936 Chinese locale on top of the 40250 English client resources.
|
||||
|
||||
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
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
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"
|
||||
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")
|
||||
|
||||
|
||||
def keyed_lines(path: Path, encoding: str):
|
||||
lines = path.read_bytes().decode(encoding).splitlines()
|
||||
values = {}
|
||||
for line in lines:
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2 and parts[0]:
|
||||
values[parts[0]] = parts[1]
|
||||
return lines, values
|
||||
|
||||
|
||||
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 vnum, name in names.items():
|
||||
try:
|
||||
rows.append(f"{vnum}\t{name}\n".encode("gbk"))
|
||||
converted += 1
|
||||
except UnicodeEncodeError:
|
||||
unrepresentable.append(vnum)
|
||||
destination.write_bytes(b"".join(rows))
|
||||
return {"converted": converted, "overlaid": len(overlaid), "unrepresentable_vnums": unrepresentable}
|
||||
|
||||
|
||||
def tool(name: str) -> Path:
|
||||
for build_dir in BUILD_DIRS:
|
||||
path = REPO / build_dir / "src/port" / name
|
||||
if path.is_file():
|
||||
return path
|
||||
return REPO / BUILD_DIRS[0] / "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, 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.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 = []
|
||||
for line in english_lines:
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2 and parts[0] in chinese:
|
||||
key = parts[0]
|
||||
candidate = chinese[key]
|
||||
if FORMAT.findall(parts[1]) == FORMAT.findall(candidate):
|
||||
parts[1] = candidate
|
||||
translated.append(key)
|
||||
else:
|
||||
incompatible.append(key)
|
||||
line = "\t".join(parts)
|
||||
merged.append(line)
|
||||
(locale_dir / filename).write_bytes(("\n".join(merged) + "\n").encode("gbk"))
|
||||
report["files"][filename] = {
|
||||
"reference_keys": len(english),
|
||||
"translated_keys": len(translated),
|
||||
"english_fallback_keys": sorted(english.keys() - set(translated)),
|
||||
"incompatible_format_keys": sorted(incompatible),
|
||||
"chinese_only_keys": sorted(chinese.keys() - english.keys()),
|
||||
}
|
||||
|
||||
# These files are prose, not versioned game data. Keep the 40250 UI layouts
|
||||
# and prototype binaries copied above; only replace their explanatory text.
|
||||
for filename in (
|
||||
"jobdesc_warrior.txt", "jobdesc_assassin.txt", "jobdesc_sura.txt",
|
||||
"jobdesc_shaman.txt", "empiredesc_a.txt", "empiredesc_b.txt", "empiredesc_c.txt",
|
||||
):
|
||||
if (zh / filename).is_file():
|
||||
shutil.copyfile(zh / filename, locale_dir / filename)
|
||||
|
||||
if proto_tool is not None:
|
||||
if not proto_tool.is_file() or names_dir is None:
|
||||
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,
|
||||
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"),
|
||||
str(locale_dir / "mob_proto")],
|
||||
check=True, capture_output=True, text=True,
|
||||
)
|
||||
report["proto"] = {
|
||||
"item_name_source": item_conversion,
|
||||
"mob_name_source": mob_conversion,
|
||||
"patch_result": result.stdout.strip().splitlines(),
|
||||
}
|
||||
|
||||
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); android/push-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():
|
||||
continue
|
||||
if target.is_symlink():
|
||||
if target.resolve() == source.resolve():
|
||||
continue
|
||||
target.unlink()
|
||||
elif target.exists():
|
||||
raise ValueError(f"refusing to replace existing client resource: {target}")
|
||||
target.symlink_to(source.resolve())
|
||||
report_path = output.parent / "zh-locale-report.json"
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return report_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--english", type=Path, default=DEFAULT_EN)
|
||||
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=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, args.pack_tool)
|
||||
print(f"Built {args.output / 'pack/locale_zh.eix'} (.epk), registered in pack/Index")
|
||||
print(f"Report: {report_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user