Przeglądaj źródła

feat(pkuxkx): 新增逍遥行

dzp 2 lat temu
rodzic
commit
84f98ee874

+ 2 - 0
mud/pkuxkx/etc/ui-settings.tin

@@ -101,4 +101,6 @@
     {{key}{\coe}    {action}{option.Toggle EchoCommand}}
     {{key}{\com}    {action}{option.Toggle GagMove}}
     {{key}{\cog}    {action}{option.Toggle GMCPDebug}}
+    {{key}{\coM}    {action}{option.Toggle MapDebug; look}}
+    {{key}{\coV}    {action}{option.Toggle ShowRoomView; look}}
 };

+ 38 - 0
mud/pkuxkx/plugins/basic/map/__init__.tin

@@ -0,0 +1,38 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分。
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+*/
+
+///=== {
+///// 地图系统
+///// };
+
+#var basic_map[META] {
+    {NAME}      {地图系统}
+    {DESC}      {地图系统}
+    {AUTHOR}    {担子炮}
+};
+
+load-lib event;
+load-lib sync;
+
+event.Define {map/walk/continue} {无参} {$MODULE} {走路机器人结束运行时,可以发射本事件以驱动后续动作继续运行。};
+event.Define {map/walk/failed}   {无参} {$MODULE} {走路机器人运行失败时,可以发射本事件以通知调用方。};
+event.Define {map/init}          {无参} {$MODULE} {map 模块开始初始化。};
+
+load-file mud/pkuxkx/plugins/basic/map/room.tin;
+load-file mud/pkuxkx/plugins/basic/map/gmcp.tin;
+load-file mud/pkuxkx/plugins/basic/map/area.tin;
+load-file mud/pkuxkx/plugins/basic/map/node.tin;
+load-file mud/pkuxkx/plugins/basic/map/xiaoyao.tin;
+load-file mud/pkuxkx/plugins/basic/map/helper.tin;
+
+#func {basic_map.Init} {
+    event.Emit map/init;
+    set localmap;
+    set area_detail;
+    #return true;
+};

+ 196 - 0
mud/pkuxkx/plugins/basic/map/area.tin

@@ -0,0 +1,196 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分
+===========
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+===========
+*/
+
+load-module bot/pp;
+
+VAR {全局区域对照表} {map.area.dict} {};
+
+#nop Careless(随便查查)、Careful(仔细查查)、CarefulOnce(仔细查一次)三选一;
+VAR {定位模式} map.Locate.mode {Careless};
+
+event.Define {map/GotArea}      {无参} {$MODULE} {已经获取到区域信息,并更新到 gMapRoom 全局变量。};
+event.Define {map/GotLocalmaps} {无参} {$MODULE} {已经解析 localmaps 命令。};
+
+event.HandleOnce {map/init} {map/area} {map} {map.area.Init};
+
+#alias {map.area.Init} {
+    storage.Load {map-area} {map.area.dict};
+    event.Handle {map/GotRoomInfo} {map/area} {map} {map.GetArea};
+};
+
+#alias {map.GetArea} {
+    #if { "$gMapRoom[area][RESOLVED]" != "" }   {#return};
+    #if { "$map.Locate.mode" == "Careless" }    {#return};
+    #if { "$gMapRoom[direction]" != "here" }    {#return};
+    #if { @ga.IsUnderway{} }                    {#return};
+
+    #if { "$map.Locate.mode" == "CarefulOnce" } {
+        #var map.Locate.mode {Careless};
+    };
+
+    event.HandleOnce {map/GotNodeInfo} {map/area} {map} {map.area.check walk-done};
+    map.GetNodeInfo;
+};
+
+#alias {map.area.check} {
+    #local stage {%1};
+
+    #local area {@map.resolveArea{$gMapRoom[area]}};
+    #if { "$area" == "" } {
+        #switch {"$stage"} {
+            #case {"walk-done"} {
+                event.HandleOnce {map/GotLocalmaps} {map/area} {map} {map.area.check localmaps-done};
+                localmaps;
+            };
+            #case {"localmaps-done"} {
+                event.HandleOnce {pp/Response} {map/area} {map} {map.GetArea.pp.done};
+                pp $user[id];
+                #delay map.GetArea.pp {map.area.check pp-timeout} {30};
+            };
+            #case {"pp-timeoout"} {
+                errLog 竭尽全力也无法获得区域信息。我这是到了什么地方?;
+                event.UnHandle {pp/Response} {map/area} {map};
+            };
+        };
+        #return;
+    };
+
+    #var gMapRoom[area][RESOLVED] {$area};
+    okLog 调查完毕,这里是 $gMapRoom[area][RESOLVED]的$gMapRoom[name];
+    event.DelayEmit map/GotArea;
+};
+
+#alias {map.GetArea.pp.done} {
+    #undelay map.GetArea.pp;
+    #local ppInfo {$gPPResponse[$user[id]]};
+    #if { "$ppInfo" == "" } {
+        errLog PP 机器人应答出错,请检查设置。;
+        #return;
+    };
+
+    #if { "$ppInfo[room]" != "$gMapRoom[name]" } {
+        warnLog PP 期间角色发生了移动。定位失效。;
+        #return;
+    };
+
+    #var gMapRoom[area][PP] {$ppInfo[area]};
+    map.area.check;
+};
+
+#alias {map.Localmaps} {
+    #local gag  {%1};
+    #local args {%2};
+
+    #if { "$args" != "" } {
+        xtt.Send localmaps $args;
+        #return;
+    };
+
+    #var gMapRoom[area][LMAP] {};
+
+    #class map.Localmaps open;
+
+    #line oneshot #action {~^{\e\[0m|}%c◆%*地图%*◆%c$} {
+        #local color {%%2};
+        #replace color {\e[} {};
+        #replace color {m$} {};
+        #replace color {m} {;};
+        #var gMapRoom[area][LMAP]   {%%3};
+        #var gMapRoom[area][COLOR]  {$color};
+    } {2};
+
+    #line oneshot #action {^%s%S略图%s$} {
+        #var gMapRoom[terrain] {随机地图};
+        #var gMapRoom[village] {%%2};
+    };
+
+    #line oneshot #action {┌─{(─)*}─%*附近详图─{(─)*}─┐$} {
+        #var gMapRoom[terrain] {随机地图};
+    };
+
+    #if { "$gag" == "gag" } {
+        #action {^%*{|ID=map/localmaps}$} {#line gag} {6};
+        #gag {^%*{|ID=map/localmaps}$} {1};
+    };
+
+    #class map.Localmaps close;
+
+    xtt.Send localmaps;
+
+    sync.Wait {
+        #class map.Localmaps kill;
+        event.DelayEmit map/GotLocalmaps;
+    };
+};
+
+#alias {lm}         {map.Localmaps nogag {%0}};
+#alias {localmaps}  {map.Localmaps gag   {%0}};
+
+#nop 根据数据源确定区域信息。数据源有三种:walk、localmaps、pp;
+#func {map.resolveArea} {
+    #local source   {%0};
+
+    #if { "$source[RESOLVED]" != "" } {
+        #return {$source[RESOLVED]};
+    };
+
+    #if { "$source[PP]$source[WALK]$source[LMAP]" == "" } {
+        #return {};
+    };
+
+    #local pp   {@default{$source[PP];%*}};
+    #local walk {@default{$source[WALK];%*}};
+    #local lmap {@default{$source[LMAP];%*}};
+    #local pattern {$pp/$walk/$lmap};
+
+    #if { "$source[PP]" != "" } {
+        #local key {};
+        #local value {{PP}{$source[PP]}};
+        #foreach {*map.area.dict[$source[PP]/%*/%*][]} {key} {
+            #local value {$map.area.dict[$key]};
+            #unvar map.area.dict[$key];
+        };
+        #local value[WALK]  {@default{$value[WALK];$source[WALK]}};
+        #local value[LMAP]  {@default{$value[LMAP];$source[LMAP]}};
+        #local value[COLOR] {@default{$value[COLOR];{$source[COLOR]}}};
+        #local key {$source[PP]/@default{$source[WALK];UNKNOWN}/@default{$source[LMAP];UNKNOWN}};
+        #var map.area.dict[$key] {$value};
+        storage.Save {map-area} {map.area.dict};
+        #return {$pp};
+    };
+
+    #if { &map.area.dict[$pattern][] != 1 } {
+        #return {};
+    };
+
+    #foreach {*map.area.dict[$pattern]} {pattern} {
+        #return {$map.area.dict[$pattern][PP]};
+    };
+};
+
+#func {map.AreaColor} {
+    #local area {%1};
+    #local pattern {$area/%*/%*};
+
+    #if { &map.area.dict[$pattern][] != 1 } {
+        #return {};
+    };
+
+    #foreach {*map.area.dict[$pattern]} {pattern} {
+        #return {$map.area.dict[$pattern][COLOR]};
+    };
+};
+
+#alias {map.Here} {
+    #var map.Locate.mode {CarefulOnce};
+    look;
+};
+
+#alias {ll} {map.Here} {9.0};

