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
+6
View File
@@ -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`)换成礼盒图标和中文说明。
+2
View File
@@ -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
+1
View File
@@ -1607,3 +1607,4 @@ VNUM LOCALE_NAME
164200 传说黑龙石
164300 传说黑龙石
164400 传说黑龙石
50307 小狩猎套装礼包
+2
View File
@@ -0,0 +1,2 @@
# vnum name description: whole itemdesc.txt rows (build_zh_locale.py ROW_FILES)
50307 小狩猎套装礼包 打开后按职业获得一套1级+9装备:武器、盔甲、头盔、盾牌、手镯、鞋子、项链、耳环,每件5条满值属性。需留15格背包空位。
+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)
+54
View File
@@ -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 件装备属性与上表一致,重新登录后仍在,礼包已扣除。
+88
View File
@@ -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
+47
View File
@@ -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"
+33
View File
@@ -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 },
@@ -42,3 +42,4 @@
71085 1 1000000 追加物品属性秘籍
71032 1 1000000 龙神的祝福书
70058 1 1 移动戒指 不限次数、无冷却(../../ring-warp)
50307 1 2000000 小狩猎套装礼包 1级+9全套、满属性(../../hunt-set)
1 # [tab name] starts a shop page (shop_table_ex.txt, at most 3 pages of 39); then vnum count price name (duration: unique items count worn play minutes, 仓库扩张卷 real time)
42 71085 1 1000000 追加物品属性秘籍
43 71032 1 1000000 龙神的祝福书
44 70058 1 1 移动戒指 不限次数、无冷却(../../ring-warp)
45 50307 1 2000000 小狩猎套装礼包 1级+9全套、满属性(../../hunt-set)