From 715578516769228b9b5cbc383678a5ebc926e8a8 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 30 Sep 2026 10:23:33 +0900 Subject: [PATCH] =?UTF-8?q?Add=20=E5=B0=8F=E7=8B=A9=E7=8C=8E=E5=A5=97?= =?UTF-8?q?=E8=A3=85=E7=A4=BC=E5=8C=85:=20level-1=20+9=20set=20with=20full?= =?UTF-8?q?=20attributes,=20sold=20in=20the=20item=20mall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- data/zh_40250/README.md | 6 ++ data/zh_40250/item_list_zh_overlay.txt | 2 + data/zh_40250/item_names_zh_overlay.txt | 1 + data/zh_40250/itemdesc_zh_overlay.txt | 2 + scripts/build_zh_locale.py | 29 +++++++ scripts/test_build_zh_locale.py | 13 ++++ tools/server/hunt-set/README.md | 54 +++++++++++++ tools/server/hunt-set/hunt_set.quest | 88 ++++++++++++++++++++++ tools/server/hunt-set/install.sh | 47 ++++++++++++ tools/server/hunt-set/server.patch | 33 ++++++++ tools/server/npc-shops/item-mall/items.tsv | 1 + 11 files changed, 276 insertions(+) create mode 100644 data/zh_40250/item_list_zh_overlay.txt create mode 100644 data/zh_40250/itemdesc_zh_overlay.txt create mode 100644 tools/server/hunt-set/README.md create mode 100644 tools/server/hunt-set/hunt_set.quest create mode 100644 tools/server/hunt-set/install.sh create mode 100644 tools/server/hunt-set/server.patch diff --git a/data/zh_40250/README.md b/data/zh_40250/README.md index c8053f12..5f36ef44 100644 --- a/data/zh_40250/README.md +++ b/data/zh_40250/README.md @@ -38,3 +38,9 @@ 以及效果列(17/20/23)。客户端把效果列当作 `_snprintf` 格式串传入最小值、最大值,所以效果列只有在格式参数与英文一致时才替换。 `skilldesc_zh_overlay.txt` 按 `VNUM\t列号\t译文` 覆盖龙驹缺的或者不对的格。例如 126–128 在龙驹里叫吴/蜀/魏国方言, 这里改成盛唐语、秦皇语、汉武语;骑马技能的说明补上等级要求。 + +## 整行覆盖(item_list.txt、itemdesc.txt) + +`item_list_zh_overlay.txt`、`itemdesc_zh_overlay.txt` 每行是目标文件的一整行(UTF-8,第一列 VNUM), +`build_zh_locale.py` 用它替换同 VNUM 的行(`ROW_FILES`),其余行字节不变;VNUM 不存在就报错。 +用于服务端改了用途的物品,例如 50307 小狩猎套装礼包(`tools/server/hunt-set`)换成礼盒图标和中文说明。 diff --git a/data/zh_40250/item_list_zh_overlay.txt b/data/zh_40250/item_list_zh_overlay.txt new file mode 100644 index 00000000..0f21c918 --- /dev/null +++ b/data/zh_40250/item_list_zh_overlay.txt @@ -0,0 +1,2 @@ +# vnum type icon[ model]: whole item_list.txt rows (build_zh_locale.py ROW_FILES) +50307 ETC icon/item/50035.tga diff --git a/data/zh_40250/item_names_zh_overlay.txt b/data/zh_40250/item_names_zh_overlay.txt index e40f3d38..97f0fa4b 100644 --- a/data/zh_40250/item_names_zh_overlay.txt +++ b/data/zh_40250/item_names_zh_overlay.txt @@ -1607,3 +1607,4 @@ VNUM LOCALE_NAME 164200 传说黑龙石 164300 传说黑龙石 164400 传说黑龙石 +50307 小狩猎套装礼包 diff --git a/data/zh_40250/itemdesc_zh_overlay.txt b/data/zh_40250/itemdesc_zh_overlay.txt new file mode 100644 index 00000000..5b45d89c --- /dev/null +++ b/data/zh_40250/itemdesc_zh_overlay.txt @@ -0,0 +1,2 @@ +# vnum name description: whole itemdesc.txt rows (build_zh_locale.py ROW_FILES) +50307 小狩猎套装礼包 打开后按职业获得一套1级+9装备:武器、盔甲、头盔、盾牌、手镯、鞋子、项链、耳环,每件5条满值属性。需留15格背包空位。 diff --git a/scripts/build_zh_locale.py b/scripts/build_zh_locale.py index 9bee6318..18275cc5 100644 --- a/scripts/build_zh_locale.py +++ b/scripts/build_zh_locale.py @@ -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 (_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()} diff --git a/scripts/test_build_zh_locale.py b/scripts/test_build_zh_locale.py index 6aa581a1..0f23b787 100644 --- a/scripts/test_build_zh_locale.py +++ b/scripts/test_build_zh_locale.py @@ -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) diff --git a/tools/server/hunt-set/README.md b/tools/server/hunt-set/README.md new file mode 100644 index 00000000..6bd66152 --- /dev/null +++ b/tools/server/hunt-set/README.md @@ -0,0 +1,54 @@ +# 小狩猎套装礼包(203 服务端) + +物品 50307(40250 里没被用到的 `任务执行书(初级)`,类型 ITEM_QUEST,使用即触发 `when 50307.use`)改名为 +**小狩猎套装礼包**,在百店老板娘「道具」页卖 2,000,000 两(`../npc-shops/item-mall/items.tsv`)。 +打开后按职业给一套 1 级可穿的 +9 装备,每件 5 条满值属性(第 6、7 条为空): + +| 部位 | 武士 | 刺客 | 修罗 | 法师 | +|---|---|---|---|---| +| 武器(选择) | 19 武士刀 / 3009 混铁斩 | 1009 白铁双刀 / 2009 藤竹弓(附 8000 木箭 ×200)/ 19 武士刀 | 19 武士刀 | 7009 纸折扇 | +| 盔甲 | 11209 | 11409 | 11609 | 11809 | +| 头盔 | 12209 | 12349 | 12489 | 12629 | + +盾牌 13009、手镯 14009、鞋子 15009、项链 16009、耳环 17009 各职业相同。法师的铃 5009 要 10 级,不给。 + +| 部位 | 属性 | +|---|---| +| 武器 | 暴击 10%、贯穿 10%、主属性 +12(武士力量、刺客敏捷、修罗/法师智力)、对恶魔 20%、对不死 20% | +| 盔甲 | 生命 +2000、吸血 10%、攻击力 +50、近身反弹 10%、施法速度 20% | +| 头盔 | 攻速 8%、对动物/兽人/秘教/不死 各 20% | +| 盾牌 | 经验 20%、金钱 20%、免疫晕眩、免疫减速、对恶魔 20% | +| 手镯 | 掉宝 20%、生命 +2000、贯穿 10%、吸血 10%、对动物 20% | +| 鞋子 | 移速 20%、攻速 8%、暴击 10%、经验 20%、金钱 20% | +| 项链 | 生命 +2000、暴击 10%、贯穿 10%、经验 20%、金钱 20% | +| 耳环 | 移速 20%、掉宝 20%、对兽人/秘教/不死 各 20% | + +属性都是 `item_attr` 表里该部位允许的类型、5 级的值。背包空格(`CountEmptyInventory`,按格数算)少于 15 +时不打开;确认前可以取消,礼包不消耗。 + +## 文件 + +- `server.patch`:40250 的任务函数不能写物品属性,补丁在 `questlua_item.cpp` 加 + `item.set_attribute(index, apply, value)`(`CItem::SetForceAttribute`,写库并记 ItemLog;apply 0 清空)。 +- `hunt_set.quest`:礼包任务(UTF-8,install.sh 转 GBK)。 +- `install.sh`:装任务、往 `quest_functions` 补 `item.set_attribute`、`pc.get_empty_inventory_count`、 + `pc.count_item`,服务端改名 50307(`item_names.txt` + `item_proto.locale_name`),重新编译任务。 + +客户端:名称 `data/zh_40250/item_names_zh_overlay.txt`,图标(用 50035 的礼盒图标)和说明 +`item_list_zh_overlay.txt`、`itemdesc_zh_overlay.txt`,由 `scripts/build_zh_locale.py` 打进 locale_zh。 + +## 构建与部署 + +```sh +cd /usr/metin2/src && patch -p1 < server.patch +cd server/game/src && gmake -j2 GAMEDIR=/root/hunt-out/new # 不覆盖线上 share/bin/game +cd /usr/metin2/server && sh close.sh +cp share/bin/game <备份>; cp /root/hunt-out/new/game share/bin/game +sh install.sh # 本目录 +sh ../npc-shops/install.sh ../npc-shops/item-mall +echo 1 | sh start.sh +``` + +2026-09-30 已在 203 部署。备份在 `/root/backup-hunt-set/`:`questlua_item.cpp.orig`、`bin-<时间>/game` +(旧二进制)、`<时间>/`(locale_list、quest_functions、item_names.txt)。用 GM 角色(武士)实测:礼包打开后 +8 件装备属性与上表一致,重新登录后仍在,礼包已扣除。 diff --git a/tools/server/hunt-set/hunt_set.quest b/tools/server/hunt-set/hunt_set.quest new file mode 100644 index 00000000..ba5a220f --- /dev/null +++ b/tools/server/hunt-set/hunt_set.quest @@ -0,0 +1,88 @@ +-- 小狩猎套装礼包 50307 (README.md): a level-1 +9 set for the player's class, five full attributes per piece. +-- 50307 is the unused 40250 任务执行书(初级) (ITEM_QUEST, so using it is `when 50307.use`). +-- item.set_attribute(index, apply, value) is not 40250: server.patch adds it to game. +-- UTF-8 here; install.sh converts it to GBK for qc. +quest hunt_set begin + state start begin + -- Gives one item and writes attributes 0..4 (a list of {apply, value}); 5 and 6 are cleared. + function give(vnum, attrs) + pc.give_item2_select(vnum) + -- Without a free cell nothing is given and the selected item is still the previous one. + if item.get_vnum() ~= vnum then + return + end + for i = 1, 7 do + local attr = attrs[i] + if attr then + item.set_attribute(i - 1, attr[1], attr[2]) + else + item.set_attribute(i - 1, 0, 0) + end + end + end + + when 50307.use begin + -- EApplyTypes (common/length.h) + local MAX_HP, INT, STR, DEX, ATT_SPEED, MOV_SPEED, CAST_SPEED = 1, 4, 5, 6, 7, 8, 9 + local CRITICAL, PENETRATE, ANIMAL, ORC, MILGYO, UNDEAD, DEVIL = 15, 16, 18, 19, 20, 21, 22 + local STEAL_HP, REFLECT_MELEE, EXP, GOLD, DROP = 23, 39, 43, 44, 45 + local IMMUNE_STUN, IMMUNE_SLOW, ATT_GRADE = 48, 49, 53 + + if pc.get_empty_inventory_count() < 15 then + syschat("背包空位不足,请至少留出15格再打开礼包。") + return + end + + local job = pc.get_job() + local weapon, arrows + say_title("小狩猎套装礼包") + if job == 0 then + say("请选择武器:") + local s = select("武士刀+9(单手剑)", "混铁斩+9(双手剑)") + local weapons = { 19, 3009 } + weapon = weapons[s] + elseif job == 1 then + say("请选择武器:") + local s = select("白铁双刀+9(双刀)", "藤竹弓+9(弓,附木箭200支)", "武士刀+9(单手剑)") + local weapons = { 1009, 2009, 19 } + weapon = weapons[s] + arrows = s == 2 + elseif job == 2 then + say("武器:武士刀+9") + weapon = 19 + else + say("武器:纸折扇+9") + weapon = 7009 + end + say("确定打开礼包吗?") + local ok = select("打开", "取消") + if ok ~= 1 then + return + end + -- The dialog waited; check again that the box is still there and the bag has room. + if pc.count_item(50307) < 1 or pc.get_empty_inventory_count() < 15 then + syschat("背包空位不足,请至少留出15格再打开礼包。") + return + end + + local mains = { STR, DEX, INT, INT } + local armors = { 11209, 11409, 11609, 11809 } + local helmets = { 12209, 12349, 12489, 12629 } + local main, armor, helmet = mains[job + 1], armors[job + 1], helmets[job + 1] + + pc.remove_item(50307, 1) + hunt_set.give(weapon, { { CRITICAL, 10 }, { PENETRATE, 10 }, { main, 12 }, { DEVIL, 20 }, { UNDEAD, 20 } }) + hunt_set.give(armor, { { MAX_HP, 2000 }, { STEAL_HP, 10 }, { ATT_GRADE, 50 }, { REFLECT_MELEE, 10 }, { CAST_SPEED, 20 } }) + hunt_set.give(helmet, { { ATT_SPEED, 8 }, { ANIMAL, 20 }, { ORC, 20 }, { MILGYO, 20 }, { UNDEAD, 20 } }) + hunt_set.give(13009, { { EXP, 20 }, { GOLD, 20 }, { IMMUNE_STUN, 1 }, { IMMUNE_SLOW, 1 }, { DEVIL, 20 } }) + hunt_set.give(14009, { { DROP, 20 }, { MAX_HP, 2000 }, { PENETRATE, 10 }, { STEAL_HP, 10 }, { ANIMAL, 20 } }) + hunt_set.give(15009, { { MOV_SPEED, 20 }, { ATT_SPEED, 8 }, { CRITICAL, 10 }, { EXP, 20 }, { GOLD, 20 } }) + hunt_set.give(16009, { { MAX_HP, 2000 }, { CRITICAL, 10 }, { PENETRATE, 10 }, { EXP, 20 }, { GOLD, 20 } }) + hunt_set.give(17009, { { MOV_SPEED, 20 }, { DROP, 20 }, { ORC, 20 }, { MILGYO, 20 }, { UNDEAD, 20 } }) + if arrows then + pc.give_item2(8000, 200) + end + syschat("小狩猎套装已放入背包。") + end + end +end diff --git a/tools/server/hunt-set/install.sh b/tools/server/hunt-set/install.sh new file mode 100644 index 00000000..423af6ba --- /dev/null +++ b/tools/server/hunt-set/install.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# Installs quest hunt_set, the 小狩猎套装礼包 50307 (README.md): run on the server from this directory after +# game is built with server.patch, then restart db + game. Adds the quest (GBK) to the locale's quest directory +# and locale_list, the functions it calls to quest_functions, names item 50307 on the server (item_names.txt +# and item_proto.locale_name; the client has its own name), and recompiles all quests. +set -eu + +SERVER=${SERVER:-/usr/metin2/server} +LOCALE=${LOCALE:-newcibn} +QUEST="$SERVER/share/locale/$LOCALE/quest" +NAMES="$SERVER/share/conf/item_names.txt" +HERE=$(cd "$(dirname "$0")" && pwd) +BACKUP=/root/backup-hunt-set/$(date +%Y%m%d-%H%M%S) +export LC_ALL=C + +mkdir -p "$BACKUP" +cp -p "$QUEST/locale_list" "$QUEST/quest_functions" "$NAMES" "$BACKUP/" +echo "backup: $BACKUP" + +iconv -f UTF-8 -t GBK "$HERE/hunt_set.quest" > "$BACKUP/hunt_set.gbk" +# A GBK trail byte 0x5C would be a backslash to qc. +if grep -q '\\' "$BACKUP/hunt_set.gbk"; then echo "hunt_set.quest: backslash byte in GBK" >&2; exit 1; fi +cat "$BACKUP/hunt_set.gbk" > "$QUEST/hunt_set.quest" + +# qc only accepts functions listed in quest_functions: item.set_attribute is new (server.patch), game has the +# other two (questlua_pc.cpp) but the list does not. +[ -z "$(tail -c 1 "$QUEST/quest_functions")" ] || echo >> "$QUEST/quest_functions" +for f in item.set_attribute pc.get_empty_inventory_count pc.count_item; do + grep -qx "$f" "$QUEST/quest_functions" || echo "$f" >> "$QUEST/quest_functions" +done + +# make.py strips only the LF of a locale_list line (203's has no newline after the last line). +sed -i '' '/^hunt_set\.quest/d' "$QUEST/locale_list" +[ -z "$(tail -c 1 "$QUEST/locale_list")" ] || echo >> "$QUEST/locale_list" +echo hunt_set.quest >> "$QUEST/locale_list" + +name=$(printf '小狩猎套装礼包' | iconv -f UTF-8 -t GB2312) +awk -F'\t' -v OFS='\t' -v name="$name" '$1 == 50307 { $2 = name; n++ } { print } + END { if (n != 1) { print "item_names.txt: 50307 not found" > "/dev/stderr"; exit 1 } }' \ + "$NAMES" > "$BACKUP/item_names.new" +cat "$BACKUP/item_names.new" > "$NAMES" +mysql -uroot player -e "UPDATE item_proto SET locale_name = '$name' WHERE vnum = 50307" + +# make.py's shebang (/usr/local/bin/python) does not exist on 203. +cd "$QUEST" && /usr/local/bin/python2.7 make.py > "$BACKUP/make.log" 2>&1 || { tail "$BACKUP/make.log" >&2; exit 1; } +ls object/50307/use +echo "done; restart db and game" diff --git a/tools/server/hunt-set/server.patch b/tools/server/hunt-set/server.patch new file mode 100644 index 00000000..783fa516 --- /dev/null +++ b/tools/server/hunt-set/server.patch @@ -0,0 +1,33 @@ +--- a/server/game/src/questlua_item.cpp ++++ b/server/game/src/questlua_item.cpp +@@ -136,6 +136,22 @@ + return 0; + } + ++ // item.set_attribute(index, apply, value): writes one attribute of the selected item (hunt-set, not ++ // 40250); apply 0 clears it. ++ int item_set_attribute(lua_State* L) ++ { ++ LPITEM item = CQuestManager::instance().GetCurrentItem(); ++ if (item && lua_isnumber(L, 1) && lua_isnumber(L, 2) && lua_isnumber(L, 3)) ++ { ++ int idx = (int) lua_tonumber(L, 1); ++ int type = (int) lua_tonumber(L, 2); ++ int value = (int) lua_tonumber(L, 3); ++ if (idx >= 0 && idx < ITEM_ATTRIBUTE_MAX_NUM && type >= 0 && type < MAX_APPLY_NUM) ++ item->SetForceAttribute(idx, type, type ? value : 0); ++ } ++ return 0; ++ } ++ + int item_get_vnum(lua_State* L) + { + CQuestManager& q = CQuestManager::instance(); +@@ -463,6 +479,7 @@ + { "remove", item_remove }, + { "get_socket", item_get_socket }, + { "set_socket", item_set_socket }, ++ { "set_attribute", item_set_attribute }, + { "get_vnum", item_get_vnum }, + { "has_flag", item_has_flag }, + { "get_value", item_get_value }, diff --git a/tools/server/npc-shops/item-mall/items.tsv b/tools/server/npc-shops/item-mall/items.tsv index de68c991..811171b0 100644 --- a/tools/server/npc-shops/item-mall/items.tsv +++ b/tools/server/npc-shops/item-mall/items.tsv @@ -42,3 +42,4 @@ 71085 1 1000000 追加物品属性秘籍 71032 1 1000000 龙神的祝福书 70058 1 1 移动戒指 不限次数、无冷却(../../ring-warp) +50307 1 2000000 小狩猎套装礼包 1级+9全套、满属性(../../hunt-set)