商城 NPC 商店:9010 百店老板娘在三个村卖商城物品(服务端安装脚本)

203 没有网页商城,系统菜单的商城窗口是空的。把 40 件商城物品放进商店 14,
挂在 a1/b1/c1 杂货店旁的 NPC 9010 上,按 item_proto gold 金币出售(红龙/蓝龙 1 金币)。
9010 的 mob_proto on_click 原为 2(无处理),改为 1(ON_CLICK_SHOP)才能打开商店。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 23:37:15 +09:00
co-authored by Claude Opus 5.5
parent 71d8f8c6e7
commit cc18db5dd1
3 changed files with 162 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
# 商城 NPC 商店(203 服务端)
40250 系统菜单里的「商城」按钮发 `/in_game_mall`,服务端回 `mall <url>`,客户端用网页窗口打开这个地址。203 没有网页商城,
URL 指向 google.com,所以窗口是空的。这里的做法是:把商城物品放进一个普通 NPC 商店,按金币卖。
- 商店 vnum 14(`shop` / `shop_item` 表),挂在 NPC 9010「百店老板娘」上(客户端模型 goods_02)。
- 9010 放在三个村(a1 / b1 / c1)的杂货店 9003 旁边,位置写在 `install.sh` 的 `VILLAGES` 里。
- 9010 在 mob_proto 里的 `on_click` 原本是 2,点击后没有任何处理。脚本把它改成 1(ON_CLICK_SHOP,见 game `trigger.cpp`),
点击才会打开商店。`item_mall.quest` 只在测试服下给 9010 加对话选项,正式服不影响。
- 售价写在 item_proto 的 `gold` 列(`item_proto.txt` 第 10 列 / DB `item_proto.gold`)。这 40 件物品的
`shop_buy_price`(卖给 NPC 的价格)是 0,所以不能低买高卖。
- `items.tsv`:vnum、数量、价格、名称,一行一件,最多 40 件(`SHOP_HOST_ITEM_MAX_NUM`,5×8 格)。
## 定价
| 物品 | 价格(金币) |
|---|---|
| 红龙 / 蓝龙的祝福(全部 8 种) | 1 |
| 经验戒指、小偷的手套、幸运金币 30 分 / 3 小时 / 10 小时 | 5 万 / 25 万 / 70 万 |
| 丝绸包囊 30 分 / 3 小时 / 10 小时 | 3 万 / 15 万 / 40 万 |
| 第三只手、兵法六统 15 分 / 30 分 / 3 小时 | 2 万 / 4 万 / 20 万 |
| 仓库扩张卷 15 分 / 30 分 / 3 小时 | 1 万 / 2 万 / 10 万 |
| 龙神的攻击 / 防御 / 生命 / 智力 | 各 10 万 |
| 双倍伤害丸、无视防御丸 | 各 15 万 |
| 经验药水、夺宝手套 | 各 20 万 |
| 物品属性转换秘籍 | 50 万 |
| 追加物品属性秘籍、龙神的祝福书 | 各 100 万 |
价格按时长大致线性递增,长时长的略有折扣。改价格时编辑 `items.tsv`,然后重新安装。
## 安装
```sh
scp -r tools/server/item-mall-shop root@<server>:/root/
ssh root@<server> 'sh /root/item-mall-shop/install.sh'
ssh root@<server> 'cd /usr/metin2/server && sh close.sh; sleep 8; echo 1 | sh start.sh'
```
db 只在启动时读取 item_proto、mob_proto 和商店表,game 只在启动时读取 npc.txt,所以装完必须重启。脚本可以重复运行:
价格和商店每次都按 `items.tsv` 重写;某个村如果已经有 9010,就不会再加一个。
## 回滚
每次运行都会备份到 `/root/backup-itemmall/<时间>/`:
```sh
B=/root/backup-itemmall/<时间>
cp $B/item_proto.txt $B/mob_proto.txt /usr/metin2/server/share/conf/
for m in metin2_map_a1 metin2_map_b1 metin2_map_c1; do
cp $B/$m.npc.txt /usr/metin2/server/share/locale/newcibn/map/$m/npc.txt
done
mysql -uroot player < $B/player-shop.sql
```
回滚后同样要重启。203 上最早的备份(改动之前)在 `/root/backup-itemmall/20260929-162714`,但这份备份里还没有 mob_proto;
9010 原来的 on_click 是 2。
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# The item-mall NPC shop (40250's 商城 was an external web shop that 203 never had). Run on the server from
# this directory, then restart db + game (README.md). Safe to run again: prices and the shop are rewritten
# from items.tsv, the NPC is only added to a village that does not have one yet.
set -eu
SERVER=${SERVER:-/usr/metin2/server}
LOCALE=${LOCALE:-newcibn}
SHOP_VNUM=14
NPC_VNUM=9010
HERE=$(cd "$(dirname "$0")" && pwd)
ITEMS="$HERE/items.tsv"
PROTO="$SERVER/share/conf/item_proto.txt"
MOB_PROTO="$SERVER/share/conf/mob_proto.txt"
MAPS="$SERVER/share/locale/$LOCALE/map"
BACKUP=/root/backup-itemmall/$(date +%Y%m%d-%H%M%S)
export LC_ALL=C
# village: map, then the NPC position (next to the general store 9003) and facing.
VILLAGES="metin2_map_a1 686 565 8
metin2_map_b1 598 689 4
metin2_map_c1 391 693 5"
mkdir -p "$BACKUP"
cp -p "$PROTO" "$BACKUP/item_proto.txt"
cp -p "$MOB_PROTO" "$BACKUP/mob_proto.txt"
echo "$VILLAGES" | while read -r map x y dir; do cp -p "$MAPS/$map/npc.txt" "$BACKUP/$map.npc.txt"; done
mysqldump -uroot player shop shop_item item_proto mob_proto > "$BACKUP/player-shop.sql"
echo "backup: $BACKUP"
# item_proto.txt column 10 is the price an NPC shop sells the item for (TItemTable::dwGold).
awk -F'\t' -v OFS='\t' 'NR == FNR { if ($1 !~ /^#/ && NF >= 3) { price[$1] = $3; want++ } next }
($1 in price) { $10 = price[$1]; n++ } { print }
END { if (n != want) { print "item_proto.txt: " n " of " want " items found" > "/dev/stderr"; exit 1 } }' \
"$ITEMS" "$PROTO" > "$BACKUP/item_proto.new"
cat "$BACKUP/item_proto.new" > "$PROTO"
# mob_proto.txt column 14 is ON_CLICK: the NPC only opens its shop with 1 (ON_CLICK_SHOP, trigger.cpp);
# 9010 ships with 2, which has no handler.
awk -F'\t' -v OFS='\t' -v v=$NPC_VNUM '$1 == v { $14 = 1; n++ } { print }
END { if (n != 1) { print "mob_proto.txt: NPC " v " not found" > "/dev/stderr"; exit 1 } }' \
"$MOB_PROTO" > "$BACKUP/mob_proto.new"
cat "$BACKUP/mob_proto.new" > "$MOB_PROTO"
{
awk -F'\t' '$1 !~ /^#/ && NF >= 3 { printf "UPDATE item_proto SET gold = %d WHERE vnum = %d;\n", $3, $1 }' "$ITEMS"
echo "UPDATE mob_proto SET on_click = 1 WHERE vnum = $NPC_VNUM;"
echo "REPLACE INTO shop (vnum, name, npc_vnum) VALUES ($SHOP_VNUM, 'item_mall', $NPC_VNUM);"
echo "DELETE FROM shop_item WHERE shop_vnum = $SHOP_VNUM;"
awk -F'\t' -v shop=$SHOP_VNUM '$1 !~ /^#/ && NF >= 3 { printf "INSERT INTO shop_item (shop_vnum, item_vnum, count) VALUES (%d, %d, %d);\n", shop, $1, $2 }' "$ITEMS"
} | mysql -uroot player
echo "$VILLAGES" | while read -r map x y dir; do
npc="$MAPS/$map/npc.txt"
if awk -F'\t' -v v=$NPC_VNUM '$11 == v { found = 1 } END { exit !found }' "$npc"; then
echo "$map: NPC $NPC_VNUM already placed"
else
[ -z "$(tail -c 1 "$npc")" ] || echo >> "$npc"
printf 'm\t%d\t%d\t0\t0\t0\t%d\t1m\t100\t1\t%d\n' "$x" "$y" "$dir" "$NPC_VNUM" >> "$npc"
echo "$map: NPC $NPC_VNUM at $x,$y"
fi
done
mysql -uroot player -N -e "SELECT COUNT(*) FROM shop_item WHERE shop_vnum = $SHOP_VNUM"
echo "done; restart db and game (README.md)"
+41
View File
@@ -0,0 +1,41 @@
# vnum count price name (duration: unique items count worn play minutes, 仓库扩张卷 real time)
72723 1 1 红龙的祝福(小)
72724 1 1 红龙的祝福(中)
72725 1 1 红龙的祝福(大)
72726 1 1 红龙的祝福(特)
72727 1 1 蓝龙的祝福(小)
72728 1 1 蓝龙的祝福(中)
72729 1 1 蓝龙的祝福(大)
72730 1 1 蓝龙的祝福(特)
72002 1 50000 经验戒指 30分钟
72003 1 250000 经验戒指 3小时
72001 1 700000 经验戒指 10小时
72005 1 50000 小偷的手套 30分钟
72006 1 250000 小偷的手套 3小时
72004 1 700000 小偷的手套 10小时
72023 1 50000 幸运金币 30分钟
72024 1 250000 幸运金币 3小时
72022 1 700000 幸运金币 10小时
72008 1 30000 丝绸包囊 30分钟
72009 1 150000 丝绸包囊 3小时
72007 1 400000 丝绸包囊 10小时
72016 1 20000 第三只手 15分钟
72017 1 40000 第三只手 30分钟
72018 1 200000 第三只手 3小时
72043 1 20000 兵法六统 15分钟
72044 1 40000 兵法六统 30分钟
72045 1 200000 兵法六统 3小时
72019 1 10000 仓库扩张卷 15分钟
72020 1 20000 仓库扩张卷 30分钟
72021 1 100000 仓库扩张卷 3小时
72031 1 100000 龙神的攻击
72034 1 100000 龙神的防御
72037 1 100000 龙神的生命
72040 1 100000 龙神的智力
72046 1 150000 双倍伤害丸
72025 1 150000 无视防御丸
71153 1 200000 经验药水
70043 1 200000 夺宝手套 2小时
71084 1 500000 物品属性转换秘籍
71085 1 1000000 追加物品属性秘籍
71032 1 1000000 龙神的祝福书
1 # vnum count price name (duration: unique items count worn play minutes, 仓库扩张卷 real time)
2 72723 1 1 红龙的祝福(小)
3 72724 1 1 红龙的祝福(中)
4 72725 1 1 红龙的祝福(大)
5 72726 1 1 红龙的祝福(特)
6 72727 1 1 蓝龙的祝福(小)
7 72728 1 1 蓝龙的祝福(中)
8 72729 1 1 蓝龙的祝福(大)
9 72730 1 1 蓝龙的祝福(特)
10 72002 1 50000 经验戒指 30分钟
11 72003 1 250000 经验戒指 3小时
12 72001 1 700000 经验戒指 10小时
13 72005 1 50000 小偷的手套 30分钟
14 72006 1 250000 小偷的手套 3小时
15 72004 1 700000 小偷的手套 10小时
16 72023 1 50000 幸运金币 30分钟
17 72024 1 250000 幸运金币 3小时
18 72022 1 700000 幸运金币 10小时
19 72008 1 30000 丝绸包囊 30分钟
20 72009 1 150000 丝绸包囊 3小时
21 72007 1 400000 丝绸包囊 10小时
22 72016 1 20000 第三只手 15分钟
23 72017 1 40000 第三只手 30分钟
24 72018 1 200000 第三只手 3小时
25 72043 1 20000 兵法六统 15分钟
26 72044 1 40000 兵法六统 30分钟
27 72045 1 200000 兵法六统 3小时
28 72019 1 10000 仓库扩张卷 15分钟
29 72020 1 20000 仓库扩张卷 30分钟
30 72021 1 100000 仓库扩张卷 3小时
31 72031 1 100000 龙神的攻击
32 72034 1 100000 龙神的防御
33 72037 1 100000 龙神的生命
34 72040 1 100000 龙神的智力
35 72046 1 150000 双倍伤害丸
36 72025 1 150000 无视防御丸
37 71153 1 200000 经验药水
38 70043 1 200000 夺宝手套 2小时
39 71084 1 500000 物品属性转换秘籍
40 71085 1 1000000 追加物品属性秘籍
41 71032 1 1000000 龙神的祝福书