From 059ceadec2999d769bb5397f67efb21baf2f27d4 Mon Sep 17 00:00:00 2001 From: shen <> Date: Wed, 30 Sep 2026 17:33:13 -0700 Subject: [PATCH] Expand movement ring to all 65 registered maps with paged categories --- docs/PORT-PLAN.md | 3 + tools/server/ring-warp/README.md | 57 +- tools/server/ring-warp/generate.py | 104 ++ tools/server/ring-warp/install.sh | 22 +- tools/server/ring-warp/maps.json | 1595 +++++++++++++++++++++ tools/server/ring-warp/ring_warp.quest | 221 ++- tools/server/ring-warp/tests/lua_runner.c | 13 + tools/server/ring-warp/tests/test_ring.py | 111 ++ 8 files changed, 2049 insertions(+), 77 deletions(-) create mode 100644 tools/server/ring-warp/generate.py create mode 100644 tools/server/ring-warp/maps.json create mode 100644 tools/server/ring-warp/tests/lua_runner.c create mode 100644 tools/server/ring-warp/tests/test_ring.py diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index 6fb87614..b35bff83 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -47,6 +47,9 @@ metin2-client 是 40250 Windows 客户端的跨平台版本。**首要目标是 2026-09-30 用户授权扩展:自动狩猎引怪改为服务端披风式追击,免费选择 3/10/20 个实际怪物队伍; 见 `tools/server/autohunt-pull/README.md`,203 已部署并实测 3 组/9 只怪物。原披风物品保持不变。 +2026-09-30 用户授权扩展:移动戒指列出 203 已登记的全部 65 张地图,按六类分页,包含活动、战争和测试地图; +受限副本走原有入口,活动保留入场流程,测试传送仅限 GM。见 `tools/server/ring-warp/README.md`。 + ## 3. 三个层和代码位置 | 层 | 40250 单元 | 做法 | 位置 | diff --git a/tools/server/ring-warp/README.md b/tools/server/ring-warp/README.md index bb5c87e9..a8408017 100644 --- a/tools/server/ring-warp/README.md +++ b/tools/server/ring-warp/README.md @@ -1,14 +1,53 @@ # 移动戒指(203 服务端) -40250 的移动戒指 70058(任务物品,30 级可用)通过 `special_item_group.txt` 的 10031 组触发任务 `ring_warp`: -选择本国一村 / 二村、升龙谷、影悲沙漠、舒寒山、导炎大地后传送。原版能用 50 次、每次冷却 30 分钟 -(item_proto value0 / value1,记在物品 socket 0 / 1 里),用完就消失。 +物品 70058 通过 special_item_group 10031 的 `sig_use` 触发 `ring_warp`。 +戒指不限等级、次数,不消耗物品,也没有自身冷却。传送仍遵守服务端的 `pc.can_warp()` 检查。 +百店老板娘的商城出售此物品(`../npc-shops/item-mall`,1 金币)。 -这里的 `ring_warp.quest` 去掉了次数和冷却:不再读写 socket,戒指不会消耗。`install.sh` 同时去掉 -`item_proto.txt` 里的 30 级限制(服务端 `CHARACTER::CanUseItem` 按它拒绝低等级角色使用),新建的角色也能用。在百店老板娘的商城里卖 -(`../npc-shops/item-mall`,1 金币)。 +2026-09-30 补齐 203 的 `newcibn/map/index` 全部 **65 张地图**,包括活动、战争和测试地图。 +目录及坐标来自实际服务器的 `index`、`Setting.txt`、`Town.txt`、`npc.txt`,保存在 `maps.json`。 +部分地图名称采用中文描述,目录名和地图编号用于唯一对应。 - sh install.sh # 在服务器上,从本目录运行;然后重启 db 和 game +| 分类 | 数量 | 操作 | +| --- | ---: | --- | +| 各国村庄 | 6 | 传送至各国一村、二村 | +| 野外地图 | 13 | 传送至地图出生点 | +| 洞穴与副本 | 15 | 普通洞穴传送;受限副本传送至原有入口 | +| 公会地图 | 6 | 公会领地及公会村庄 | +| 活动与战争 | 20 | 列出地图及原有入场方式,保留报名、婚礼及战争流程 | +| 测试地图 | 5 | 所有人可查看;仅 GM 可传送 | -`install.sh` 会备份到 `/root/backup-ringwarp/<时间>`,把任务转成 GBK 换掉 `ring_warp.quest`,用 `make.py` 重新编译 -全部任务,并把服务端的物品名(`item_names.txt`、DB `item_proto.locale_name`)改成「移动戒指」。 +每页最多 5 个地点,包含上一页、下一页、返回分类;选择传送后再确认。 +受限副本编号为 66、71、72、73、208、216、217、351。蜘蛛洞三层先经一层守卫进入二层; +流放二层和蓝龙巢穴先经流放洞窟入口进入,戒指不绕过通行证、血石、组队及副本入场条件。 +活动地图只显示入场说明,避免进入尚未开放或未分配给玩家的场景。 +资源活动地图 101 的服务器 Town.txt 坐标在地图边界外,目录记录保留原值,未用于传送。 + +## 修改与验证 + +修改 `maps.json` 后执行: + +```sh +python3 tools/server/ring-warp/generate.py +python3 tools/server/ring-warp/tests/test_ring.py SERVER_LIBLUA_DIRECTORY LIVE_MAP_INDEX +``` + +测试使用 40250 服务端的 Lua 5.0 源码构建临时解释器,执行生成的真实任务函数。 +检查地图覆盖、坐标边界、GBK 编码、全部 65 个地点的选择,以及翻页、取消、传送限制和 GM 权限。 +可增加第三个参数 `COMPILED_OBJECT_ROOT`,直接验证服务器 qc 生成的两个任务对象。 + +## 安装 + +从此目录在服务器执行: + +```sh +sh install.sh --quest-only # 仅更新任务;每个 game core 重载任务或重启 game +sh install.sh # 同时安装物品名、移除等级限制;首次安装需重启 db/game +``` + +安装前备份任务源码、两个任务对象、物品表至 `/root/backup-ringwarp/<时间>`。 +先在隔离目录编译并检查结果,再替换 ring_warp 的两个对象;不会清空其他任务对象。 +`--quest-only` 不更改物品表或数据库,不需要重启 db/auth。 +任务转换成 GBK;拒绝包含反斜杠字节的编码,避免 qc 解析异常。 + +203 已于本次部署仅重启四个 game core,db/auth 保持运行。此次是服务端任务更新,已有客户端重登后即可使用。 diff --git a/tools/server/ring-warp/generate.py b/tools/server/ring-warp/generate.py new file mode 100644 index 00000000..09c00e0d --- /dev/null +++ b/tools/server/ring-warp/generate.py @@ -0,0 +1,104 @@ +# coding: utf-8 +"""Generate the quest from the audited server map catalog (Python 3).""" +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +def generate(): + catalog = json.loads((ROOT / 'maps.json').read_text(encoding='utf-8')) + def quote(value): + return json.dumps(value, ensure_ascii=False) + lines = ['-- Generated by generate.py from maps.json. Do not edit by hand.', + '-- UTF-8 source; install.sh compiles GBK for the 203 server.', + 'quest ring_warp begin', ' state start begin', + ' function destinations()', ' return {'] + for category in catalog['categories']: + lines.append(' {name = %s, maps = {' % quote(category['name'])) + for m in category['maps']: + lines.append(' {id = %d, name = %s, x = %d, y = %d, mode = %s, note = %s},' % + (m['id'], quote(m['name']), m['target'][1], m['target'][2], quote(m['mode']), quote(m['note']))) + lines.append(' }},') + lines.extend([' }', ' end']) + lines.append(''' when 10031.sig_use begin + local categories = ring_warp.destinations() + local category_options = {} + for i = 1, table.getn(categories) do + table.insert(category_options, categories[i].name) + end + table.insert(category_options, "关闭") + while true do + say_title("移动戒指") + say("请选择地图分类:") + local category = select_table(category_options) + if category == table.getn(category_options) then return end + local maps = categories[category].maps + local page = 1 + local browsing = true + while browsing do + local first = (page - 1) * 5 + 1 + local last = math.min(first + 4, table.getn(maps)) + local options = {} + for i = first, last do + local label = maps[i].name + if maps[i].mode == "entrance" then label = label .. "(入口)" end + table.insert(options, label) + end + local count = table.getn(options) + local previous = 0 + local following = 0 + if page > 1 then + table.insert(options, "上一页") + previous = table.getn(options) + end + if last < table.getn(maps) then + table.insert(options, "下一页") + following = table.getn(options) + end + table.insert(options, "返回分类") + say_title(categories[category].name) + say("第 " .. page .. " 页 / 共 " .. math.ceil(table.getn(maps) / 5) .. " 页") + local choice = select_table(options) + if choice == table.getn(options) then + browsing = false + elseif previous > 0 and choice == previous then + page = page - 1 + elseif following > 0 and choice == following then + page = page + 1 + elseif choice >= 1 and choice <= count then + local destination = maps[first + choice - 1] + say_title(destination.name) + say("地图编号:" .. destination.id) + if destination.mode == "gm" and not pc.is_gm() then + say(destination.note) + wait() + elseif destination.mode == "event" then + say(destination.note) + wait() + else + if destination.mode == "entrance" then say(destination.note) end + if destination.mode == "gm" then say("管理员专用测试地图。") end + if select("传送", "返回列表") == 1 then + if not pc.can_warp() then + say("当前无法传送,请结束交易或其他操作后再试。") + return + end + pc.warp(destination.x, destination.y) + return + end + end + end + end + end + end + end +end''') + result = '\n'.join(lines) + '\n' + encoded = result.encode('gbk') + if b'\\' in encoded: + bad = sorted({c for c in result if b'\\' in c.encode('gbk')}) + raise ValueError('qc cannot parse GBK backslash bytes: %r' % bad) + return result + +if __name__ == '__main__': + (ROOT / 'ring_warp.quest').write_text(generate(), encoding='utf-8') diff --git a/tools/server/ring-warp/install.sh b/tools/server/ring-warp/install.sh index b442e7b1..e723d3b8 100755 --- a/tools/server/ring-warp/install.sh +++ b/tools/server/ring-warp/install.sh @@ -1,9 +1,11 @@ #!/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 +# Replaces quest ring_warp with ./ring_warp.quest (GBK), compiles only ring_warp in an isolated staging directory, and names # item 70058 移动戒指 on the server (item_names.txt and item_proto.locale_name; the client has its own name), and # drops its level 30 limit in item_proto.txt (CHARACTER::CanUseItem refuses it to lower characters). set -eu +MODE=${1:-full} +case "$MODE" in full|--quest-only) ;; *) echo "usage: $0 [--quest-only]" >&2; exit 2 ;; esac SERVER=${SERVER:-/usr/metin2/server} LOCALE=${LOCALE:-newcibn} @@ -21,7 +23,22 @@ 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 +# Compile in isolation before touching any live quest objects. +mkdir -p "$BACKUP/stage" "$BACKUP/object/state" "$BACKUP/object/10031/sig_use" +cp -p "$QUEST/object/state/ring_warp" "$BACKUP/object/state/" +cp -p "$QUEST/object/10031/sig_use/ring_warp.start" "$BACKUP/object/10031/sig_use/" +cp "$QUEST/quest_functions" "$BACKUP/stage/" +cp "$BACKUP/ring_warp.gbk" "$BACKUP/stage/ring_warp.quest" +(cd "$BACKUP/stage" && "$QUEST/qc" ring_warp.quest) > "$BACKUP/qc.log" 2>&1 || { cat "$BACKUP/qc.log" >&2; exit 1; } +test -s "$BACKUP/stage/object/state/ring_warp" +test -s "$BACKUP/stage/object/10031/sig_use/ring_warp.start" cat "$BACKUP/ring_warp.gbk" > "$QUEST/ring_warp.quest" +cp "$BACKUP/stage/object/state/ring_warp" "$QUEST/object/state/ring_warp" +cp "$BACKUP/stage/object/10031/sig_use/ring_warp.start" "$QUEST/object/10031/sig_use/ring_warp.start" +if [ "$MODE" = "--quest-only" ]; then + echo "done; reload quests on every game core, or restart game cores (db/auth need no restart)" + exit 0 +fi name=$(printf '移动戒指' | iconv -f UTF-8 -t GB2312) awk -F'\t' -v OFS='\t' -v name="$name" '$1 == 70058 { $2 = name; n++ } { print } @@ -38,7 +55,6 @@ awk -F'\t' -v OFS='\t' '$1 == 70058 { if ($15 == "LEVEL") { $15 = "LIMIT_NONE"; cat "$BACKUP/item_proto.new" > "$PROTO" mysql -uroot player -e "UPDATE item_proto SET limittype0 = 0, limitvalue0 = 0 WHERE vnum = 70058 AND limittype0 = 1" -# 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; } +# Only ring_warp is compiled; other quest objects are preserved. ls "$QUEST/object/10031/sig_use" echo "done; restart db and game" diff --git a/tools/server/ring-warp/maps.json b/tools/server/ring-warp/maps.json new file mode 100644 index 00000000..32d6f1ff --- /dev/null +++ b/tools/server/ring-warp/maps.json @@ -0,0 +1,1595 @@ +{ + "source": "192.168.21.203 newcibn map/index + Setting.txt + Town.txt + npc.txt, 2026-09-30", + "categories": [ + { + "name": "各国村庄", + "maps": [ + { + "id": 1, + "name": "盛唐国一村", + "directory": "metin2_map_a1", + "base": [ + 409600, + 896000 + ], + "size": [ + 4, + 5 + ], + "spawn": [ + 469300, + 964200 + ], + "mode": "direct", + "target": [ + 1, + 469300, + 964200 + ], + "note": "" + }, + { + "id": 3, + "name": "盛唐国二村", + "directory": "metin2_map_a3", + "base": [ + 307200, + 819200 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 363000, + 875200 + ], + "mode": "direct", + "target": [ + 3, + 363000, + 875200 + ], + "note": "" + }, + { + "id": 21, + "name": "秦皇国一村", + "directory": "metin2_map_b1", + "base": [ + 0, + 102400 + ], + "size": [ + 4, + 5 + ], + "spawn": [ + 55700, + 157900 + ], + "mode": "direct", + "target": [ + 21, + 55700, + 157900 + ], + "note": "" + }, + { + "id": 23, + "name": "秦皇国二村", + "directory": "metin2_map_b3", + "base": [ + 102400, + 204800 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 137800, + 234600 + ], + "mode": "direct", + "target": [ + 23, + 137800, + 234600 + ], + "note": "" + }, + { + "id": 41, + "name": "周武国一村", + "directory": "metin2_map_c1", + "base": [ + 921600, + 204800 + ], + "size": [ + 4, + 5 + ], + "spawn": [ + 969600, + 278400 + ], + "mode": "direct", + "target": [ + 41, + 969600, + 278400 + ], + "note": "" + }, + { + "id": 43, + "name": "周武国二村", + "directory": "metin2_map_c3", + "base": [ + 819200, + 204800 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 877400, + 253700 + ], + "mode": "direct", + "target": [ + 43, + 877400, + 253700 + ], + "note": "" + } + ] + }, + { + "name": "野外地图", + "maps": [ + { + "id": 61, + "name": "舒寒山", + "directory": "map_n_snowm_01", + "base": [ + 358400, + 153600 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 435200, + 230400 + ], + "mode": "direct", + "target": [ + 61, + 435200, + 230400 + ], + "note": "" + }, + { + "id": 62, + "name": "导炎大地", + "directory": "metin2_map_n_flame_01", + "base": [ + 588800, + 614400 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 665600, + 691200 + ], + "mode": "direct", + "target": [ + 62, + 665600, + 691200 + ], + "note": "" + }, + { + "id": 63, + "name": "影悲沙漠", + "directory": "metin2_map_n_desert_01", + "base": [ + 204800, + 486400 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 281600, + 563200 + ], + "mode": "direct", + "target": [ + 63, + 281600, + 563200 + ], + "note": "" + }, + { + "id": 64, + "name": "升龙谷", + "directory": "map_n_threeway", + "base": [ + 256000, + 665600 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 323500, + 806900 + ], + "mode": "direct", + "target": [ + 64, + 323500, + 806900 + ], + "note": "" + }, + { + "id": 65, + "name": "鬼族寺院", + "directory": "metin2_map_milgyo", + "base": [ + 537600, + 51200 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 553700, + 145000 + ], + "mode": "direct", + "target": [ + 65, + 553700, + 145000 + ], + "note": "" + }, + { + "id": 67, + "name": "幽灵森林", + "directory": "metin2_map_trent", + "base": [ + 281600, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 290100, + 5700 + ], + "mode": "direct", + "target": [ + 67, + 290100, + 5700 + ], + "note": "" + }, + { + "id": 68, + "name": "红森林", + "directory": "metin2_map_trent02", + "base": [ + 1049600, + 0 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 1119600, + 70000 + ], + "mode": "direct", + "target": [ + 68, + 1119600, + 70000 + ], + "note": "" + }, + { + "id": 69, + "name": "巨人荒地", + "directory": "metin2_map_WL_01", + "base": [ + 1049600, + 716800 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 1058800, + 725800 + ], + "mode": "direct", + "target": [ + 69, + 1058800, + 725800 + ], + "note": "" + }, + { + "id": 70, + "name": "蛇之领域", + "directory": "metin2_map_nusluck01", + "base": [ + 819200, + 716800 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 827700, + 763400 + ], + "mode": "direct", + "target": [ + 70, + 827700, + 763400 + ], + "note": "" + }, + { + "id": 301, + "name": "龙头海角", + "directory": "Metin2_map_CapeDragonHead", + "base": [ + 1024000, + 1664000 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 1104800, + 1788800 + ], + "mode": "direct", + "target": [ + 301, + 1104800, + 1788800 + ], + "note": "" + }, + { + "id": 302, + "name": "迷雾森林", + "directory": "metin2_map_dawnmistwood", + "base": [ + 1177600, + 1664000 + ], + "size": [ + 7, + 4 + ], + "spawn": [ + 1225700, + 1682000 + ], + "mode": "direct", + "target": [ + 302, + 1225700, + 1682000 + ], + "note": "" + }, + { + "id": 303, + "name": "黑沙海湾", + "directory": "metin2_map_BayBlackSand", + "base": [ + 1049600, + 1510400 + ], + "size": [ + 3, + 6 + ], + "spawn": [ + 1087900, + 1649800 + ], + "mode": "direct", + "target": [ + 303, + 1087900, + 1649800 + ], + "note": "" + }, + { + "id": 304, + "name": "雷鸣山脉", + "directory": "metin2_map_Mt_Thunder", + "base": [ + 1126400, + 1510400 + ], + "size": [ + 4, + 6 + ], + "spawn": [ + 1134100, + 1655400 + ], + "mode": "direct", + "target": [ + 304, + 1134100, + 1655400 + ], + "note": "" + } + ] + }, + { + "name": "洞穴与副本", + "maps": [ + { + "id": 5, + "name": "盛唐国猴洞", + "directory": "metin2_map_monkey_dungeon_11", + "base": [ + 768000, + 435200 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 775200, + 447700 + ], + "mode": "direct", + "target": [ + 5, + 775200, + 447700 + ], + "note": "" + }, + { + "id": 25, + "name": "秦皇国猴洞", + "directory": "metin2_map_monkey_dungeon_12", + "base": [ + 844800, + 435200 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 852000, + 447700 + ], + "mode": "direct", + "target": [ + 25, + 852000, + 447700 + ], + "note": "" + }, + { + "id": 45, + "name": "周武国猴洞", + "directory": "metin2_map_monkey_dungeon_13", + "base": [ + 921600, + 435200 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 928800, + 447700 + ], + "mode": "direct", + "target": [ + 45, + 928800, + 447700 + ], + "note": "" + }, + { + "id": 66, + "name": "恶鬼塔", + "directory": "metin2_map_deviltower1", + "base": [ + 204800, + 665600 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 216100, + 726700 + ], + "mode": "entrance", + "target": [ + 65, + 590800, + 110800 + ], + "note": "将传送到恶鬼塔守卫附近,请与守卫交谈入场。" + }, + { + "id": 71, + "name": "蜘蛛洞二层", + "directory": "metin2_map_spiderdungeon_02", + "base": [ + 665600, + 435200 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 704000, + 462500 + ], + "mode": "entrance", + "target": [ + 104, + 92400, + 525500 + ], + "note": "将传送到蜘蛛洞守卫附近,二层仍需使用原有通行证。" + }, + { + "id": 72, + "name": "流放洞窟一层", + "directory": "metin2_map_skipia_dungeon_01", + "base": [ + 0, + 1203200 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 10000, + 1207800 + ], + "mode": "entrance", + "target": [ + 64, + 284200, + 811100 + ], + "note": "将传送到流放洞窟入口附近,仍需满足原有等级及血石条件。" + }, + { + "id": 73, + "name": "流放洞窟二层", + "directory": "metin2_map_skipia_dungeon_02", + "base": [ + 153600, + 1203200 + ], + "size": [ + 6, + 6 + ], + "spawn": [ + 241300, + 1275400 + ], + "mode": "entrance", + "target": [ + 64, + 284200, + 811100 + ], + "note": "请先从入口进入流放洞窟,再通过洞内通道前往二层。" + }, + { + "id": 104, + "name": "蜘蛛洞一层", + "directory": "metin2_map_spiderdungeon", + "base": [ + 51200, + 486400 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 60000, + 496600 + ], + "mode": "direct", + "target": [ + 104, + 60000, + 496600 + ], + "note": "" + }, + { + "id": 107, + "name": "猴洞初级", + "directory": "metin2_map_monkey_dungeon", + "base": [ + 819200, + 51200 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 826400, + 62800 + ], + "mode": "direct", + "target": [ + 107, + 826400, + 62800 + ], + "note": "" + }, + { + "id": 108, + "name": "猴洞中级", + "directory": "metin2_map_monkey_dungeon2", + "base": [ + 128000, + 640000 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 135200, + 652500 + ], + "mode": "direct", + "target": [ + 108, + 135200, + 652500 + ], + "note": "" + }, + { + "id": 109, + "name": "猴洞高级", + "directory": "metin2_map_monkey_dungeon3", + "base": [ + 128000, + 716800 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 135200, + 729300 + ], + "mode": "direct", + "target": [ + 109, + 135200, + 729300 + ], + "note": "" + }, + { + "id": 208, + "name": "蓝龙巢穴", + "directory": "metin2_map_skipia_dungeon_boss", + "base": [ + 819200, + 1049600 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 845300, + 1074200 + ], + "mode": "entrance", + "target": [ + 64, + 284200, + 811100 + ], + "note": "请先进入流放洞窟,再到二层的蓝龙入口办理入场。" + }, + { + "id": 216, + "name": "恶魔地下墓穴", + "directory": "metin2_map_devilcatacomb", + "base": [ + 307200, + 1203200 + ], + "size": [ + 8, + 8 + ], + "spawn": [ + 309200, + 1205200 + ], + "mode": "entrance", + "target": [ + 65, + 592400, + 100600 + ], + "note": "将传送到地下墓穴入口附近,请与入口守卫交谈。" + }, + { + "id": 217, + "name": "蜘蛛洞三层", + "directory": "metin2_map_spiderdungeon_03", + "base": [ + 51200, + 563200 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 99000, + 571100 + ], + "mode": "entrance", + "target": [ + 104, + 92400, + 525500 + ], + "note": "请先通过守卫进入二层,再从二层入口进入三层。" + }, + { + "id": 351, + "name": "火焰地牢", + "directory": "metin2_map_n_flame_dungeon_01", + "base": [ + 742400, + 614400 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 780700, + 753800 + ], + "mode": "entrance", + "target": [ + 62, + 614800, + 707000 + ], + "note": "将传送到火焰地牢入口附近,请按原有组队及入场流程进入。" + } + ] + }, + { + "name": "公会地图", + "maps": [ + { + "id": 4, + "name": "盛唐国公会领地", + "directory": "metin2_map_guild_01", + "base": [ + 128000, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 135400, + 5500 + ], + "mode": "direct", + "target": [ + 4, + 135400, + 5500 + ], + "note": "" + }, + { + "id": 24, + "name": "秦皇国公会领地", + "directory": "metin2_map_guild_02", + "base": [ + 179200, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 221900, + 9200 + ], + "mode": "direct", + "target": [ + 24, + 221900, + 9200 + ], + "note": "" + }, + { + "id": 44, + "name": "周武国公会领地", + "directory": "metin2_map_guild_03", + "base": [ + 230400, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 270900, + 12700 + ], + "mode": "direct", + "target": [ + 44, + 270900, + 12700 + ], + "note": "" + }, + { + "id": 6, + "name": "盛唐国公会村庄", + "directory": "metin2_guild_village_01", + "base": [ + 204800, + 204800 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 230600, + 231900 + ], + "mode": "direct", + "target": [ + 6, + 230600, + 231900 + ], + "note": "" + }, + { + "id": 26, + "name": "秦皇国公会村庄", + "directory": "metin2_guild_village_02", + "base": [ + 614400, + 384000 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 640200, + 411100 + ], + "mode": "direct", + "target": [ + 26, + 640200, + 411100 + ], + "note": "" + }, + { + "id": 46, + "name": "周武国公会村庄", + "directory": "metin2_guild_village_03", + "base": [ + 256000, + 819200 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 281800, + 846300 + ], + "mode": "direct", + "target": [ + 46, + 281800, + 846300 + ], + "note": "" + } + ] + }, + { + "name": "活动与战争", + "maps": [ + { + "id": 81, + "name": "婚礼地图", + "directory": "metin2_map_wedding_01", + "base": [ + 819200, + 0 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 822300, + 22000 + ], + "mode": "event", + "target": [ + 81, + 822300, + 22000 + ], + "note": "婚礼地图请通过婚礼管理员及婚礼邀请进入。" + }, + { + "id": 100, + "name": "野外活动地牢", + "directory": "metin2_map_fielddungeon", + "base": [ + 3200000, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 3215100, + 7600 + ], + "mode": "event", + "target": [ + 100, + 3215100, + 7600 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 101, + "name": "资源活动地图", + "directory": "metin2_map_resources_zon", + "base": [ + 3200000, + 51200 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 3225600, + 76800 + ], + "mode": "event", + "target": [ + 101, + 3212800, + 64000 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 112, + "name": "决斗场", + "directory": "metin2_map_duel", + "base": [ + 844800, + 0 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 857400, + 2400 + ], + "mode": "event", + "target": [ + 112, + 857400, + 2400 + ], + "note": "决斗场请通过决斗管理员报名或观战。" + }, + { + "id": 113, + "name": "知识竞赛场", + "directory": "metin2_map_oxevent", + "base": [ + 870400, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 896500, + 24600 + ], + "mode": "event", + "target": [ + 113, + 896500, + 24600 + ], + "note": "知识竞赛请在活动开放时通过活动管理员报名。" + }, + { + "id": 114, + "name": "圣地战场", + "directory": "metin2_map_sungzi", + "base": [ + 921600, + 0 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 948100, + 41500 + ], + "mode": "event", + "target": [ + 114, + 948100, + 41500 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 118, + "name": "火焰圣地通道一", + "directory": "metin2_map_sungzi_flame_hill_01", + "base": [ + 1126400, + 102400 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1149100, + 118500 + ], + "mode": "event", + "target": [ + 118, + 1149100, + 118500 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 119, + "name": "火焰圣地通道二", + "directory": "metin2_map_sungzi_flame_hill_02", + "base": [ + 1126400, + 204800 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1149100, + 220900 + ], + "mode": "event", + "target": [ + 119, + 1149100, + 220900 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 120, + "name": "火焰圣地通道三", + "directory": "metin2_map_sungzi_flame_hill_03", + "base": [ + 1126400, + 307200 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1149100, + 323300 + ], + "mode": "event", + "target": [ + 120, + 1149100, + 323300 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 121, + "name": "冰雪圣地", + "directory": "metin2_map_sungzi_snow", + "base": [ + 1152000, + 0 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 1189300, + 17900 + ], + "mode": "event", + "target": [ + 121, + 1189300, + 17900 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 122, + "name": "冰雪圣地通道一", + "directory": "metin2_map_sungzi_snow_pass01", + "base": [ + 1177600, + 102400 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1200700, + 168900 + ], + "mode": "event", + "target": [ + 122, + 1200700, + 168900 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 123, + "name": "冰雪圣地通道二", + "directory": "metin2_map_sungzi_snow_pass02", + "base": [ + 1177600, + 204800 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1200700, + 271300 + ], + "mode": "event", + "target": [ + 123, + 1200700, + 271300 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 124, + "name": "冰雪圣地通道三", + "directory": "metin2_map_sungzi_snow_pass03", + "base": [ + 1177600, + 307200 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1200700, + 373700 + ], + "mode": "event", + "target": [ + 124, + 1200700, + 373700 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 125, + "name": "沙漠圣地", + "directory": "metin2_map_sungzi_desert_01", + "base": [ + 1228800, + 0 + ], + "size": [ + 4, + 4 + ], + "spawn": [ + 1270000, + 56400 + ], + "mode": "event", + "target": [ + 125, + 1270000, + 56400 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 126, + "name": "沙漠圣地通道一", + "directory": "metin2_map_sungzi_desert_hill_01", + "base": [ + 1228800, + 102400 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1242400, + 142100 + ], + "mode": "event", + "target": [ + 126, + 1242400, + 142100 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 127, + "name": "沙漠圣地通道二", + "directory": "metin2_map_sungzi_desert_hill_02", + "base": [ + 1228800, + 204800 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1242400, + 244500 + ], + "mode": "event", + "target": [ + 127, + 1242400, + 244500 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 128, + "name": "沙漠圣地通道三", + "directory": "metin2_map_sungzi_desert_hill_03", + "base": [ + 1228800, + 307200 + ], + "size": [ + 2, + 4 + ], + "spawn": [ + 1242400, + 346900 + ], + "mode": "event", + "target": [ + 128, + 1242400, + 346900 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 181, + "name": "帝国战争一", + "directory": "metin2_map_empirewar01", + "base": [ + 972800, + 102400 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 980000, + 114400 + ], + "mode": "event", + "target": [ + 181, + 980000, + 114400 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 182, + "name": "帝国战争二", + "directory": "metin2_map_empirewar02", + "base": [ + 921600, + 153600 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 934400, + 166400 + ], + "mode": "event", + "target": [ + 182, + 934400, + 166400 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + }, + { + "id": 183, + "name": "帝国战争三", + "directory": "metin2_map_empirewar03", + "base": [ + 972800, + 153600 + ], + "size": [ + 2, + 2 + ], + "spawn": [ + 985600, + 166400 + ], + "mode": "event", + "target": [ + 183, + 985600, + 166400 + ], + "note": "请在对应活动或战争开放时,通过原有报名及入场流程进入。" + } + ] + }, + { + "name": "测试地图", + "maps": [ + { + "id": 103, + "name": "测试地图一", + "directory": "metin2_map_t1", + "base": [ + 0, + 25600 + ], + "size": [ + 3, + 3 + ], + "spawn": [ + 45200, + 83500 + ], + "mode": "gm", + "target": [ + 103, + 45200, + 83500 + ], + "note": "此地图仅供管理员测试,普通角色不能直接进入。" + }, + { + "id": 105, + "name": "测试地图二", + "directory": "metin2_map_t2", + "base": [ + 6400, + 0 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 19200, + 12800 + ], + "mode": "gm", + "target": [ + 105, + 19200, + 12800 + ], + "note": "此地图仅供管理员测试,普通角色不能直接进入。" + }, + { + "id": 110, + "name": "测试地图三", + "directory": "metin2_map_t3", + "base": [ + 32000, + 0 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 44800, + 12800 + ], + "mode": "gm", + "target": [ + 110, + 44800, + 12800 + ], + "note": "此地图仅供管理员测试,普通角色不能直接进入。" + }, + { + "id": 111, + "name": "测试地图四", + "directory": "metin2_map_t4", + "base": [ + 57600, + 0 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 70400, + 12800 + ], + "mode": "gm", + "target": [ + 111, + 70400, + 12800 + ], + "note": "此地图仅供管理员测试,普通角色不能直接进入。" + }, + { + "id": 200, + "name": "公会建设测试地图", + "directory": "gm_guild_build", + "base": [ + 83200, + 0 + ], + "size": [ + 1, + 1 + ], + "spawn": [ + 105200, + 10000 + ], + "mode": "gm", + "target": [ + 200, + 105200, + 10000 + ], + "note": "此地图仅供管理员测试,普通角色不能直接进入。" + } + ] + } + ] +} diff --git a/tools/server/ring-warp/ring_warp.quest b/tools/server/ring-warp/ring_warp.quest index b58ecf51..c2385254 100644 --- a/tools/server/ring-warp/ring_warp.quest +++ b/tools/server/ring-warp/ring_warp.quest @@ -1,67 +1,158 @@ --- 移动戒指 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. +-- Generated by generate.py from maps.json. Do not edit by hand. +-- UTF-8 source; install.sh compiles GBK for the 203 server. 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 + state start begin + function destinations() + return { + {name = "各国村庄", maps = { + {id = 1, name = "盛唐国一村", x = 469300, y = 964200, mode = "direct", note = ""}, + {id = 3, name = "盛唐国二村", x = 363000, y = 875200, mode = "direct", note = ""}, + {id = 21, name = "秦皇国一村", x = 55700, y = 157900, mode = "direct", note = ""}, + {id = 23, name = "秦皇国二村", x = 137800, y = 234600, mode = "direct", note = ""}, + {id = 41, name = "周武国一村", x = 969600, y = 278400, mode = "direct", note = ""}, + {id = 43, name = "周武国二村", x = 877400, y = 253700, mode = "direct", note = ""}, + }}, + {name = "野外地图", maps = { + {id = 61, name = "舒寒山", x = 435200, y = 230400, mode = "direct", note = ""}, + {id = 62, name = "导炎大地", x = 665600, y = 691200, mode = "direct", note = ""}, + {id = 63, name = "影悲沙漠", x = 281600, y = 563200, mode = "direct", note = ""}, + {id = 64, name = "升龙谷", x = 323500, y = 806900, mode = "direct", note = ""}, + {id = 65, name = "鬼族寺院", x = 553700, y = 145000, mode = "direct", note = ""}, + {id = 67, name = "幽灵森林", x = 290100, y = 5700, mode = "direct", note = ""}, + {id = 68, name = "红森林", x = 1119600, y = 70000, mode = "direct", note = ""}, + {id = 69, name = "巨人荒地", x = 1058800, y = 725800, mode = "direct", note = ""}, + {id = 70, name = "蛇之领域", x = 827700, y = 763400, mode = "direct", note = ""}, + {id = 301, name = "龙头海角", x = 1104800, y = 1788800, mode = "direct", note = ""}, + {id = 302, name = "迷雾森林", x = 1225700, y = 1682000, mode = "direct", note = ""}, + {id = 303, name = "黑沙海湾", x = 1087900, y = 1649800, mode = "direct", note = ""}, + {id = 304, name = "雷鸣山脉", x = 1134100, y = 1655400, mode = "direct", note = ""}, + }}, + {name = "洞穴与副本", maps = { + {id = 5, name = "盛唐国猴洞", x = 775200, y = 447700, mode = "direct", note = ""}, + {id = 25, name = "秦皇国猴洞", x = 852000, y = 447700, mode = "direct", note = ""}, + {id = 45, name = "周武国猴洞", x = 928800, y = 447700, mode = "direct", note = ""}, + {id = 66, name = "恶鬼塔", x = 590800, y = 110800, mode = "entrance", note = "将传送到恶鬼塔守卫附近,请与守卫交谈入场。"}, + {id = 71, name = "蜘蛛洞二层", x = 92400, y = 525500, mode = "entrance", note = "将传送到蜘蛛洞守卫附近,二层仍需使用原有通行证。"}, + {id = 72, name = "流放洞窟一层", x = 284200, y = 811100, mode = "entrance", note = "将传送到流放洞窟入口附近,仍需满足原有等级及血石条件。"}, + {id = 73, name = "流放洞窟二层", x = 284200, y = 811100, mode = "entrance", note = "请先从入口进入流放洞窟,再通过洞内通道前往二层。"}, + {id = 104, name = "蜘蛛洞一层", x = 60000, y = 496600, mode = "direct", note = ""}, + {id = 107, name = "猴洞初级", x = 826400, y = 62800, mode = "direct", note = ""}, + {id = 108, name = "猴洞中级", x = 135200, y = 652500, mode = "direct", note = ""}, + {id = 109, name = "猴洞高级", x = 135200, y = 729300, mode = "direct", note = ""}, + {id = 208, name = "蓝龙巢穴", x = 284200, y = 811100, mode = "entrance", note = "请先进入流放洞窟,再到二层的蓝龙入口办理入场。"}, + {id = 216, name = "恶魔地下墓穴", x = 592400, y = 100600, mode = "entrance", note = "将传送到地下墓穴入口附近,请与入口守卫交谈。"}, + {id = 217, name = "蜘蛛洞三层", x = 92400, y = 525500, mode = "entrance", note = "请先通过守卫进入二层,再从二层入口进入三层。"}, + {id = 351, name = "火焰地牢", x = 614800, y = 707000, mode = "entrance", note = "将传送到火焰地牢入口附近,请按原有组队及入场流程进入。"}, + }}, + {name = "公会地图", maps = { + {id = 4, name = "盛唐国公会领地", x = 135400, y = 5500, mode = "direct", note = ""}, + {id = 24, name = "秦皇国公会领地", x = 221900, y = 9200, mode = "direct", note = ""}, + {id = 44, name = "周武国公会领地", x = 270900, y = 12700, mode = "direct", note = ""}, + {id = 6, name = "盛唐国公会村庄", x = 230600, y = 231900, mode = "direct", note = ""}, + {id = 26, name = "秦皇国公会村庄", x = 640200, y = 411100, mode = "direct", note = ""}, + {id = 46, name = "周武国公会村庄", x = 281800, y = 846300, mode = "direct", note = ""}, + }}, + {name = "活动与战争", maps = { + {id = 81, name = "婚礼地图", x = 822300, y = 22000, mode = "event", note = "婚礼地图请通过婚礼管理员及婚礼邀请进入。"}, + {id = 100, name = "野外活动地牢", x = 3215100, y = 7600, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 101, name = "资源活动地图", x = 3212800, y = 64000, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 112, name = "决斗场", x = 857400, y = 2400, mode = "event", note = "决斗场请通过决斗管理员报名或观战。"}, + {id = 113, name = "知识竞赛场", x = 896500, y = 24600, mode = "event", note = "知识竞赛请在活动开放时通过活动管理员报名。"}, + {id = 114, name = "圣地战场", x = 948100, y = 41500, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 118, name = "火焰圣地通道一", x = 1149100, y = 118500, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 119, name = "火焰圣地通道二", x = 1149100, y = 220900, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 120, name = "火焰圣地通道三", x = 1149100, y = 323300, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 121, name = "冰雪圣地", x = 1189300, y = 17900, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 122, name = "冰雪圣地通道一", x = 1200700, y = 168900, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 123, name = "冰雪圣地通道二", x = 1200700, y = 271300, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 124, name = "冰雪圣地通道三", x = 1200700, y = 373700, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 125, name = "沙漠圣地", x = 1270000, y = 56400, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 126, name = "沙漠圣地通道一", x = 1242400, y = 142100, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 127, name = "沙漠圣地通道二", x = 1242400, y = 244500, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 128, name = "沙漠圣地通道三", x = 1242400, y = 346900, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 181, name = "帝国战争一", x = 980000, y = 114400, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 182, name = "帝国战争二", x = 934400, y = 166400, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + {id = 183, name = "帝国战争三", x = 985600, y = 166400, mode = "event", note = "请在对应活动或战争开放时,通过原有报名及入场流程进入。"}, + }}, + {name = "测试地图", maps = { + {id = 103, name = "测试地图一", x = 45200, y = 83500, mode = "gm", note = "此地图仅供管理员测试,普通角色不能直接进入。"}, + {id = 105, name = "测试地图二", x = 19200, y = 12800, mode = "gm", note = "此地图仅供管理员测试,普通角色不能直接进入。"}, + {id = 110, name = "测试地图三", x = 44800, y = 12800, mode = "gm", note = "此地图仅供管理员测试,普通角色不能直接进入。"}, + {id = 111, name = "测试地图四", x = 70400, y = 12800, mode = "gm", note = "此地图仅供管理员测试,普通角色不能直接进入。"}, + {id = 200, name = "公会建设测试地图", x = 105200, y = 10000, mode = "gm", note = "此地图仅供管理员测试,普通角色不能直接进入。"}, + }}, + } + end + when 10031.sig_use begin + local categories = ring_warp.destinations() + local category_options = {} + for i = 1, table.getn(categories) do + table.insert(category_options, categories[i].name) + end + table.insert(category_options, "关闭") + while true do + say_title("移动戒指") + say("请选择地图分类:") + local category = select_table(category_options) + if category == table.getn(category_options) then return end + local maps = categories[category].maps + local page = 1 + local browsing = true + while browsing do + local first = (page - 1) * 5 + 1 + local last = math.min(first + 4, table.getn(maps)) + local options = {} + for i = first, last do + local label = maps[i].name + if maps[i].mode == "entrance" then label = label .. "(入口)" end + table.insert(options, label) + end + local count = table.getn(options) + local previous = 0 + local following = 0 + if page > 1 then + table.insert(options, "上一页") + previous = table.getn(options) + end + if last < table.getn(maps) then + table.insert(options, "下一页") + following = table.getn(options) + end + table.insert(options, "返回分类") + say_title(categories[category].name) + say("第 " .. page .. " 页 / 共 " .. math.ceil(table.getn(maps) / 5) .. " 页") + local choice = select_table(options) + if choice == table.getn(options) then + browsing = false + elseif previous > 0 and choice == previous then + page = page - 1 + elseif following > 0 and choice == following then + page = page + 1 + elseif choice >= 1 and choice <= count then + local destination = maps[first + choice - 1] + say_title(destination.name) + say("地图编号:" .. destination.id) + if destination.mode == "gm" and not pc.is_gm() then + say(destination.note) + wait() + elseif destination.mode == "event" then + say(destination.note) + wait() + else + if destination.mode == "entrance" then say(destination.note) end + if destination.mode == "gm" then say("管理员专用测试地图。") end + if select("传送", "返回列表") == 1 then + if not pc.can_warp() then + say("当前无法传送,请结束交易或其他操作后再试。") + return + end + pc.warp(destination.x, destination.y) + return + end + end + end + end + end + end + end end diff --git a/tools/server/ring-warp/tests/lua_runner.c b/tools/server/ring-warp/tests/lua_runner.c new file mode 100644 index 00000000..546f9d09 --- /dev/null +++ b/tools/server/ring-warp/tests/lua_runner.c @@ -0,0 +1,13 @@ +/* Minimal Lua 5.0 runner; use the server's liblua sources, not a newer Lua. */ +#include +#include "lua.h" +#include "lauxlib.h" +#include "lualib.h" +int main(int argc, char **argv) { + lua_State *L = lua_open(); + luaopen_base(L); luaopen_table(L); luaopen_string(L); luaopen_math(L); + if (argc != 2 || luaL_loadfile(L, argv[1]) || lua_pcall(L, 0, 0, 0)) { + fprintf(stderr, "%s\n", lua_tostring(L, -1)); return 1; + } + lua_close(L); return 0; +} diff --git a/tools/server/ring-warp/tests/test_ring.py b/tools/server/ring-warp/tests/test_ring.py new file mode 100644 index 00000000..cf3549bd --- /dev/null +++ b/tools/server/ring-warp/tests/test_ring.py @@ -0,0 +1,111 @@ +# coding: utf-8 +"""Catalog coverage plus Lua 5.0 menu/permission/warp integration checks. +Usage: python3 test_ring.py SERVER_LIBLUA_DIRECTORY [LIVE_MAP_INDEX] [COMPILED_OBJECT_ROOT] +""" +import importlib.util +import json +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('ring_generate', ROOT / 'generate.py') +generator = importlib.util.module_from_spec(spec) +spec.loader.exec_module(generator) +quest = generator.generate() +assert quest == (ROOT / 'ring_warp.quest').read_text(encoding='utf-8'), 'regenerate quest' +catalog = json.loads((ROOT / 'maps.json').read_text(encoding='utf-8')) +maps = [m for c in catalog['categories'] for m in c['maps']] +byid = {m['id']:m for m in maps} +assert len(maps) == len(byid) == 65 +if len(sys.argv) > 2: + live = dict((int(line.split()[0]),line.split()[1]) for line in Path(sys.argv[2]).read_text().splitlines() if line.strip() and not line.lstrip().startswith('#')) + assert {m['id']:m['directory'] for m in maps} == live +for m in maps: + assert m['mode'] in ('direct', 'entrance', 'gm', 'event') + target = byid[m['target'][0]] + for j in range(2): + assert target['base'][j] <= m['target'][j+1] < target['base'][j] + target['size'][j] * 25600, m +assert {m['id'] for m in maps if m['mode']=='entrance'} == {66,71,72,73,208,216,217,351} +assert byid[217]['target'][0] == 104 and byid[208]['target'][0] == 64 +assert 'remove_item' not in quest and 'socket' not in quest and 'setqf' not in quest +# Run the actual generated functions/event body under the server's Lua 5.0. +helper = quest.split(' function destinations()',1)[1].split(' when 10031.sig_use begin',1)[0] +body = quest.split(' when 10031.sig_use begin',1)[1].rsplit(' end',1)[0] +script = 'ring_warp = {}\nfunction ring_warp.destinations()' + helper + '\nfunction use_ring()\n' + body + '\nend\n' +if len(sys.argv) > 3: + objects = Path(sys.argv[3]) + state = (objects/'state/ring_warp').read_bytes().decode('gbk') + event = (objects/'10031/sig_use/ring_warp.start').read_bytes().decode('gbk') + script = state + '\nfunction use_ring()\n' + event + '\nend\n' +script += ''' +local categories = ring_warp.destinations() +local queue = {} +local cursor = 0 +local gm = false +local can_warp = true +local warps = {} +pc = { + is_gm = function() return gm end, + can_warp = function() return can_warp end, + warp = function(x,y) table.insert(warps,{x,y}) end, +} +say_title = function(text) end +say = function(text) end +wait = function() end +select_table = function(options) + assert(table.getn(options) <= 8, "menu too tall") + cursor = cursor + 1 + local choice = queue[cursor] + assert(choice and choice >= 1 and choice <= table.getn(options), "bad selection") + return choice +end +select = function(...) return select_table(arg) end +local runs = 0 +function run(answers, is_gm, allowed) + queue = answers; cursor = 0; gm = is_gm; can_warp = allowed; warps = {} + use_ring() + assert(cursor == table.getn(queue), "unexpected navigation") + runs = runs + 1 +end +for category = 1, table.getn(categories) do + local maps = categories[category].maps + for index = 1, table.getn(maps) do + local page = math.floor((index-1)/5)+1 + local answer = {} + table.insert(answer, category) + for p = 1,page-1 do table.insert(answer, p==1 and 6 or 7) end + table.insert(answer, math.mod(index-1,5)+1) + local m = maps[index] + local blocked = m.mode == "gm" or m.mode == "event" + if blocked then + local count = math.min(5,table.getn(maps)-(page-1)*5) + local buttons = count + 1 + if page > 1 then buttons = buttons + 1 end + if page*5 < table.getn(maps) then buttons = buttons + 1 end + table.insert(answer,buttons); table.insert(answer,7) + else table.insert(answer,1) end + run(answer,false,true) + assert(table.getn(warps) == (blocked and 0 or 1)) + if not blocked then assert(warps[1][1]==m.x and warps[1][2]==m.y) end + end +end +run({7},false,true) -- close +run({1,6,2,7,7},false,true) -- next, previous, return list/category, close +run({1,1,1},false,false) -- blocked by active trade etc. +assert(table.getn(warps)==0) +run({6,1,1},true,true) -- GM test-map access +assert(table.getn(warps)==1 and warps[1][1]==categories[6].maps[1].x) +print("PASS: 65 map selections, paging, back/cancel, warp guard and GM access; " .. runs .. " runs") +''' +lua = Path(sys.argv[1]) +with tempfile.TemporaryDirectory(prefix='ring-warp-test-') as d: + d = Path(d) + sources = [p for p in sorted((lua/'src').glob('*.c')) if p.name != 'ltests.c'] + sources += [lua/'src/lib'/name for name in ('lauxlib.c','lbaselib.c','ltablib.c','lstrlib.c','lmathlib.c')] + command = ['cc','-w','-I'+str(lua/'include'),'-I'+str(lua/'src'),str(ROOT/'tests/lua_runner.c'),*[str(p) for p in sources],'-lm','-o',str(d/'lua')] + subprocess.run(command,check=True) + (d/'test.lua').write_bytes(script.encode('gbk')) + subprocess.run([str(d/'lua'),str(d/'test.lua')],check=True) +print('PASS: catalog matches server index, coordinates in bounds, GBK source safe')