+ 59 - 0
mud/pkuxkx/plugins/basic/map/gmcp.tin

@@ -0,0 +1,59 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分
+===========
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+===========
+*/
+
+VAR {未核销的 GMCP.Move 信息队列}   {map.gmcp-move.tbc}     {};
+
+event.HandleOnce {map/init} {map/gmcp} {map} {map.GMCP.TraceMove};
+
+#alias {map.GMCP.TraceMove} {
+    event.Handle GMCP.Move {map/gmcp} {map} {map.GMCP.OnMove};
+};
+
+#alias {map.GMCP.StopTrace} {
+    event.Handle GMCP.Move {map/gmcp} {map};
+};
+
+#alias {map.GMCP.OnMove} {
+    #list map.gmcp-move.tbc add {{$gGMCP[Move]}};
+};
+
+#func {map.GMCP.Confirm} {
+    #local move-success {@default{%1;true}};
+
+    #local cmd {@ga.ThisCmd{}};
+    #if { {$cmd} == {%s{l|look}{|\s+(.*)}%s} } {
+        #replace cmd {^%s{l|look}{|\s+(.*)}%s$} {
+            {cmd}   {look}
+            {exit}  {@dir.Long{&4}}
+        };
+        #return {$cmd};
+    };
+
+    #if { @isTrue{$move-success} } {
+        #while { &map.gmcp-move.tbc[] > 0 && @isFalse{$map.gmcp-move.tbc[1][成功]} } {
+            #list map.gmcp-move.tbc delete 1;
+        };
+    };
+
+    #if { &map.gmcp-move.tbc[] == 0 } {
+        errLog 发现 BUG,遇到了遗失先导 GMCP.Move 事件的行走反馈。;
+        #return {
+            {cmd} {$cmd}
+        };
+    };
+
+    #local gmcp {$map.gmcp-move.tbc[1]};
+    #list map.gmcp-move.tbc delete 1;
+
+    #return {
+        {cmd}   {$cmd}
+        {gmcp}  {$gmcp}
+    };
+};

+ 151 - 0
mud/pkuxkx/plugins/basic/map/helper.tin

@@ -0,0 +1,151 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分
+===========
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+===========
+*/
+
+load-lib event;
+
+event.Define {map/walk/boat/in}     {无参} {$MODULE}    {已上船};
+event.Define {map/walk/boat/out}    {无参} {$MODULE}    {即将下船};
+
+VAR {地图脚本同步信息} map.sync.room-id {};
+
+#alias {map.Sync} {
+    #local prev     {%1};
+    #local current  {%2};
+    #local next     {%3};
+    #local message  {};
+
+    #if { "$prev$current$next" == "" } {
+        #local message {WALK-SYNC-MESSAGE};
+    };
+    #else {
+        #local message {F${prev}-V${current}-T$next};
+        #var map.sync.room-id {
+            {prev}{$prev}
+            {current}{$current}
+            {next}{$next}
+        };
+    };
+
+    sync.Wait {
+        #line gag;
+        okLog 服务器已同步。;
+        event.DelayEmit map/walk/continue;
+    } {$message};
+};
+
+#alias {map.GuoJiang}   {map.shaogong 过江};
+#alias {map.GuoHe}      {map.shaogong 过河};
+#alias {map.YellBoat}   {map.shaogong 自助};
+
+#alias {map.shaogong} {
+    #local words {%1};
+
+    #class map.shaogong open;
+
+    #alias {map.waitBoat} { #var map-Boat-state {waitBoat} };
+
+    #action {^只听得江面上隐隐传来:“别急嘛,这儿正忙着呐……”$} {
+        #nop 不依赖触发了,用定时器靠谱一些;
+    };
+    #action {^岸边一只渡船上的艄公说道:正等着你呢,上来吧。$} {
+        #if { "${map-Boat-state}" == "waitBoat" } {
+            #var map-Boat-state {enterBoat};
+            #untick map.yellboat;
+            enter;
+            event.Emit map/walk/boat/in;
+        };
+    };
+    #action {^一叶扁舟缓缓地驶了过来,艄公将一块踏脚板搭上堤岸,以便乘客{上下。|}$} {
+        #if { "${map-Boat-state}" == "waitBoat" } {
+            #var map-Boat-state {enterBoat};
+            #untick map.yellboat;
+            enter;
+            event.Emit map/walk/boat/in;
+        };
+    };
+
+    #action {^%*接过你递给的船资%S。$}                                  {map.waitBoat};
+    #action {^你的车船通账上还剩%S,这一趟的船资是%S。$}                {map.waitBoat};
+    #action {^%S道:原来是%S的人,快请上船。$}                          {map.waitBoat};
+    #action {^你吸了口气,一声“船家”,声音中正平和地远远传了出去。$}  {map.waitBoat};
+    #action {^你使出吃奶的力气喊了一声:“船家”$}                      {map.waitBoat};
+
+    #action {^艄公把踏脚板收起来,说了一声“坐稳喽”,竹篙一点,扁舟向{|江心驶去。}$} {
+        #if { "${map-Boat-state}" == "enterBoat" } {
+            #var map-Boat-state {inBoat};
+            #untick map.yellboat;
+        };
+    };
+
+    #action {^艄公说{ |}“到啦,上岸吧”{ |},随即把一块踏脚板搭上堤岸。$} {
+        #if { "${map-Boat-state}" == "inBoat" } {
+            #nop 北侠 BUG 导致这条信息和下面赶下船的信息同时出现,这样会导致 out 失效。;
+            #nop 所以做一些防御处理;
+            #delay map-Boat-out {
+                #var map.stepaccu 1;
+                #unvar map-Boat-state;
+                #undelay map-Boat-out;
+                event.Emit map/walk/boat/out;
+                busy.Halt {out; map.BotReturn map.shaogong};
+            } {0.05};
+        };
+    };
+
+    #action {^艄公要继续做生意了,所有人被赶下了渡船。$} {
+        #if { "${map-Boat-state}" == "inBoat" } {
+            #delay map-Boat-out {
+                #var map.stepaccu 1;
+                #unvar map-Boat-state;
+                #undelay map-Boat-out;
+                event.Emit map/walk/boat/out;
+                busy.Halt {map.BotReturn map.shaogong};
+            } {0};
+        };
+    };
+
+    #tick map.yellboat {yell boat} 1;
+
+    #class map.shaogong close;
+
+    #if { "$words" == "自助" } {
+        yell boat;
+    };
+    #else {
+        ask shao gong about $words;
+    };
+};
+
+#alias {map.SpecialBoat} {
+    #class map.SpecialBoat open;
+    #action {^你跃上木船,船夫把木船划向海中。$}    {event.Emit map/walk/boat/in};  #nop 神龙岛;
+    #action {^你朝船夫挥了挥手便跨上岸去。$}        {map.SpecialBoat.return};       #nop 神龙岛;
+    #action {^你从踏板上走上了船。$}                {event.Emit map/walk/boat/in};  #nop 桃花岛;
+    #action {^你沿着踏板走了上去。$}                {map.SpecialBoat.return};       #nop 桃花岛;
+    #action {map.SpecialBoat.return} {event.Emit map/walk/boat/out; busy.Halt {map.BotReturn map.SpecialBoat}};
+    #class map.SpecialBoat close;
+    enter boat;
+};
+
+#alias {map.Ride} {
+    #local target {%1};
+
+    #class map.Ride open;
+    #action {^你跳上了小船,操舟向%*划去。$}        {event.Emit map/walk/boat/in};
+    #action {^你跳上了羊皮筏子,操舟向%*划去。$}    {event.Emit map/walk/boat/in};
+    #action {^你从小船上跳了下来,到了%*。$}        {event.Emit map/walk/boat/out; busy.Halt {map.BotReturn map.Ride}};
+    #class map.Ride close;
+    ride $target;
+};
+
+#alias {map.BotReturn} {
+    #local bot {%1};
+    #class $bot kill;
+    event.DelayEmit {map/walk/continue} {$bot};
+};

