Add 小狩猎套装礼包: level-1 +9 set with full attributes, sold in the item mall

Server (tools/server/hunt-set, deployed on 203): game patch adds quest function
item.set_attribute (CItem::SetForceAttribute); quest hunt_set turns the unused
ITEM_QUEST 50307 into a per-class box of eight +9 pieces with five max-value
attributes each. Item mall sells it for 2,000,000.

Client locale: name overlay plus whole-row overlays for item_list.txt (gift-box
icon) and itemdesc.txt (build_zh_locale.py ROW_FILES).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 10:23:33 +09:00
co-authored by Claude Opus 5.5
parent 20b189a9da
commit 7155785167
11 changed files with 276 additions and 0 deletions
+29
View File
@@ -33,6 +33,9 @@ KEYED_FILES = ("locale_game.txt", "locale_interface.txt")
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)
# vnum-keyed 40250 files whose rows an overlay (<stem>_zh_overlay.txt) replaces whole: items the server
# gives a new use (tools/server/hunt-set) get a new icon and description.
ROW_FILES = ("item_list.txt", "itemdesc.txt")
def keyed_lines(path: Path, encoding: str):
@@ -73,6 +76,27 @@ def convert_names(source: Path, destination: Path, overlay: Path = None):
return {"converted": converted, "overlaid": len(overlaid), "unrepresentable_vnums": unrepresentable}
def overlay_rows(path: Path, overlay: Path):
"""Replace the rows of a vnum-keyed file by the overlay's (UTF-8, whole rows, no header); other rows keep
their bytes (the English files are cp1252)."""
rows = {}
for line in overlay.read_text(encoding="utf-8").splitlines():
if line and not line.startswith("#"):
rows[line.split("\t", 1)[0]] = line.encode("gbk")
lines = path.read_bytes().split(b"\n")
replaced = []
for index, line in enumerate(lines):
key = line.split(b"\t", 1)[0].decode("ascii", "replace")
if key in rows:
lines[index] = rows[key] + (b"\r" if line.endswith(b"\r") else b"")
replaced.append(key)
missing = sorted(rows.keys() - set(replaced))
if missing:
raise ValueError(f"{path.name}: overlay rows {missing} not in the file")
path.write_bytes(b"\n".join(lines))
return {"replaced_rows": replaced}
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."""
@@ -212,6 +236,11 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
en / "skilldesc.txt", zh / "skilldesc.txt", locale_dir / "skilldesc.txt",
names_dir / "skilldesc_zh_overlay.txt" if names_dir else None)
for filename in ROW_FILES:
overlay = names_dir / (Path(filename).stem + "_zh_overlay.txt") if names_dir else None
if overlay and overlay.is_file():
report["files"][filename] = overlay_rows(locale_dir / filename, overlay)
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()}
+13
View File
@@ -82,6 +82,19 @@ 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_row_overlay_replaces_whole_rows(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "itemdesc.txt").write_bytes(b"50306\tBook\tCaf\xe9.\r\n50307\tBook\tEasy.\r\n")
(root / "overlay.txt").write_text("# vnum\tname\tdescription\n50307\t礼包\t打开。\n")
result = MODULE.overlay_rows(root / "itemdesc.txt", root / "overlay.txt")
self.assertEqual((root / "itemdesc.txt").read_bytes(),
b"50306\tBook\tCaf\xe9.\r\n" + "50307\t礼包\t打开。\r\n".encode("gbk"))
self.assertEqual(result, {"replaced_rows": ["50307"]})
(root / "overlay.txt").write_text("99\tx\ty\n")
with self.assertRaises(ValueError):
MODULE.overlay_rows(root / "itemdesc.txt", root / "overlay.txt")
def test_skilldesc_keeps_format_arguments(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)