From 42e4cf1a9ff31c8391301eaf86cc2d9b064e82ac Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 30 Sep 2026 08:18:56 +0900 Subject: [PATCH] server: unlimited teleport ring 70058, sold in a two-page item mall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ring-warp/: ring_warp.quest without use count or cooldown (the ring is never used up) + install.sh (GBK, make.py, item name 移动戒指). npc-shops: [page] lines in items.tsv make an extended shop written to shop_table_ex.txt; item-mall is now 增益 (29) + 道具 (12, incl. 70058 at 1 gold). Co-Authored-By: Claude Opus 5.5 --- tools/server/npc-shops/README.md | 9 ++- tools/server/npc-shops/install.sh | 59 ++++++++++++++++--- tools/server/npc-shops/item-mall/items.tsv | 5 +- tools/server/npc-shops/item-mall/shop.conf | 2 + tools/server/ring-warp/README.md | 13 +++++ tools/server/ring-warp/install.sh | 34 +++++++++++ tools/server/ring-warp/ring_warp.quest | 67 ++++++++++++++++++++++ 7 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 tools/server/ring-warp/README.md create mode 100755 tools/server/ring-warp/install.sh create mode 100644 tools/server/ring-warp/ring_warp.quest diff --git a/tools/server/npc-shops/README.md b/tools/server/npc-shops/README.md index aae1b9d3..1d4a10f7 100644 --- a/tools/server/npc-shops/README.md +++ b/tools/server/npc-shops/README.md @@ -6,10 +6,14 @@ 可选的 `NPC_NAME`(NPC 头顶显示的名字,UTF-8)。 - `items.tsv`:vnum、数量、价格、名称,一行一件。最多 40 件(`SHOP_HOST_ITEM_MAX_NUM`,5×8 格)。 客户端按 vnum 从小到大排列。 +- 多页商店:`items.tsv` 里的 `[页名]` 行开始新的一页(最多 3 页,每页最多 39 件,按文件顺序摆放), + `shop.conf` 的 `TAB_VNUMS` 给第 2、3 页的商店 vnum。这种商店不走 DB 的 `shop` / `shop_item`, + 而是写进 `share/locale/newcibn/shop_table_ex.txt`(game 的 `CShopManager::ReadShopTableEx`,价格就在文件里); + 同一个 NPC 不能同时有 DB 商店,所以 install.sh 会删掉 DB 里对应的行。目前这个文件只归 `item-mall` 用。 | 目录 | 商店 | NPC | 位置 | |---|---|---|---| -| `item-mall/` | 14 `item_mall` | 9010 百店老板娘 | 杂货店 9003 旁 | +| `item-mall/` | 14 / 16 `item_mall`(两页:增益、道具) | 9010 百店老板娘 | 杂货店 9003 旁 | | `mount/` | 15 `mount` | 20368 坐骑商人(原「铁算盘」,未使用的货郎模型) | 马夫 20349 旁 | `install.sh` 对一个商店做这些事: @@ -25,7 +29,7 @@ 系统菜单里的「商城」按钮发 `/in_game_mall`,服务端回 `mall `。203 没有网页商城,所以把商城物品放进 9010 的商店。 客户端的 PORT 模块 `mt_mallhint` 会把这个按钮改成提示玩家去找百店老板娘。`item_mall.quest` 只在测试服下给 9010 -加对话选项,正式服不受影响。这 40 件物品的 `shop_buy_price` 是 0,卖回给 NPC 拿不到钱。 +加对话选项,正式服不受影响。这些物品的 `shop_buy_price` 是 0,卖回给 NPC 拿不到钱。 | 物品 | 价格(金币) | |---|---| @@ -38,6 +42,7 @@ | 双倍伤害丸、无视防御丸 | 各 15 万 | | 经验药水、夺宝手套 | 各 20 万 | | 物品属性转换秘籍 | 50 万 | +| 移动戒指(不限次数、无冷却,见 `../ring-warp`) | 1 | | 追加物品属性秘籍、龙神的祝福书 | 各 100 万 | ## 坐骑(mount) diff --git a/tools/server/npc-shops/install.sh b/tools/server/npc-shops/install.sh index b7e08ac4..14928e49 100755 --- a/tools/server/npc-shops/install.sh +++ b/tools/server/npc-shops/install.sh @@ -16,10 +16,14 @@ MAPS="$SERVER/share/locale/$LOCALE/map" export LC_ALL=C # SHOP_VNUM, SHOP_NAME, NPC_VNUM, VILLAGES (one "map x y facing" line per village) and optionally NPC_NAME -# (UTF-8), the name shown over the NPC. +# (UTF-8), the name shown over the NPC; TAB_VNUMS, the shop vnums of pages 2 and 3 when items.tsv has +# [page name] lines. NPC_NAME= +TAB_VNUMS= . "$SHOP_DIR/shop.conf" MOB_NAMES="$SERVER/share/conf/mob_names.txt" +SHOP_EX="$SERVER/share/locale/$LOCALE/shop_table_ex.txt" +EX_MARK="# npc-shops/install.sh $SHOP_NAME" BACKUP=/root/backup-npcshop/$SHOP_NAME-$(date +%Y%m%d-%H%M%S) mkdir -p "$BACKUP" @@ -27,6 +31,7 @@ cp -p "$PROTO" "$BACKUP/item_proto.txt" cp -p "$MOB_PROTO" "$BACKUP/mob_proto.txt" cp -p "$MOB_NAMES" "$BACKUP/mob_names.txt" echo "$VILLAGES" | while read -r map x y dir; do cp -p "$MAPS/$map/npc.txt" "$BACKUP/$map.npc.txt"; done +[ ! -f "$SHOP_EX" ] || cp -p "$SHOP_EX" "$BACKUP/shop_table_ex.txt" mysqldump -uroot player shop shop_item item_proto mob_proto > "$BACKUP/player-shop.sql" echo "backup: $BACKUP" @@ -53,13 +58,49 @@ if [ -n "$NPC_NAME" ]; then cat "$BACKUP/mob_names.new" > "$MOB_NAMES" fi -{ - 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, '$SHOP_NAME', $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 +# A shop with pages ([page name] lines) is an extended shop: game reads it from shop_table_ex.txt, one +# group per page, and refuses an NPC that also has a shop in the DB. +if grep -q '^\[' "$ITEMS"; then + if [ -f "$SHOP_EX" ] && ! head -1 "$SHOP_EX" | grep -qxF "$EX_MARK"; then + echo "$SHOP_EX belongs to another shop" >&2; exit 1 + fi + awk -F'\t' -v npc=$NPC_VNUM -v shop="$SHOP_NAME" -v vnums="$SHOP_VNUM $TAB_VNUMS" -v mark="$EX_MARK" ' + /^\[/ { name[++pages] = substr($1, 2, length($1) - 2); next } + $1 !~ /^#/ && NF >= 3 { if (!pages) exit 1; n = ++count[pages]; row[pages, n] = $1 "\t" $2 "\t" $3 } + END { + split(vnums, vnum, " ") + if (pages > 3 || !(pages in vnum)) { print "items.tsv: " pages " pages, TAB_VNUMS too short or more than 3" > "/dev/stderr"; exit 1 } + print mark + print "Group\tShopNPC\n{\n\t#--#\tnpc\tgroup" + for (p = 1; p <= pages; p++) printf "\t%d\t%d\t%s_%d\n", p, npc, shop, p + print "}" + for (p = 1; p <= pages; p++) { + if (count[p] > 39) { print "items.tsv: page " p " has more than 39 items" > "/dev/stderr"; exit 1 } + # Rows are read in key order (a std::map), so the keys are zero-padded. + printf "Group\t%s_%d\n{\n\tVnum\t%d\n\tName\t%s\n\tGroup\titems\n\t{\n\t\t#--#\tvnum\tcount\tprice\n", shop, p, vnum[p], name[p] + for (i = 1; i <= count[p]; i++) printf "\t\t%02d\t%s\n", i, row[p, i] + print "\t}\n}" + } + }' "$ITEMS" > "$BACKUP/shop_table_ex.utf8" + # Page names go to the client in the locale's code page. + iconv -f UTF-8 -t GB2312 "$BACKUP/shop_table_ex.utf8" > "$SHOP_EX" + vnums=$(echo $SHOP_VNUM $TAB_VNUMS | tr ' ' ',') + { + 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 "DELETE FROM shop_item WHERE shop_vnum IN ($vnums);" + echo "DELETE FROM shop WHERE vnum IN ($vnums);" + } | mysql -uroot player + echo "$SHOP_EX: $(grep -c '^Group' "$SHOP_EX") groups" +else + { + 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, '$SHOP_NAME', $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 +fi echo "$VILLAGES" | while read -r map x y dir; do npc="$MAPS/$map/npc.txt" @@ -72,5 +113,5 @@ echo "$VILLAGES" | while read -r map x y dir; do fi done -mysql -uroot player -N -e "SELECT COUNT(*) FROM shop_item WHERE shop_vnum = $SHOP_VNUM" +grep -q '^\[' "$ITEMS" || mysql -uroot player -N -e "SELECT COUNT(*) FROM shop_item WHERE shop_vnum = $SHOP_VNUM" echo "done; restart db and game (README.md)" diff --git a/tools/server/npc-shops/item-mall/items.tsv b/tools/server/npc-shops/item-mall/items.tsv index 173dc3d5..de68c991 100644 --- a/tools/server/npc-shops/item-mall/items.tsv +++ b/tools/server/npc-shops/item-mall/items.tsv @@ -1,4 +1,5 @@ -# vnum count price name (duration: unique items count worn play minutes, 仓库扩张卷 real time) +# [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) +[增益] 72723 1 1 红龙的祝福(小) 72724 1 1 红龙的祝福(中) 72725 1 1 红龙的祝福(大) @@ -28,6 +29,7 @@ 72019 1 10000 仓库扩张卷 15分钟 72020 1 20000 仓库扩张卷 30分钟 72021 1 100000 仓库扩张卷 3小时 +[道具] 72031 1 100000 龙神的攻击 72034 1 100000 龙神的防御 72037 1 100000 龙神的生命 @@ -39,3 +41,4 @@ 71084 1 500000 物品属性转换秘籍 71085 1 1000000 追加物品属性秘籍 71032 1 1000000 龙神的祝福书 +70058 1 1 移动戒指 不限次数、无冷却(../../ring-warp) diff --git a/tools/server/npc-shops/item-mall/shop.conf b/tools/server/npc-shops/item-mall/shop.conf index 99b47245..551d6d0e 100644 --- a/tools/server/npc-shops/item-mall/shop.conf +++ b/tools/server/npc-shops/item-mall/shop.conf @@ -5,3 +5,5 @@ NPC_VNUM=9010 VILLAGES="metin2_map_a1 686 565 8 metin2_map_b1 598 689 4 metin2_map_c1 391 693 5" +# Shop vnum of each page after the first (items.tsv has [tab] lines, so this is a shop_table_ex.txt shop). +TAB_VNUMS="16 17" diff --git a/tools/server/ring-warp/README.md b/tools/server/ring-warp/README.md new file mode 100644 index 00000000..244d17e9 --- /dev/null +++ b/tools/server/ring-warp/README.md @@ -0,0 +1,13 @@ +# 移动戒指(203 服务端) + +40250 的移动戒指 70058(任务物品,30 级可用)通过 `special_item_group.txt` 的 10031 组触发任务 `ring_warp`: +选择本国一村 / 二村、升龙谷、影悲沙漠、舒寒山、导炎大地后传送。原版能用 50 次、每次冷却 30 分钟 +(item_proto value0 / value1,记在物品 socket 0 / 1 里),用完就消失。 + +这里的 `ring_warp.quest` 去掉了次数和冷却:不再读写 socket,戒指不会消耗。在百店老板娘的商城里卖 +(`../npc-shops/item-mall`,1 金币)。 + + sh install.sh # 在服务器上,从本目录运行;然后重启 db 和 game + +`install.sh` 会备份到 `/root/backup-ringwarp/<时间>`,把任务转成 GBK 换掉 `ring_warp.quest`,用 `make.py` 重新编译 +全部任务,并把服务端的物品名(`item_names.txt`、DB `item_proto.locale_name`)改成「移动戒指」。 diff --git a/tools/server/ring-warp/install.sh b/tools/server/ring-warp/install.sh new file mode 100755 index 00000000..80e02ac9 --- /dev/null +++ b/tools/server/ring-warp/install.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Installs the unlimited 移动戒指 (README.md): run on the server from this directory, then restart db + game. +# Replaces quest ring_warp with ./ring_warp.quest (GBK), recompiles all quests with make.py, and names +# item 70058 移动戒指 on the server (item_names.txt and item_proto.locale_name; the client has its own name). +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-ringwarp/$(date +%Y%m%d-%H%M%S) +export LC_ALL=C + +mkdir -p "$BACKUP" +cp -p "$QUEST/ring_warp.quest" "$NAMES" "$BACKUP/" +echo "backup: $BACKUP" + +# A GBK trail byte 0x5C would be a backslash to qc. +iconv -f UTF-8 -t GBK "$HERE/ring_warp.quest" > "$BACKUP/ring_warp.gbk" +if grep -q '\\' "$BACKUP/ring_warp.gbk"; then echo "ring_warp.quest: backslash byte in GBK" >&2; exit 1; fi +cat "$BACKUP/ring_warp.gbk" > "$QUEST/ring_warp.quest" + +name=$(printf '移动戒指' | iconv -f UTF-8 -t GB2312) +awk -F'\t' -v OFS='\t' -v name="$name" '$1 == 70058 { $2 = name; n++ } { print } + END { if (n != 1) { print "item_names.txt: 70058 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 = 70058" + +# 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 "$QUEST/object/10031/sig_use" +echo "done; restart db and game" diff --git a/tools/server/ring-warp/ring_warp.quest b/tools/server/ring-warp/ring_warp.quest new file mode 100644 index 00000000..b58ecf51 --- /dev/null +++ b/tools/server/ring-warp/ring_warp.quest @@ -0,0 +1,67 @@ +-- 移动戒指 70058 (special_item_group 10031 "Quest" -> sig_use), from 40250 ring_warp.quest. +-- Changed for 203 (install.sh): no use count, no cooldown, the ring is never used up. +-- UTF-8 here; install.sh converts it to GBK for qc. +quest ring_warp begin + state start begin + when 10031.sig_use begin + local empire = pc.get_empire() + + say("请选择传送地点:") + + local town1 = { + gameforge.functions._100_say, + gameforge.functions._110_say, + gameforge.functions._120_say, + } + + local town2 = { + gameforge.functions._130_say, + gameforge.functions._140_say, + gameforge.functions._150_say, + } + local sub_set = select(town1[empire], town2[empire], locale.map_name[64], locale.map_name[63], locale.map_name[61], locale.map_name[62], gameforge.locale.cancel) + if sub_set == 7 then + return + end + + local warp = { + { -- empire town -1 + { 474300, 954800 }, + { 63800, 166400 }, + { 959900, 269200 }, + }, + { -- empire town -2 + { 353100, 882900 }, + { 145500, 240000 }, + { 863900, 246000 }, + }, + -- 64 map_n_threeway + { + { 402100, 673900 }, + { 270400, 739900 }, + { 321300, 808000 }, + }, + -- 63 metin2_map_n_desert_01 + { + { 217800, 627200 }, + { 221900, 502700 }, + { 344000, 502500 }, + }, + -- 61 map_n_snowm_01 + { + { 434200, 290600 }, + { 375200, 174900 }, + { 491800, 173600 }, + }, + -- 62 metin2_map_n_flame_01 + { + { 599400, 756300 }, + { 597800, 622200 }, + { 730700, 689800 }, + }, + } + + pc.warp(warp[sub_set][empire][1], warp[sub_set][empire][2]) + end + end +end