+ 172 - 0
mud/pkuxkx/plugins/basic/map/node.tin

@@ -0,0 +1,172 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分
+===========
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+===========
+*/
+
+load-lib event;
+
+event.Define {map/GotNodeInfo}  {无参} {$MODULE} {已经获取到节点信息,并更新到 gMapRoom 全局变量。};
+
+/*
+╭───扬州─────────┬─────────────╮
+│目的地                      │拼音名称                  │
+│丐帮分舵                    │gaibang                   │
+│濠州府                      │haozhou                   │
+│长江[建康府]                │jiankang                  │
+│曲阜                        │qufu                      │
+│往生堂                      │shashou                   │
+│信阳                        │xinyang                   │
+│长江[镇江]                  │zhenjiang                 │
+├──────────────┴─────────────┤
+│walk [拼音名]命令使用内建路径。                         │
+│walk -c [拼音名]返回具体地点的路径。                    │
+│node 命令列出玩家自建路径。                             │
+╰───────────────────北大侠客行────╯
+*/
+
+///=== {
+// ## map.GetNodeInfo
+//    通过 walk -c 命令查询当前房间的节点信息。如果当前房间不是节点,则什么也获取不到。
+//    否则可以获得两个信息:
+//        1. 当前节点的名称,如「扬州」,储存在 \$gMapRoom[node]。
+//        2. 当前节点所联通的节点列表,是个表格,储存在 \$gMapRoom[nodeLinks];
+// };
+#alias {map.GetNodeInfo} {
+    #if { "$gMapRoom[node]" != "" } {
+        #return;
+    };
+
+    #class map.GetNodeInfo open;
+
+    #alias {map.GetNodeInfo.done} {
+        #var gMapRoom[area][WALK] {@default{%%1;$gMapRoom[area][WALK]}};
+        #line gag;
+        #class map.GetNodeInfo kill;
+
+        #if { &gMapRoom[nodeLinks][] > 0 } {
+            #delay 0 okLog 节点信息已识别。;
+        };
+
+        event.DelayEmit map/GotNodeInfo;
+    };
+
+    #gag {^%*{|ID=map/getnode}$};
+
+    #action {{*UTF8}{^}╭─{(─)*}─{\p{Han}+}─{(─|┬)*}──╮{|ID=map/getnode}$} {
+        #var gMapRoom[node]         {%%4};
+        #var gMapRoom[nodeLinks]    {};
+
+        #class map.GetNodeInfo open;
+
+        #action {^│目的地%s│拼音名称%s│$} {#line gag} {4.999};
+        #action {^│%S%s│%S%s│{|ID=map/getnode}$} {
+            #var {gMapRoom[nodeLinks][%%%3]} {%%%1};
+        };
+
+        #class map.GetNodeInfo close;
+    };
+
+    #action {^%*内的系统内建路径出发点在:%*,请查询localmaps获得具体方位。{|ID=map/getnode}$} {
+        #var gMapRoom[area][WALK] {%%1};
+    };
+
+    #action {^%*内共有一处内建玩家路径起点,在%*。{|ID=map/getnode}$} {
+        #var gMapRoom[area][WALK] {%%1};
+    };
+
+    #action {^%*内共有%d处内建玩家路径起点,分别在%*。{|ID=map/getnode}$} {
+        #var gMapRoom[area][WALK] {%%1};
+    };
+
+    #action {^%*当前区域没有任何内建玩家路径起点。} {
+        #var gMapRoom[area][WALK] {%%1};
+    };
+
+    #class map.GetNodeInfo close;
+
+    walk -c;
+    sync.Wait {map.GetNodeInfo.done} {map/GetNodeInfo};
+};
+
+///=== {
+// ## map.WalkNodes <节点列表> [<回调钩子ID>]
+//    执行 walk 命令,按节点列表顺序依次行走,并发射走路机器人事件。
+//    可选的回调钩子 ID 可以只唤醒指定的钩子,避免惊群。
+//    到达目的地后会自动执行 look 命令。
+// };
+#alias {map.WalkNodes} {
+    #class map.WalkNodes open;
+
+    #var map.WalkNodes.nodes   {@list.FromSlist{%1}};
+    #var map.WalkNodes.hook    {%2};
+    #var map.WalkNodes.delay   {3};
+
+    #local ID {|ID=map/WalkNodes};
+
+    #action {^你开始往%*方向飞奔过去……{$ID}$} {
+        #nop prompt.Set {{nodeLinks}{<120>正在前往【%%1】,赶路中…}};
+        #var map.WalkNodes.delay   {3};
+    };
+
+    #action {^你因为种种原因停了下来,可以用walk继续进行。{$ID}$} {
+        #switch {"$gMapRoom[name]"} {
+            #case {"襄阳南门"}  {ask shou jiang about 投军; walk};
+            #case {"万纶台"}    {ask liang liuhe about 拜山; walk};
+            #default {
+                tuna max;
+                #delay $map.WalkNodes.delay {halt; walk};
+                #var map.WalkNodes.delay   {1};
+            };
+        };
+    };
+
+    #action {^你到达了%*。{$ID}$} {
+        #delay 0 {map.WalkNodes.walk-next};
+    };
+
+    #alias {map.WalkNodes.walk-next} {
+        #if { &map.WalkNodes.nodes[] > 0 } {
+            #local node {$map.WalkNodes.nodes[1]};
+            #list map.WalkNodes.nodes delete 1;
+            #if { "$node" == "DOCK/dock" } {
+                event.HandleOnce map/walk/continue {map.shaogong} {map} {map.WalkNodes.walk-next};
+                map.YellBoat;
+            };
+            #elseif { "$node" == "DOCK/ride{| \S+}" } {
+                #replace node {DOCK/ride} {};
+                event.HandleOnce map/walk/continue {map.Ride} {map} {map.WalkNodes.walk-next};
+                map.Ride $node;
+            };
+            #elseif { "$node" == "PATH/{\{.*\}}" } {
+                #replace node {PATH/{\{(.*)\}}} {&2};
+                xtt.SendAtOnce {$node};
+                sync.Wait {map.WalkNodes.walk-next};
+            };
+            #else {
+                xtt.Send {walk $node};
+            };
+        };
+        #else {
+            #local hook {$map.WalkNodes.hook};
+            #class map.WalkNodes kill;
+            event.Emit map/walk/continue {$hook};
+        };
+    };
+
+    #class map.WalkNodes close;
+
+    map.WalkNodes.walk-next;
+};
+
+/*
+襄阳南门
+守军拦住了你的去路,大声喝到:干什么的?要想通过先问问我们守将大人!
+守将给了你一块腰牌。
+牧民告诉你:牧场上最近出现了一只游荡的野狼,已经吞食了很多单身的旅人,没事最好经过这里。
+走路太快,你没在意脚下,被杂草绊了一下。
+*/

+ 446 - 0
mud/pkuxkx/plugins/basic/map/room.tin

@@ -0,0 +1,446 @@
+#nop 房间信息解析模块;
+
+load-module basic/title;
+
+VAR {当前房间ID}    gMapHereID  {0};
+VAR {当前房间信息}  gMapRoom    {};
+
+option.Define {ShowRoomView}    {Bool} {是否显示房间风景}       {false};
+option.Define {MapDebug}        {Bool} {房间信息解析调试开关}   {false};
+
+event.Define {map/GotRoomInfo}      {无参} {$MODULE} {已经获取到房间信息,并储存到 gMapRoom 全局变量。};
+event.Define {map/ArriveDock}       {无参} {$MODULE} {已经抵达码头};
+event.Define {map/ArriveCoachPark}  {无参} {$MODULE} {已经抵达马车行};
+
+event.HandleOnce {map/init} {map/room} {map} {map.Room.Watch};
+
+#alias {map.Room.Watch} {
+    #local nation   {?:[ ]+\[(大宋|大元|大理|大夏)国\]|};
+    #local terrain  {?:[ ]+\[(都城|城市|城内|村镇|野外|门派|帮派|阴间|(\S+)势力范围)\]|};
+    #local save     {?:[ ]+\[(存盘点)\]|};
+    #local store    {?:[ ]+\[(玩家储物柜)\]|};
+    #local group    {?:[ ]+\[(\S+)\]|};
+    #local mark     {?:[ ]+((?:㊣|★|☆|\s)*)|};
+    #local undef    {?:[ ]+\[未定义\]|};
+
+    #class map.Room.Watch open;
+
+    #action {^%* -{$nation}{$terrain}{$save}{$store}{$group}{$mark}{$undef}%s{?:房间名颜色代码: ([0-9;]+)|}$} {
+        #local ret {@map.GMCP.Confirm{}};
+
+        #local keep {};
+        #if { &ret[gmcp][] > 0 } {
+            #local keep[direction] {here};
+        };
+        #elseif { "$ret[cmd]" == "look" } {
+            #if { "$ret[exit]" != "" } {
+                #local keep {
+                    {direction} {$ret[exit]}
+                };
+            };
+            #else {
+                #local keep {
+                    {area}      {$gMapRoom[area]}
+                    {node}      {$gMapRoom[node]}
+                    {nodeLinks} {$gMapRoom[nodeLinks]}
+                    {coach}     {$gMapRoom[coach]}
+                    {coachLinks}{$gMapRoom[coachLinks]}
+                    {direction} {here}
+                };
+            };
+        };
+
+        #local room {
+            {name}{%%1}
+            {nation}{%%2}
+            {terrain}{%%3}
+            {save}{@if{{"%%5" == "存盘点"};true;false}}
+            {store}{@if{{"%%6" == "玩家储物柜"};true;false}}
+            {group}{%%7}
+            {mark}{@str.Split{{%%8};{{ +}};{;}}}
+            {cmd}{$ret[cmd]}
+            {look}{$ret[exit]}
+            {gmcp}{$ret[gmcp]}
+            $keep
+        };
+
+        map.Room.getInfo {$room};
+    };
+
+    #class map.Room.Watch close;
+};
+
+#alias {map.StopWatchRoom} {
+    #class map.Room.Watch kill;
+};
+
+#alias {map.Room.getInfo} {
+    #local args {%1};
+
+    #var gMapRoom               {};
+    #var gMapRoom[id]           {};                 #nop 房间ID;
+    #var gMapRoom[name]         {$args[name]};      #nop 房间名称;
+    #var gMapRoom[colorName]    {};                 #nop 带颜色的房间名称;
+    #var gMapRoom[nation]       {$args[nation]};    #nop 房间国家,大宋/大理/大元/大夏;
+    #var gMapRoom[terrain]      {$args[terrain]};   #nop 房间地段,包含村落和城墙;
+    #var gMapRoom[save]         {$args[save]};      #nop 是否为存盘点;
+    #var gMapRoom[store]        {$args[store]};     #nop 是否为玩家储物柜;
+    #var gMapRoom[group]        {$args[group]};     #nop 帮派名称,非帮派驻地留空;
+    #var gMapRoom[village]      {};                 #nop 村落名称(仅村落有效);
+    #var gMapRoom[mark]         {$args[mark]};      #nop 帮派名称,非帮派驻地留空;
+    #var gMapRoom[prev]         {$args[prev]};      #nop 上一个房间 ID;
+    #var gMapRoom[entry]        {$args[entry]};     #nop 房间入口,是用哪个方向指令进入本房间的;
+    #var gMapRoom[cmd]          {$args[cmd]};       #nop 本条房间信息是用何种命令获得;
+    #var gMapRoom[look]         {$args[look]};      #nop 如果是 look 命令,那么 look 的是哪个方向;
+    #var gMapRoom[gmcp]         {$args[gmcp]};      #nop GMCP.Move 信息;
+    #var gMapRoom[area]         {$args[area]};      #nop 房间所在区域;
+    #var gMapRoom[map]          {};                 #nop 房间小地图;
+    #var gMapRoom[maphere]      {};                 #nop 周围地形图;
+    #var gMapRoom[desc]         {};                 #nop 房间文字描述;
+    #var gMapRoom[descEnd]      {false};            #nop 房间文字描述已结束(目前以空行标识);
+    #var gMapRoom[view]         {};                 #nop 房间的风景(非文字描述);
+    #var gMapRoom[fog]          {false};            #nop 是否有雾;
+    #var gMapRoom[exits]        {};                 #nop 出口列表;
+    #var gMapRoom[exitHint]     {};                 #nop 出口所连接的房间;
+    #var gMapRoom[existShown]   {false};            #nop 出口信息已出现;
+    #var gMapRoom[lookable]     {};                 #nop 你可以看看(look)的东西;
+    #var gMapRoom[objs]         {};                 #nop 房间物品,不包含生物;
+    #var gMapRoom[items]        {};                 #nop 房间特殊物品;
+    #var gMapRoom[dynItems]     {};                 #nop 房间动态物品;
+    #var gMapRoom[npcs]         {};                 #nop 房间NPC, 不包含非生物和玩家;
+    #var gMapRoom[players]      {};                 #nop 房间玩家,不包含非生物和NPC;
+    #var gMapRoom[dock]         {};                 #nop 是否为渡口;
+    #var gMapRoom[funcs]        {};                 #nop 房间功能,领悟,睡觉,吃喝,买卖,等等;
+    #var gMapRoom[node]         {$args[node]};      #nop 节点名称,非节点留空;
+    #var gMapRoom[nodeLinks]    {$args[nodeLinks]}; #nop 通过本节点可以抵达的节点列表,非节点留空;
+    #var gMapRoom[coach]        {$args[coach]};     #nop 马车行名称,非马车行留空;
+    #var gMapRoom[coachLinks]   {$args[coachLinks]};#nop 通过本车行可以抵达的车行列表,非节点留空;
+    #var gMapRoom[direction]    {$args[direction]}; #nop 该房间所在的方位,如果是当前位置则为 here;
+
+    #local color {@buffer.RawLine{}};
+    #replace color {%S - %*$} {&1};
+    #replace color {\e[2;37;0m} {};
+    #var gMapRoom[colorName] {$color};
+
+    #if { @option.IsEnable{MapDebug} } {
+        #local optional {};
+        #if { "$gMapRoom[mark]" != "" }    {#cat optional { <175>标记<299> $gMapRoom[mark] }};
+        #if { @isTrue{$gMapRoom[save]} }   {#cat optional { <129><存盘点><299>}};
+        #if { @isTrue{$gMapRoom[store]} }  {#cat optional { <139><储物柜><299>}};
+        #if { "$gMapRoom[group]" != "" }   {#cat optional { <175>帮派驻地<299> $gMapRoom[group]}};
+
+        #echo {<175>房间名称<299> %s <175>国家<299> %s <175>地段<299> %s%s}
+            {$gMapRoom[name]} {$gMapRoom[nation]} {$gMapRoom[terrain]} $optional;
+
+        #line gag;
+    };
+
+    #class map.Room.getInfo open;
+
+    event.HandleOnce {GA} {map/room} {map} {map.Room.getInfo.done};
+
+    #action {{*UTF8}{^\s{4}((.*)(\p{Han}+).*\([A-Z][a-z A-Z']*\).*)}{|ID=map/Room/getInfo/objs}$} {
+        #local obj {@ParseTitle{%%2}};
+        #local name {$obj[name]};
+        #local type {房间物品};
+
+        #if { "$obj[title]" == "船老大" } {
+            #list gMapRoom[npcs] add {{$obj}};
+            #var gMapRoom[dock] {dock};
+            #local type {房间 NPC};
+        };
+        #else {
+            #list gMapRoom[objs] add {{$obj}};
+        };
+
+        #if { @option.IsEnable{MapDebug} } {
+            #if { @isFalse{$gMapRoom[existShown]} } {
+                #var gMapRoom[existShown] {true};
+                #echo {%s} {<134>房间出口<299> @slist.JoinAs{{@slist.FromList{$gMapRoom[gmcp][出口信息]}};{<179>%s<299>};{ }}};
+            };
+
+            #if { "$obj[emoji]" != "" } {
+                #local name {$name<299>{<139>$obj[emoji]<299>}};
+            };
+
+            #echo {<164>$type<299> %-18s<299> <169>%-16s<299> %s [%s] %s}
+                {$name} {$obj[id]} {{$obj[title]}} {$obj[nick]}
+                {@slist.Join{{$obj[status1];$obj[status2]}}};
+
+            #line gag;
+        };
+    } {9.9};
+
+    #action {^%s你可以看看(look):%*。$} {
+        #local items {@str.Split{{%%2};{{,|,}}}};
+        #var gMapRoom[items] {$items};
+
+        #class map.Room.getInfo.map kill;
+        #class map.Room.getInfo.desc kill;
+
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<171>特殊物品<299> {$items}};
+            #line gag;
+        };
+    };
+
+    #action {^%s你可以获取(get):%*。$} {
+        #local items {@str.Split{{%%2};{{,|,}}}};
+        #var gMapRoom[dynItems] {$items};
+
+        #class map.Room.getInfo.map kill;
+        #class map.Room.getInfo.desc kill;
+
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<171>动态物品<299> {$items}};
+            #line gag;
+        };
+    };
+
+    #action {^%s这里正是{举世闻名|声威赫赫|名震天下}的%*的产业,你可以进店(shop)逛逛。$} {
+        #var gMapRoom[group] {%%3};
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<171>帮派驻地<299> $gMapRoom[group],有商店};
+            #line gag;
+        };
+    } {3};
+
+    #action {^%s这里正是%*的产业$} {
+        #var gMapRoom[group] {%%2};
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<171>帮派驻地<299> $gMapRoom[group],无商店};
+            #line gag;
+        };
+    } {4};
+
+    #action {^%s这里建起了一大片宅子,气势恢宏,不知道主人是谁。$} {
+        #var gMapRoom[group] {UNKNOWN};
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<171>帮派驻地<299> $gMapRoom[group]};
+            #line gag;
+        };
+    } {4};
+
+    #action {^%s这里是%*通往外界的唯一道路。$} {
+        #var gMapRoom[terrain] {村落};
+        #var gMapRoom[village] {%%2};
+    };
+
+    #action {^%s这里是%*的一段城墙。$} {
+        #var gMapRoom[terrain] {城墙};
+    };
+
+    #action {^%*这里是一处%*,人迹罕至,也不知道你怎么会来到这里的。$} {
+        #var gMapRoom[terrain] {随机地图};
+    };
+
+    #action {^%s{这里(?:明显|所有|唯一)的(?:出口|方向)有|浓雾中你觉得似乎有出口通往} %*$} {
+        #local desc {%%2};
+        #if { "$desc" == "%*浓雾%*" } {
+            #var gMapRoom[fog] {true};
+            #var gMapRoom[exitStr] {};
+        };
+        #else {
+            #var gMapRoom[exitStr] {%%3};
+        };
+
+        #var gMapRoom[existShown] {true};
+
+        #class map.Room.getInfo.map kill;
+        #class map.Room.getInfo.desc kill;
+
+        #if { @option.IsEnable{MapDebug} } {#line gag};
+
+        #if { "$gMapRoom[exitStr]" == "%*。" } {
+            map.parseExit;
+            #return;
+        };
+
+        #class map.Room.getInfo.exit open;
+        #local exitCharsets {和|、|[a-z A-Z]};
+        #action {^{($exitCharsets)+}$} {
+            #var gMapRoom[exitStr] {$gMapRoom[exitStr]%%%1};
+            #if { @option.IsEnable{MapDebug} } {#line gag};
+        };
+
+        #action {^{($exitCharsets)*}。$} {
+            #class map.Room.getInfo.exit kill;
+            #var gMapRoom[exitStr] {$gMapRoom[exitStr]%%%1};
+            map.parseExit;
+        } {4};
+        #class map.Room.getInfo.exit close;
+    };
+
+    #action {^%s这里没有任何明显的出口。$} {
+        #class map.Room.getInfo.map kill;
+        #class map.Room.getInfo.desc kill;
+        #var gMapRoom[existShown] {true};
+        #var {gMapRoom[exits]} {};
+        #if { @option.IsEnable{MapDebug} } {
+            #line gag;
+            #echo {%s} {<134>房间出口<299> %%0};
+        };
+    };
+
+    #action {^%s「%*」: %*{|ID=map/Room/getInfo/weather}$} {
+        #var gMapRoom[season] {%%2};
+        #var gMapRoom[weather] {%%3};
+
+        #class map.Room.getInfo.map kill;
+        #class map.Room.getInfo.desc kill;
+
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<174>时令季节<299> %%2 <174>天气信息<299> %%3};
+            #line gag;
+        };
+    };
+
+    #action {{*UTF8}{^    (\p{Han}+)等(.*)人\(users\)也在此处。$}} {
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {<164>房间玩家<299> 仍有%s等 %d 人。} {%%2} {@math.ParseCN{%%3}};
+            #line gag;
+        };
+    };
+
+    #alias {map.parseExit} {
+        #local exitList {$gMapRoom[exitStr]};
+        #unvar gMapRoom[exitStr];
+
+        #replace exitList {、} {;};
+        #replace exitList { 和 } {;};
+        #replace exitList {。} {};
+        #replace exitList { } {};
+
+        #local exitList {@slist.Sort{$exitList}};
+        #var gMapRoom[exits] {$exitList};
+
+        #if { @option.IsEnable{MapDebug} } {
+            #local exitStr {$gMapRoom[exits]};
+            #if { &gMapRoom[exitHint][] > 0 } {
+                #local template {VALUE<299>(<129>\@if{{"\$gMapRoom[exitHint][VALUE]" == ""};{N/A};{\$gMapRoom[exitHint][VALUE]}}<299>)};
+                #local exitStr {@fp.Transform{{$exitStr};{$template}}};
+            };
+            #local exitStr {@slist.JoinAs{{$exitStr};{<179>%s<299>};{ }}};
+            #echo {%s} {<134>房间出口<299> $exitStr};
+        };
+    };
+
+    #alias {map.Room.getInfo.done} {
+        #local __unused {%%0};
+        #class map.Room.getInfo kill;
+        #class map.Room.getInfo.map kill;
+        #class map.Room.getInfo.desc kill;
+        #unvar gMapRoom[descEnd];
+        #unvar gMapRoom[existShown];
+
+        #if { @slist.Contains{{$gMapRoom[items]};{<ferry>}} } {
+            #var gMapRoom[dock] {ride};
+        };
+
+        #nop 同步事件以允许插件修改房间信息。;
+        event.Emit {map/GotRoomInfo};
+    };
+
+    #class map.Room.getInfo close;
+
+    #class map.Room.getInfo.map open;
+
+    #action {^{?:\s{8}}%*{|ID=map/Room/getInfo/minimap}$} {
+        #local line {%%1};
+        #replace line {%+1..u%+1..s$} {&1};
+        #cat gMapRoom[map] {|$line|};
+        #if { @option.IsEnable{MapDebug} } {
+            #echo {%s} {<174>地图信息<299> %%1};
+            #line gag;
+        };
+    };
+
+    #class map.Room.getInfo.map close;
+
+    #class map.Room.getInfo.desc open;
+
+    #action {^%*{|ID=map/Room/getInfo/others}$} {
+        #class map.Room.getInfo.map kill;
+
+        #if { @isTrue{$gMapRoom[descEnd]} } {
+            #if { "%%0" == "%s" && "$gMapRoom[view]" == "" } {
+                #return;
+            };
+
+            #cat gMapRoom[view] {%%0};
+            map.Room.ShowView {%%0};
+            #return;
+        };
+
+        #if { "%%0" == "%s" } {
+            #var gMapRoom[descEnd] {true};
+            #return;
+        };
+
+        #local obj {@ParseTitle{%%0}};
+        #if { &obj[] > 0 } {
+            #class map.Room.getInfo.desc kill;
+            #var gMapRoom[descEnd] {true};
+            #showme @buffer.RawLine{};
+            #line gag;
+            #return;
+        };
+
+        #local text {%%0};
+        #replace {text} {{*UTF8}{\p{Han}}} {};
+
+        #local origLen {@str.Len{%%0}};
+        #local nonHans {@str.Len{$text}};
+        #if { $origLen > 5 && $nonHans * 2 > $origLen } {
+            #var gMapRoom[descEnd] {true};
+            #cat gMapRoom[view] {%%0};
+            map.Room.ShowView {%%0};
+        };
+        #else {
+            #cat gMapRoom[desc] {@str.Trim{%%0}};
+            #if { @option.IsEnable{MapDebug} } {
+                #echo {%s} {<172>房间描述<299> %%0};
+                #line gag;
+            };
+        };
+    } {9};
+
+    #class map.Room.getInfo.desc close;
+};
+
+#alias {map.Room.ShowView} {
+    #if { @option.IsDisable{ShowRoomView} } {
+        #line gag;
+    };
+    #elseif { @option.IsEnable{MapDebug} } {
+        #echo {%s} {<174>房间风景<299> @Beautify{{%1}}};
+        #line gag;
+    };
+};
+
+#func {map.Room.GetObjByName} {
+    #local name  {%1};
+    #local title {%2};
+
+    #local idx {};
+    #foreach {*gMapRoom[objs][]} {idx} {
+        #if { "$gMapRoom[objs][$idx][title]" == "$title" && "$gMapRoom[objs][$idx][name]" == "$name" } {
+            #return {$gMapRoom[objs][$idx]};
+        };
+    };
+
+    #return {};
+};
+
+#func {map.Room.GetObjByID} {
+    #local id  {%1};
+
+    #local idx {};
+    #foreach {*gMapRoom[objs][]} {idx} {
+        #if { "$gMapRoom[objs][$idx][id]" == "$id" } {
+            #return {$gMapRoom[objs][$idx]};
+        };
+    };
+
+    #return {};
+};

+ 479 - 0
mud/pkuxkx/plugins/basic/map/xiaoyao.tin

@@ -0,0 +1,479 @@
+#nop vim: set filetype=tt:;
+
+/*
+本文件属于 PaoTin++ 的一部分
+===========
+PaoTin++ © 2020~2023 的所有版权均由担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利
+你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。
+===========
+*/
+
+VAR {逍遥行地图数据} map.xiaoyao.map    {};
+VAR {逍遥行房间数据} map.xiaoyao.room   {};
+
+load-lib storage;
+load-lib event;
+
+load-module basic/busy;
+
+event.HandleOnce {map/init} {map/xiaoyao} {map} {xiaoyao.Init};
+
+#alias {xiaoyao.Init} {
+    event.Handle map/walk/continue {xiaoyao.walk-end} {xiaoyao} {xiaoyao.walk-end};
+    storage.Load {map-xiaoyao} {map.xiaoyao.map;map.xiaoyao.room};
+};
+
+#alias {xiaoyao.checkMap} {
+    #local node {};
+    #local areaMap {};
+    #foreach {*map.xiaoyao.map[]} {node} {
+        #local next {};
+        #if { &map.xiaoyao.map[$node][] == 1 && "$map.xiaoyao.map[$node][+1][link]" == "DOCK" } {
+            warnLog 这是个纯粹的码头 => $node;
+        };
+        #foreach {*map.xiaoyao.map[$node][]} {next} {
+            #regex {$node} {%*(%*的%*)} {
+                #format {areaMap[&2][$node]} {true};
+            };
+            #local link {$map.xiaoyao.map[$node][$next][link]};
+            #if { "$link" == "" } {
+                errLog $node => $next 尚未联通。;
+            };
+        };
+    };
+
+    #local maxNodeNum {0};
+    #local maxNodeNumArea {};
+    #local area {};
+    #foreach {*areaMap[]} {area} {
+        okLog $area(&areaMap[$area][]): @slist.Join{{@slist.FromList{@list.FromSlist{*areaMap[$area][]}}};、};
+        #if { &areaMap[$area][] > $maxNodeNum } {
+            #local maxNodeNum {&areaMap[$area][]};
+            #local maxNodeNumArea {$area};
+        };
+    };
+
+    okLog 地图连接性检查正常,一共包含 &map.xiaoyao.map[] 个节点,&areaMap[] 个区域,节点最多的区域是「$maxNodeNumArea」,共有 $maxNodeNum 个节点。;
+};
+
+#alias {xiaoyao.SimpleMap} {
+    #local bigArea {};
+    #local areaMap {};
+    #local area {};
+    #local node {};
+    #foreach {*map.xiaoyao.map[]} {node} {
+        #regex {$node} {%*(%*的%*)} {
+            #format {area} {%s} {&2};
+        };
+        #local bigArea {@map.AreaColor{$area}};
+        #list {areaMap[$bigArea][$area]} add {$node};
+    };
+
+    #local screenWidth {};
+    #screen get cols screenWidth;
+
+    #local lines {};
+    #local line {};
+    #local color {};
+    #local lineWidth {0};
+    #local buttons {};
+    #local bigAreaCount {0};
+    #local areaCount {0};
+    #local nodeCount {0};
+
+    #loop {1} {&areaMap[]} {bigArea} {
+        #math bigAreaCount {$bigAreaCount + 1};
+        #local bgColor *areaMap[+$bigArea];
+        #local fgColor {0};
+        #loop {1} {&areaMap[+$bigArea][]} {area} {
+            #math areaCount {$areaCount + 1};
+            #math fgColor {($fgColor + 1) % 2};
+            #local index {$bgColor};
+            #replace index {%*4%+1d%*} {&2};
+            #if { "$bgColor" == "46" } {
+                #local color {\e[${bgColor};@math.Eval{30 + $fgColor * 4}m};
+            };
+            #elseif { $index == 3 } {
+                #local color {\e[${bgColor};@math.Eval{30 + $fgColor * 4}m};
+            };
+            #else {
+                #local color {\e[${bgColor};@math.Eval{37 - $fgColor * 4}m};
+            };
+            #cat line {$color};
+            #loop {1} {&areaMap[+$bigArea][+$area][]} {node} {
+                #local node {$areaMap[+$bigArea][+$area][+$node]};
+                #local city {$node};
+                #replace city {%S(%S)} {&1};
+                #local width {@math.Eval{ ( @math.Int{@str.Width{$city} * 1.0 / 4 + 0.4} + 1 ) * 4 }};
+                #if { $lineWidth + $width > $screenWidth } {
+                    #if { $screenWidth > $lineWidth } {
+                        #cat line {@str.Space{@math.Eval{$screenWidth - $lineWidth}}};
+                    };
+                    #local lineWidth {0};
+                    #list lines add {{
+                        {text}{$line}
+                        {buttons}{$buttons}
+                    }};
+                    #local line {$color};
+                    #local buttons {};
+                };
+                #cat line {@str.AlignLeft{{$city};$width}};
+                #list buttons add {{
+                    {begin}{@math.Eval{$lineWidth + 1}}
+                    {end}{@math.Eval{$lineWidth + $width}}
+                    {node}{$node}
+                }};
+                #local lineWidth {@math.Eval{ $lineWidth + $width }};
+            };
+        };
+    };
+
+    #list lines add {{
+        {text}{$line}
+        {buttons}{$buttons}
+    }};
+
+    #class xiaoyao.Map kill;
+    #class xiaoyao.Map open;
+    #local lineNo {1};
+    #loop {1} {&lines[]} {lineNo} {
+        #echo {%s} {$lines[$lineNo][text]};
+        #local button {};
+        #foreach {$lines[$lineNo][buttons][]} {button} {
+            #local row {@math.Eval{$lineNo - 4 - &lines[] - $prompt-bot-max-line}};
+            #line sub var #button {$row;$button[begin];$row;$button[end]} {
+                #class xiaoyao.Map kill;
+                #buffer lock off;
+                #buffer end;
+                xiaoyao.Goto $button[node];
+            };
+        };
+    };
+
+    #line oneshot #event {RECEIVED INPUT CHARACTER} {
+        okLog 你略作观察后收起了地图继续赶路。;
+        #class xiaoyao.Map kill;
+        #buffer end;
+    };
+
+    #class xiaoyao.Map close;
+
+    okLog 共包含 &map.xiaoyao.map[] 个节点,$bigAreaCount 个大区,$areaCount 个区域。;
+    #buffer lock on;
+};
+
+#alias {xiaoyao.Map} {
+    #local retry {@defaultNum{%1;0}};
+    #local args  {%2};
+
+    #if { "$args" != "" } {
+        xtt.Send map $args;
+        #return;
+    };
+
+    #local retry {@defaultNum{%1;0}};
+
+    #if { $retry == 0 &&
+        (  "$gMapRoom[node]$gMapRoom[dock]" == ""
+        ||  &gMapRoom[area][] == 0
+        || "$gMapRoom[area][RESOLVED]" == "" )
+    } {
+        event.HandleOnce map/GotArea {xiaoyao/Map} {map} {xiaoyao.Map {@math.Eval{$retry + 1}} $args};
+        map.Here;
+        #return;
+    };
+
+    #local here {@xiaoyao.locate{}};
+    #if { "$here" == "" } {
+        xtt.Send map;
+        #return;
+    };
+
+    #class xiaoyao.Map open;
+
+    #alias {xiaoyao.Map.open} {
+        #class xiaoyao.Map open;
+        #var xiaoyao.Map.lines {0};
+        #action {^%*{|ID=map}$} {#math xiaoyao.Map.lines {$xiaoyao.Map.lines + 1}} {2.5};
+        #sub {~{*UTF8};4;44m{\p{Han}+}} {;4;44m@mslp.Exec{xiaoyao.Map.Goto %%%1;%%%1}};
+        #action {担子炮修订时间} {xiaoyao.Map.close} {2.0};
+        #class xiaoyao.Map close;
+        #if { @existsFile{var/data/map.txt} } {
+            #scan txt var/data/map.txt;
+        };
+        #elseif { @existsFile{mud/$gCurrentMUDLIB/data/map.txt} } {
+            #scan txt mud/$gCurrentMUDLIB/data/map.txt;
+        };
+        #elseif { @existsFile{data/map.txt} } {
+            #scan txt data/map.txt;
+        };
+        #else {
+            errLog 缺少 data/map.txt 文件。;
+            xtt.Send map;
+        };
+    };
+
+    #alias {xiaoyao.Map.close} {
+        #local lines {};
+
+        #screen get rows lines;
+
+        #if { $prompt-bot-max-line > 0 } {
+            #math lines {$lines - $prompt-bot-max-line - 1};
+        };
+
+        #if { $prompt-top-max-line > 0 } {
+            #math lines {$lines - $prompt-top-max-line - 1};
+        };
+
+        #buffer end;
+        keyboard.LessMode;
+
+        #math lines {$xiaoyao.Map.lines - $lines + 2};
+
+        #if { $lines > 0 } {
+            #buffer up $lines;
+        };
+
+        #class xiaoyao.Map kill;
+    };
+
+    okLog <560>你展开地图,发现不知为什么许多地方似乎被人涂成了蓝色。<099>;
+    xiaoyao.Map.open;
+
+    #class xiaoyao.Map close;
+};
+
+#alias {xiaoyao.Map.Goto} {
+    #local node {%1};
+    keyboard.NormalMode;
+    #if { &map.xiaoyao.map[$node(%*)] == 1 } {
+        #local node *map.xiaoyao.map[$node(%*)];
+    };
+    xiaoyao.Goto $node;
+};
+
+#alias {map} {
+    #local width {0};
+    #screen get cols width;
+    #if { $width > 132 } {
+        xiaoyao.Map 0 {%0};
+    };
+    #else {
+        xiaoyao.SimpleMap;
+    };
+};
+
+///=== {
+// ## xiaoyao.Goto <目的节点>
+//    逍遥行快速行走,可以从一个城市移动到另一个城市。支持自动坐船、自动过河。
+//
+//    逍遥行底层采用的是系统 walk 命令,这要求你必须站在逍遥行节点才能使用本别名。
+//    但是本别名<169>可以自动连续 walk<299>,达到长途行走的目的。
+//
+//    为避免重复,完整的逍遥行节点名称采用「<169>节点名(区域的房间名)<299>」格式表达。
+//    目的地暂时仅支持中文,但允许模糊查询。举例来说,假如你想前往「全真派(全真教的宫门)」,
+//    那么你输入「全真派」、「全真教」、「宫门」、「全真」、甚至「教的宫」都是可以的。
+//
+//    本别名也可简写为 <139>xy<299>。许多用户可能喜欢重设为 <139>gt<299>,请自行设定。
+//
+//    关于 walk 命令的细节可以参考 help walk。
+// };
+#alias {xiaoyao.Goto} {
+    #local target   {%1};
+    #local hook     {%2};
+    #local retry    {@defaultNum{%3;0}};
+
+    #if { "%1" == "" } {
+        xtt.Usage xiaoyao.Goto {<169>这里是 PaoTin++ 逍遥行};
+        #return;
+    };
+
+    #if { &map.xiaoyao.map[] == 0 } {
+        errLog 加载逍遥行节点数据文件失败。;
+        okLog 请确保逍遥行数据文件 var/data/map-xiaoyao.tin 或 data/map-xiaoyao.tin 正确无误。;
+        #return;
+    };
+
+    #if { $retry > 1 } {
+        errLog 请先前往逍遥行节点。所有的码头、walk 节点均为逍遥行节点。;
+        #return;
+    };
+
+    #if { "$hook" == "" } {
+        event.HandleOnce map/walk/continue {xiaoyao.walk-end}  {xiaoyao} {xiaoyao.walk-end};
+        #local hook {xiaoyao.walk-end};
+    };
+
+    #if {   "$gMapRoom[node]$gMapRoom[dock]" == ""
+        ||  &gMapRoom[area][] == 0
+        || "$gMapRoom[area][RESOLVED]" == ""
+    } {
+        event.HandleOnce map/GotArea {xiaoyao/Goto} {xiaoyao} {xiaoyao.Goto {$target} {$hook} {@math.Eval{$retry + 1}}};
+        #var map.Locate.mode {CarefulOnce};
+        look;
+        #return;
+    };
+
+    #local here {@xiaoyao.locate{}};
+
+    #if { "$here" == "" } {
+        errLog 请先前往逍遥行节点。所有的码头、walk 节点均为逍遥行节点。;
+        #return;
+    };
+
+    #if { "$here" == "$target" } {
+        #if { "$hook" != "" } {
+            event.DelayEmit map/walk/continue {$hook};
+        };
+        #return;
+    };
+
+    infoLog 计算从<129>$here<299>到<139>$target<299>的路径。;
+    #local target {@xiaoyao.findPath{$here;"NODE" == "%*$target%*"}};
+    #if { "$target" == "" } {
+        errLog 找不到路径。;
+        #return;
+    };
+
+    #if { "$target[path]" == "" } {
+        okLog 你已经来到了 $target[room];
+        #return;
+    };
+
+    okLog 计算结果: {$target[path]};
+    #replace {target[route]} {(%*)} {};
+    okLog 途经节点 $target[route];
+
+    map.WalkNodes {$target[path]} {$hook};
+};
+
+#alias {xiaoyao.walk-end} {
+    okLog 行走完成。;
+    map.Here;
+};
+
+#func {xiaoyao.locate} {
+    #local room     {$gMapRoom[name]};
+    #local area     {@default{$gMapRoom[area][RESOLVED];%*}};
+    #local node     {@default{$gMapRoom[node];%*}};
+    #local dock     {$gMapRoom[dock]};
+    #local location {};
+
+    #if { @slist.Contains{{$gMapRoom[items]};{<node>}} } {
+        #nop 节点以 walk 节点名称标记;
+        #local location {$node($area的$room)};
+    };
+    #elseif { "$dock" != "" } {
+        #nop 没有节点的码头以区域名称加码头标记。;
+        errLog 这是个码头,但没有设置 walk 节点。;
+        #local location {$area码头($area的$room)};
+    };
+    #else {
+        errLog 此处既非码头,也非节点,逍遥行无法定位。;
+        #return {};
+    };
+
+    #nop 如果已经有区域信息,则不用查数据库。;
+    #if { "$gMapRoom[area][RESOLVED]" != "" } {
+        #return {$location};
+    };
+
+    #nop 否则参考数据库来确定,当且仅当数据库中只有一条匹配记录时,才能断定;
+    #if { &map.xiaoyao.map[$location][] != 1 } {
+        #return {};
+    };
+
+    #foreach {*map.xiaoyao.map[$location]} {location} {
+        #return {$location};
+    };
+};
+
+#nop 计算路径;
+#func {xiaoyao.findPath} {
+    #local src  {%1};
+    #local cond {%2};
+    #local dst  {};
+
+    #replace cond  {NODE}   {\$node};
+    #replace cond  {LINK}   {\$map.xiaoyao.map[\$node]};
+
+    #local routeMap {
+        {$src}{START}
+    };
+    #local checkList {{1}{$src}};
+
+    #while {1} {
+        #if { &checkList[] == 0 || ( "$dst" != "" && "$routeMap[$dst]" != "" ) } {
+            #break;
+        };
+
+        #nop 遍历所有新发现的节点;
+        #local nodes        {$checkList};
+        #local checkList    {};
+        #local node         {};
+        #foreach {$nodes[]} {node} {
+            #local next {};
+
+            #local c {};
+            #line sub {var;functions;escapes} #format c {%s} {$cond};
+            #if { $c } {
+                #nop 满足条件的节点。;
+                #local dst {$node};
+                #break;
+            };
+
+            #foreach {*map.xiaoyao.map[$node][]} {next} {
+                #if { "$routeMap[$next]" != "" } {
+                    #nop 已经检索过的节点。;
+                    #continue;
+                };
+
+                #local link {$map.xiaoyao.map[$node][$next][link]};
+                #if { "$link" == "" } {
+                    #nop BUG: 不完整的连接。;
+                    #continue;
+                };
+
+                #list {checkList} {add} {$next};
+                #local routeMap[$next] {$node};
+            };
+        };
+    };
+
+    #if { "$dst" == "" || "$routeMap[$dst]" == "" } {
+        #return {};
+    };
+
+    #local route    {$dst};
+    #local path     {};
+    #local node     {$dst};
+    #while { "$node" != "$src" } {
+        #local prev     {$routeMap[$node]};
+        #local link     {$map.xiaoyao.map[$prev][$node][link]};
+        #local action   {$map.xiaoyao.map[$prev][$node][action]};
+        #local node     {$prev};
+
+        #format route   {%s-%s} {$node} {$route};
+
+        #if { "$link" == "WALK" } {
+            #list path insert 1 {$action};
+        };
+        #elseif { "$link" == "PATH" } {
+            #list path insert 1 {PATH/{$action}};
+        };
+        #elseif { "$link" == "DOCK" } {
+            #list path insert 1 {DOCK/$action};
+        };
+    };
+
+    #list path {simplify};
+
+    #return {
+        {room}{$dst}
+        {route}{$route}
+        {path}{$path}
+    };
+};
+
+#alias {xy} {xiaoyao.Goto};