Преглед изворни кода

feat: 基本框架,语法增强,基本 UI

dzp пре 3 година
родитељ
комит
c6ab7e7153

+ 88 - 0
etc/ui-settings.tin

@@ -0,0 +1,88 @@
+#nop {Top} {TopSepBar} {MidSepBar} {Bot} {BotSepBar};
+#list prompt-fields create {
+    { {place}{MidSepBar}        {label}{重启}       {name}{reboot}   {countdown}{Auto} }
+    { {place}{MidSepBar}        {label}{经验转化率} {name}{pot/exp}  }
+    { {place}{MidSepBar}        {label}{签到}       {name}{sign}     }
+    { {place}{MidSepBar}        {label}{铜雀台}     {name}{tqt}      }
+
+    { {place}{MidSepBar}        {label}{衣钵传承}   {name}{yibo}     }
+    { {place}{MidSepBar}        {label}{药炉}       {name}{stove}    }
+
+    { {place}{MidSepBar}        {label}{亲戚}       {name}{renqin}   }
+    { {place}{MidSepBar}        {label}{燕青拳}     {name}{yanqing}  }
+
+    { {place}{MidSepBar}        {label}{保卫}       {name}{baowei}   }
+    { {place}{MidSepBar}        {label}{答题}       {name}{dati}     }
+    { {place}{MidSepBar}        {label}{比武}       {name}{biwu}     {countdown}{Auto} }
+    { {place}{MidSepBar}        {label}{福米}       {name}{fullme}   {cooldown}{600} }
+    { {place}{MidSepBar}        {label}{URL}        {name}{URL}      {cooldown}{180} }
+
+    { {place}{Bot} {line}{1}    {label}{经验}       {name}{exp}      {width}{8}  {visibility}{Always} }
+    { {place}{Bot} {line}{1}    {label}{本次新增}   {name}{expDelta} {width}{6}  {visibility}{HideLabel} {cooldown}{600} }
+    { {place}{Bot} {line}{1}    {label}{增速}       {name}{expSpd}   {width}{8}  {cooldown}{600} }
+    { {place}{Bot} {line}{1}    {label}{潜能}       {name}{pot}      {width}{8}  {visibility}{Always} }
+    { {place}{Bot} {line}{1}    {label}{本次新增}   {name}{potDelta} {width}{6}  {visibility}{HideLabel} {cooldown}{600} }
+    { {place}{Bot} {line}{1}    {label}{气血恢复}   {name}{yunqi}    {width}{5}  {cooldown}{60}  }
+    { {place}{Bot} {line}{1}    {label}{存款}       {name}{saving}   {width}{10} {cooldown}{1800}  }
+    { {place}{Bot} {line}{1}    {label}{现金}       {name}{cash}     {width}{10} {cooldown}{600}   }
+    { {place}{Bot} {line}{1}    {label}{收益速度}   {name}{profit}   {width}{10} {cooldown}{600}   }
+
+    { {place}{Bot} {line}{2}    {label}{不利战况}   {name}{chousui}  {visibility}{Always}    {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{透骨钉}     {name}{tougu}    {visibility}{HideLabel} {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{刺穴}       {name}{cixue}    {visibility}{HideLabel} {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{刺目}       {name}{cimu}     {visibility}{HideLabel} {cooldown}{10} {color}{<170><570>} }
+    { {place}{Bot} {line}{2}    {label}{刺腕}       {name}{ciwan}    {visibility}{HideLabel} {cooldown}{10} {color}{<510>} }
+    { {place}{Bot} {line}{2}    {label}{内力不济}   {name}{neili}    {visibility}{HideLabel} {cooldown}{10} {color}{<110><510>} }
+
+    { {place}{Bot} {line}{2}    {label}{中毒情况}   {name}{bingpo1}  {visibility}{Always}    {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{星宿毒掌}   {name}{duzhang1} {visibility}{HideLabel} {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{情毒}       {name}{qingdu1}  {visibility}{HideLabel} {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{凝血神爪毒} {name}{zhuadu1}  {visibility}{HideLabel} {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{生死符}     {name}{ssfu1}    {visibility}{HideLabel} {cooldown}{60} }
+
+    { {place}{Bot} {line}{2}    {label}{有利战况}   {name}{ssfu2}    {visibility}{Always}    {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{日魂激发}   {name}{rihun}    {visibility}{HideLabel} {cooldown}{60} }
+    { {place}{Bot} {line}{2}    {label}{毒免}       {name}{dumian}   {visibility}{HideLabel} {cooldown}{60} }
+
+    { {place}{Bot} {line}{3}    {label}{状态}       {name}{status}   {cooldown}{600} }
+    { {place}{Bot} {line}{3}    {label}{持续效果}   {name}{persist}  {countdown}{Seconds} }
+
+    { {place}{BotSepBar}        {label}{美化 / Ctrl+B 切换}  {name}{beautify} {cooldown}{600} {visibility}{HideCool} }
+    { {place}{BotSepBar}        {label}{状态栏更新} {name}{disable}  {visibility}{HideLabel} }
+    { {place}{BotSepBar}        {label}{任务}       {name}{job}     }
+    { {place}{BotSepBar}        {label}{阶段}       {name}{stage}   {cooldown}{1200} {countdown}{Auto} }
+    { {place}{BotSepBar}        {label}{区域}       {name}{area}    {cooldown}{1200} }
+    { {place}{BotSepBar}        {label}{地点}       {name}{room}    {cooldown}{1200} }
+    { {place}{BotSepBar}        {label}{目标}       {name}{target}  }
+    { {place}{BotSepBar}        {label}{类型}       {name}{type}    }
+};
+
+#nop 配色主题,注意这里不要直接嵌入 SGR(ansi codes),否则计算宽度时会有问题。;
+#var prompt-theme {
+    {Disable}{<bbc>}
+    {BusyColor}{<030>}
+    {BattleColor}{<110>}
+    {BattleBusyColor}{<500><110>}
+    {TopSepBar}{<020>}
+    {MidSepBar}{<020>}
+    {BotSepBar}{<020>}
+    {HotLabel}{<100><F399>}
+    {CoolLabel}{<100>}
+    {Value}{<070>}
+};
+
+#nop 自定义图标;
+#var prompt-icon {
+    {DisableRefresh}{🚫}
+};
+
+#var prompt-prompt {};
+
+#nop 热键绑定;
+#list global-key-bindings create {
+    {{key}{\cs}     {action}{prompt.ToggleSwitch}}
+    {{key}{\cb}     {action}{beautify.ToggleSwitch}}
+    {{key}{\cd}     {action}{xtt.ToggleDebug}}
+    {{key}{\ce}     {action}{option.Toggle EchoCommand}}
+    {{key}{\col}    {action}{option.List}}
+};

+ 55 - 0
framework/class.tin

@@ -0,0 +1,55 @@
+#list xtt-class-stack create {};
+
+#alias {class.name} {
+    #local currentClassName {${xtt-class-stack[-1]}};
+};
+
+#alias {class.open} {
+    #local className {%1};
+
+    #list {xtt-class-stack} {add} {$className};
+    #class {$className} open;
+};
+
+#alias {class.close} {
+    #local className {%1};
+    #class {$className} close;
+
+    #list {xtt-class-stack} {delete} {-1};
+    #local preClassName {${xtt-class-stack[-1]}};
+    #if { "$preClassName" != "" } {
+        #class {$preClassName} open;
+    };
+};
+
+#alias {class.kill} {
+    #local className {%1};
+    #class {$className} kill;
+};
+
+#alias {class.read} {
+    #local className {%1};
+    #local filePath {%2};
+
+    class.open {$className};
+    class.do {$className} {load-file $filePath};
+    class.close {$className};
+};
+
+#alias {class.do} {
+    #local className {%1};
+    #local code      {%2};
+
+    #class {$className} {assign} {$code};
+};
+
+#alias {class.enable} {
+    #local className {%1};
+    #class {$className} {load};
+};
+
+#alias {class.disable} {
+    #local className {%1};
+    #class {$className} {save};
+    #class {$className} {clear};
+};

+ 96 - 0
framework/log.tin

@@ -0,0 +1,96 @@
+#nop 日志模块;
+
+#var gLog[buffer]   {buffer.log};
+#var gLog[socket]   {socket.log};
+
+#var gLog[info]     {info.log};
+#var gLog[ok]       {ok.log};
+#var gLog[warn]     {warn.log};
+#var gLog[error]    {error.log};
+#var gLog[debug]    {debug.log};
+
+#var gLog[PATH]     {log};
+
+#function {InitLog} {
+    #local path {%1};
+
+    #script output {test -d var && echo true || echo false};
+    #if { "$output[1]" == "true" } {
+        #var gLog[PATH] {var/log};
+    };
+    #else {
+        #var gLog[PATH] {log};
+    };
+
+    #if { "@mkdir{$gLog[PATH]/$path}" != "true" } {
+        #return {false};
+    };
+
+    #var gLog[PATH] {$gLog[PATH]/$path};
+
+    load-lib option;
+    option.Define {EchoCommand} {Bool} {是否回显发送的命令} {false};
+
+    #return {true};
+};
+
+#alias {mudLog}     {log.write {$gLog[socket]} {%0}};
+
+#alias {okLog}      {log.write {$gLog[ok]}     {<120>%0}   {ECHO}};
+#alias {warnLog}    {log.write {$gLog[warn]}   {<130>%0}   {ECHO}};
+#alias {errLog}     {log.write {$gLog[error]}  {<110>%0}   {ECHO}};
+#alias {infoLog}    {log.write {$gLog[info]}   {%0}        {ECHO}};
+#alias {dbgLog}     {log.write {$gLog[debug]}  {%0}};
+
+#alias {{[a-z]{1,10}}Log %*} {
+    #format logName {%l} {%1};
+    log.write {${logName}.log} {%2};
+};
+
+#alias {log.write} {
+    #local file {%1};
+    #local text {%2};
+    #local echo {%3};
+
+    #line log {$gLog[PATH]/$file} {$text<070>};
+
+    #if { "$echo" == "ECHO" } {
+        #echo {%s} {$text<070>};
+    };
+};
+
+#event {RECEIVED INPUT} {
+    #local needEcho {false};
+
+    #if { @option.IsDisable{EchoCommand} } {
+        #local needEcho {true};
+    };
+    #else {
+        #line sub escape #var tmp {%0};
+        #local cmds {};
+        #list cmds create {$tmp};
+        #if { &cmds[] > 1 } {
+            #local needEcho {true};
+        };
+    };
+
+    #if { "$needEcho" == "true" } {
+        #echo {<020>%t INPUT: <420>%s<070>} {%Y-%m-%d %H:%M:%S} {<420>%0<070>};
+    };
+};
+
+#alias {log.Open} {
+    #config {LOG} {RAW};
+    #config {LOG LEVEL} {HIGH};
+    #log timestamp {%Y-%m-%d %H:%M:%S };
+    #log append {$gLog[PATH]/$gLog[buffer]};
+    #event {RECEIVED LINE} {mudLog %%0};
+    #event {SEND OUTPUT} {
+        #local text {};
+        #format text {<020>SEND: <420>%p<070>} {%%0};
+        mudLog $text;
+        #if { @option.IsEnable{EchoCommand} } {
+            #echo {<020>%t %s} {%Y-%m-%d %H:%M:%S} {$text};
+        };
+    };
+};

+ 101 - 0
framework/main.tin

@@ -0,0 +1,101 @@
+#nop 模块名称:框架主程序;
+#nop 模块说明:本文件属于框架代码的一部分,不建议修改。如有需求请在 GitHub 发 issue 或者 PR;
+#nop 版权声明:本文件属于《担子炮 TinTin 套装》的一部分;
+#nop ===========;
+#nop 《担子炮 TinTin 套装》的所有版权均由 © 2020 担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利;
+#nop 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。;
+#nop ===========;
+
+#class main open;
+
+#kill all;
+
+#event {PROGRAM START} {
+    #if { "$user[id]" != "{[A-Za-z0-9]+}" } {
+        #echo {<110>请参考使用文档指定用户正确的 user id。<070>};
+        #return;
+    };
+
+    #if { "@InitLog{$user[id]}" != "true" } {
+        #echo {<110>创建日志目录 $gLog[PATH]/$user[id] 时遇到错误。<070>};
+        #echo {<130>请检查你的安装环境,或者参考使用手册重新安装本软件。<070>};
+        #return;
+    };
+
+    load-module basic/login;
+    auto-login;
+};
+
+#alias {auto-login} {
+    login {$session} {$user} {
+        load-file {framework/online.tin};
+    };
+};
+
+#event {SESSION CREATED} {
+    #%0 {
+        log.Open;
+        load-module lib/ui/prompt;
+        load-module lib/ui/beautify;
+        load-module lib/event;
+    };
+};
+
+#event {SESSION TIMED OUT} {
+    #echo {%s} {<110>连接服务器超时,稍后自动重试。<070>};
+    #gts #delay 3 auto-login;
+};
+
+#event {SESSION DISCONNECTED} {
+    #if { "%0" == "$session[name]" } {
+        #local reconnect {$session[reconnect]};
+        #if { "$session[remote_maint]" == "true" } {
+            #local reconnect {$session[reconnect_slow]};
+            #var session[remote_maint] {false};
+        };
+
+        #if { "$reconnect" == "{|0}" } {
+            #local reconnect 12;
+        };
+
+        #nop 断开连接后再次重连不要太频繁,以免服务器不高兴。;
+        #echo {%s} {<110>连接已被服务器断开,$reconnect 秒后自动重连。<070>};
+        #gts #delay {$reconnect} {
+            #echo {%s} {<120>自动重连。<070>};
+            auto-login;
+        };
+    };
+};
+
+#alias {load-file} {
+    #local file {%1};
+    #local output {};
+    #script output {test -f var/$file && echo true || echo false};
+    #if { "$output[1]" == "true" } {
+        #read var/$file;
+    };
+    #else {
+        #read $file;
+    };
+};
+
+#alias {init} {
+    #nop 调整 tintin 环境;
+    load-file framework/settings.tin;
+    #nop 框架依赖的基本函数;
+    load-file framework/utils.tin;
+    #nop 日志支持;
+    load-file framework/log.tin;
+    #nop 为 TinTin 赋能,实现模块加载器;
+    load-file framework/module-loader.tin;
+
+    #nop 为 TinTin 赋能,自行实现的扩展语法和实用函数集。;
+    load-lib xtintin;
+
+    #nop 默认的用户环境配置;
+    load-file ids/DEFAULT;
+};
+
+init;
+
+#class main close;

+ 419 - 0
framework/module-loader.tin

@@ -0,0 +1,419 @@
+#nop 模块名称:模块加载器;
+#nop 模块说明:本文件属于框架代码的一部分,不建议修改。如有需求请在 GitHub 发 issue 或者 PR;
+#nop 版权声明:本文件属于《担子炮 TinTin 套装》的一部分;
+#nop ===========;
+#nop 《担子炮 TinTin 套装》的所有版权均由 © 2020 担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利;
+#nop 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。;
+#nop ===========;
+
+load-file {framework/class.tin};
+
+class.open module-loader;
+
+#var xtt-modules    {};
+#var xtt-modConfig  {};
+
+#alias {LM} {load-module};
+#alias {load-module} {
+    #local moduleName   {%1};
+    #local moduleConfig {%2};
+
+    #if { "$moduleName" == "" } {
+        warnLog 用法: load-module <模块全限定名称> <模块参数>;
+        #return;
+    };
+
+    #nop 检查有没有加载成功。;
+
+    #local prefix {$moduleName};
+    #replace prefix {/} {_};
+    #if { "${${prefix}-loaded}" == "true" } {
+        infoLog 模块 $moduleName 已载入。;
+        #if { "${xtt-modules[$moduleName][ENABLE]}" == "false" } {
+            infoLog 但是该模块已经被禁用,如果你想要重新启用,请使用「<120>enable-module %1<070>」。;
+        };
+        #return;
+    };
+
+    #local path {};
+    #foreach {$moduleName;$moduleName/__init__;$moduleName/__main__} {path} {
+        class.open {load-module-$moduleName};
+        #line sub var #action {^#READ {{plugins/($path)\.tin}} - FILE NOT FOUND.$} {
+            class.kill {load-module-$moduleName};
+            #var {${prefix}-loaded} {false};
+            #line gag;
+        };
+        class.close {load-module-$moduleName};
+
+        class.open module-loader;
+        #var {${prefix}-loaded} {true};
+        class.close module-loader;
+
+        class.open {$moduleName};
+        #var {${prefix}} {};
+        class.close {$moduleName};
+
+        #format path {plugins/%s.tin} {$path};
+        #line local {
+            #nop 模块名称;
+            #local MODULE {$moduleName};
+            #nop 模块路径;
+            #local PATH {$path};
+            #nop 模块中触发器的默认 ID;
+            #local ID     {%!{|id=${moduleName}}};
+            #nop 文本开始;
+            #local {B}    {^{[> ]*}};
+            #nop 文本结束;
+            #local {E}    {%!{|id=${moduleName}}$};
+            #nop 颜色触发中的文本结束;
+            #local {CE}   {%!{\e\[[0-9\;]*[mK]}$E};
+            class.read {$moduleName} {$path};
+        };
+
+        #if { "${${prefix}-loaded}" == "true" } {
+            class.kill {load-module-$moduleName};
+            init-module {$moduleName} {$path} {$moduleConfig};
+            #return;
+        };
+    };
+
+     errLog 模块 $moduleName 加载失败。;
+    #return;
+};
+
+#alias {init-module} {
+    #local moduleName   {%1};
+    #local modulePath   {%2};
+    #local modConfig    {%3};
+
+    #local prefix       {$moduleName};
+    #replace prefix {/} {_};
+
+    #nop 检查有没有加载成功。;
+    #if { "${${prefix}-loaded}" != "true" } {
+        #return;
+    };
+
+    #nop 检查有没有初始化过;
+    #if { "${xtt-modules[$moduleName]}" != "" } {
+        #return;
+    };
+
+    #local metaVarName  {};
+    #format metaVarName {%s[META]} {$prefix};
+    #local metaInfo {
+        ${$metaVarName}
+        {PATH}{$modulePath}
+        {ENABLE}{true}
+    };
+
+    #if { "${$metaVarName}" == "" } {
+        #local metaInfo[TYPE] {弱模块};
+    };
+    #elseif { "@existsAlias{${moduleName}.Run}" == "true" } {
+        #local metaInfo[TYPE] {纯模块};
+    };
+    #else {
+        #local metaInfo[TYPE] {混合模块};
+    };
+
+    #format xtt-modules {%s%s} {${xtt-modules}} {{$moduleName}{$metaInfo}};
+
+    #if { "$metaInfo[TYPE]" != "弱模块" } {
+        #if { "$metaInfo[CONFIG]" != "" } {
+            #foreach {*metaInfo[CONFIG][]} {key} {
+                class.open {$moduleName};
+                #var ${prefix}[config][$key] {$modConfig[$key]};
+                class.close {$moduleName};
+                class.open module-loader;
+                #var xtt-modConfig[$moduleName][$key] {$modConfig[$key]};
+                class.close module-loader;
+            };
+        };
+
+        #local initFunc {${prefix}.Init};
+        #if { "@existsFunction{$initFunc}" == "true" } {
+            class.open {$moduleName};
+            #local ok @$initFunc{};
+            class.close {$moduleName};
+            #if { "$ok" == "false" } {
+                errLog 模块 $moduleName 加载失败。;
+                class.kill {$moduleName};
+                #return;
+            };
+        };
+    };
+
+    okLog 模块 $moduleName 已初始化成功。;
+
+    #if { "$metaInfo[TYPE]" == "纯模块" } {
+        okLog 本模块是纯模块,请使用 ${moduleName}.Start/Stop 来控制启动停止。;
+        disable-module {$moduleName} {true};
+        make-starter {$moduleName} {$prefix};
+    };
+};
+
+#alias {KM} {kill-module};
+#alias {kill-module} {
+    #local moduleName   {%1};
+    #if { "$moduleName" == "" } {
+        warnLog 用法: reload-module <模块全限定名称>;
+        #return;
+    };
+
+    infoLog 卸载 $moduleName。;
+    #local modules {};
+    #foreach {*{xtt-modules[]}} {name} {
+        #if { "$name" != "$moduleName" } {
+            #format modules {%s%s} {$modules} {{$name}{${xtt-modules[$name]}}};
+        };
+    };
+    #var {xtt-modules} {$modules};
+
+    #local prefix {$moduleName};
+    #replace prefix {/} {_};
+    #unvar {${prefix}-loaded};
+
+    class.kill {$moduleName};
+};
+
+#alias {RLM} {reload-module};
+#alias {reload-module} {
+    #local moduleName {%1};
+    #if { "$moduleName" == "" } {
+        warnLog 用法: reload-module <模块全限定名称>;
+        #return;
+    };
+    kill-module %1;
+    load-module %1;
+};
+
+#alias {DM} {disable-module};
+#alias {disable-module} {
+    #local moduleName   {%1};
+    #local dontCallStop {%2};
+
+    #if { "${xtt-modules[$moduleName][TYPE]}" == "纯模块" && "$dontCallStop" != "true" } {
+        ${moduleName}.Stop;
+        #return;
+    };
+
+    #if { "$moduleName" == "" } {
+        warnLog 用法: disable-module <模块全限定名称>;
+        #return;
+    };
+
+    #if { "${xtt-modules[$moduleName]}" == "" } {
+        errLog 模块 $moduleName 尚未加载模块。;
+        #return;
+    };
+
+    #if { "${xtt-modules[$moduleName][ENABLE]}" == "false" } {
+        warnLog 模块 $moduleName 已经禁用。;
+        #return;
+    };
+
+    #var xtt-modules[$moduleName][ENABLE] {false};
+
+    class.disable {$moduleName};
+    infoLog <120>模块 $moduleName 已经禁用。<070>;
+};
+
+#alias {EM} {enable-module};
+#alias {enable-module} {
+    #local moduleName       {%1};
+    #local dontCallStart    {%2};
+
+    #if { "${xtt-modules[$moduleName][TYPE]}" == "纯模块" && "$dontCallStart" != "true" } {
+        ${moduleName}.Start;
+        #return;
+    };
+
+    #if { "$moduleName" == "" } {
+        warnLog 用法: enable-module <模块全限定名称>;
+        #return;
+    };
+
+    #if { "${xtt-modules[$moduleName]}" == "" } {
+        errLog 模块 $moduleName 尚未加载模块。;
+        #return;
+    };
+
+    #if { "${xtt-modules[$moduleName][ENABLE]}" == "true" } {
+        warnLog 模块 $moduleName 已经启用。;
+        #return;
+    };
+
+    #var xtt-modules[$moduleName][ENABLE] {true};
+
+    class.enable {$moduleName};
+    infoLog <120>模块 $moduleName 已经启用。<070>;
+};
+
+#nop 列出所有已加载模块以及它们的开关状态、配置参数;
+#alias {MODS} {list-modules};
+#alias {list-modules} {
+    #local _ {%0};
+    #local format {  %c%-30s %-10s %-10s %-6s %-6s %s};
+    #echo {%c%h}    {cyan} { 已加载模块 -- 请用 MOD <模块名> 来查看详细内容 };
+    #echo {$format} {cyan} {模块名称} {作者} {类型} {状态} {配置项} {说明};
+    #echo {$format} {cyan} {------------} {----------} {-------} {----} {----} {------------------------------------};
+    #local name {};
+    #local count {0};
+    #foreach {*{xtt-modules[]}} {name} {
+        #math count {$count + 1};
+        #local metaInfo {${xtt-modules[$name]}};
+        #local cnName   {$metaInfo[NAME]};
+        #local author   {$metaInfo[AUTHOR]};
+        #local type     {$metaInfo[TYPE]};
+        #local desc     {$metaInfo[DESC]};
+        #local config   {$metaInfo[CONFIG]};
+        #local enable   {<120>开启<070>};
+        #local hasConfig {有};
+
+        #if { "$cnName" != "" } {
+            #local cnName {(<160>$cnName<070>)};
+        };
+
+        #if { "$config" == "" } {
+            #local hasConfig {无};
+        };
+
+        #if { "$type" == "纯模块" } {
+            #local type {<120>纯模块<070>};
+        };
+        #elseif { "$type" == "混合模块" } {
+            #local type {<130>混合模块<070>};
+        };
+
+        #if { "$metaInfo[ENABLE]" == "false" } {
+            #local enable {<110>禁用<070>};
+        };
+
+        #echo {$format} {white} {$name $cnName} {$author} {$type} {$enable} {$hasConfig} {$desc};
+    };
+    #echo {%c%h} {cyan} { 共列出 $count 项模块信息 };
+};
+
+#nop 列出所有已加载模块以及它们的开关状态、配置参数;
+#alias {MOD} {look-module};
+#alias {look-module} {
+    #local moduleName   {%1};
+    #local metaInfo     {${xtt-modules[$moduleName]}};
+    #local cnName       {$metaInfo[NAME]};
+    #local path         {$metaInfo[PATH]};
+    #local type         {$metaInfo[TYPE]};
+    #local enable       {$metaInfo[ENABLE]};
+    #local desc         {$metaInfo[DESC]};
+    #local author       {$metaInfo[AUTHOR]};
+    #local note         {$metaInfo[NOTE]};
+    #local config       {$metaInfo[CONFIG]};
+
+    #if { "$moduleName" == "" } {
+        warnLog 用法: look-module <模块全限定名称>;
+        #return;
+    };
+
+    #if { "${xtt-modules[$moduleName]}" == "" } {
+        errLog 模块 $moduleName 尚未加载。;
+        #return;
+    };
+
+    #if { "$enable" == "false" } {
+        #local enable {<110>禁用<070>};
+    };
+    #else {
+        #local enable {<120>开启<070>};
+    };
+
+    #if { "$cnName" != "" } {
+        #local cnName { (<160>$cnName<070>)<060>};
+    };
+
+    #local format {    %c%-20s %-20s %s};
+    #echo {%c%h}    {cyan} { $moduleName$cnName };
+    #echo {%s}      {    <060>名称:<070> $moduleName$cnName <060>类型:<070> $type <060>状态:<070> $enable <060>作者:<070> $author};
+    #echo {%s}      {    <060>脚本路径:<070> $path};
+    #if { "$type" == "弱模块" } {
+        #echo {%c%h} {cyan} { 更多信息仅纯模块可见 };
+        #return;
+    };
+
+    #echo {%s}      {    <060>说明:<070> $desc};
+    #echo {%s}      {    <060>备注:<070> $note};
+    #echo {%s}      {    <060>接口:<070> 你可以通过 $moduleName\.Start/Stop 命令来启动/停止模块。};
+    #echo {%s}      {    <060>配置参数表:<070>};
+    #echo {$format} {cyan} {配置项} {当前值} {作用};
+    #echo {$format} {cyan} {--------------------} {--------------------} {------------------------------};
+    #local key {};
+    #local count {0};
+    #foreach {*config[]} {key} {
+        #math count {$count + 1};
+        #local value ${xtt-modConfig[$moduleName][$key]};
+        #local desc  {$config[$key]};
+        #local prefix {$moduleName};
+        #replace prefix {/} {_};
+        #echo {$format} {light cyan} {$key} {$value} {$desc};
+    };
+
+    #echo {%c%h} {cyan} { 共列出 $count 项配置信息 };
+};
+
+#alias {make-starter} {
+    #local moduleName   {%1};
+    #local prefix       {%2};
+
+    class.open module-loader;
+
+    #tab {%1.Start};
+    #alias {%1.Start} {
+        #if { "${xtt-modules[%1][ENABLE]}" == "false" } {
+            enable-module {%1} {true};
+        };
+
+        #local config   {%%0};
+        #local metaInfo {${%2[META]}};
+
+        #foreach {*metaInfo[config][]} {key} {
+            class.open {%1};
+            #format {%2[config][$key]} {%s} {$config[$key]};
+            class.close {%1};
+        };
+
+        #format xtt-modConfig {%s%s} {${xtt-modConfig}} {{%1}{${%2[config]}}};
+
+        class.do {%1} %1.Run;
+    };
+
+    #tab {%1.Stop};
+    #alias {%1.Stop} {
+        #local reason {%%1};
+
+        #if { "$reason" == "" } {
+            #format reason {人为操作};
+        };
+
+        infoLog <160>由于<130>$reason<160>,%1 模块停止运行。<070>;
+
+        #if { "@existsAlias{%1.Pause}" == "true" } {
+            %1.Pause;
+        };
+
+        #if { "${xtt-modules[%1][ENABLE]}" == "true" } {
+            disable-module {%1} {true};
+        };
+    };
+
+    class.close module-loader;
+};
+
+#alias {LL} {load-lib};
+#alias {load-lib} {load-module lib/%1};
+
+#alias {RLL} {reload-lib};
+#alias {reload-lib} {
+    kill-module lib/%1;
+    load-lib %1;
+};
+
+class.close module-loader;

+ 42 - 0
framework/online.tin

@@ -0,0 +1,42 @@
+#nop 几个常用的别名;
+
+#nop 一些中文 MUD 服务器在接收到 quit 命令时会让角色从服务器上下线,;
+#nop 这往往会导致玩家丢失背包里的物品。;
+#nop 为了避免悲剧发生,这里特别映射一下,改成仅断开连接,而不退出服务器角色。;
+#nop 如果玩家真的需要向服务器发送 quit 指令,请输入 #send quit。exit 同理;
+#alias {exit} {#end};
+#alias {quit} {#end};
+
+#nop 类似于 DOS/Unix,设置 clear/cls 命令为清除屏幕上的内容。;
+#alias {cls} {clear};
+#alias {clear} {
+    #system {tput clear};
+    prompt.refresh;
+};
+
+#nop 武当、少林等门派的诵经文本会干扰机器运行,使用全局替换从一开始就屏蔽掉。;
+#substitute {~\e[1;36m%+1u..\e[2;37;0m} {};
+
+#ticker {save-data}     {xtt.Send save}     {600};
+#ticker {backup-data}   {xtt.Send backup}   {1800};
+
+#delay 1 {
+    set table_pattern 1;
+    set area_detail 1;
+    set learn_emote 1;
+    set localmap 1;
+
+    load-module basic/char;
+    load-module shortcut;
+
+    event.handle {char/hpbrief} {prompt} {framework/online} {prompt.UpdateHP};
+    event.handle {char/status}  {prompt} {framework/online} {prompt.UpdateSM};
+
+    #nop 提供给用户的自动执行函数;
+    user-online;
+};
+
+#event {RECEIVED OUTPUT} {
+    #undelay KeepAlive;
+    #delay KeepAlive {#zap} 180;
+};

+ 17 - 0
framework/settings.tin

@@ -0,0 +1,17 @@
+#nop 默认使用 GBK 编码,国内最常见的编码。支持通过触发动态切换编码。;
+#config {charset} {GBK1TOUTF8};
+#nop 网络延迟较大的时候,设置较大的包延迟选项可以尽量避免命令输出接受不完整。;
+#config {packet patch} {0.8};
+#nop 颜色代码的影响不会持续到下一行,防止未闭合的文字特效污染后续文本。;
+#config {color patch} {on};
+#nop 按回车键重复输入之前的命令。;
+#config {repeat enter} {on};
+
+#nop 关闭 MCCP 协议。;
+#config {mccp} {off};
+
+#nop 开启 wordwrap 常常导致折行的文本显示为乱码,因此要关掉它;
+#config {wordwrap} {off};
+
+#nop 切分出专门的文字输入区。;
+#split;

+ 72 - 0
framework/utils.tin

@@ -0,0 +1,72 @@
+#function {existsAlias} {
+    #local pName     {%1};
+    #local pClass    {%2};
+
+    #info alias save;
+
+    #local idx {};
+    #foreach {*{info[ALIASES][]}} {idx} {
+        #local name  {$info[ALIASES][$idx][arg1]};
+        #local class {$info[ALIASES][$idx][class]};
+
+        #if { "$name" == "$pName" && ( "$pClass" == "" || "$class" == "$pClass" ) } {
+            #return {true};
+        };
+    };
+
+    #return {false};
+};
+
+#function {existsFunction} {
+    #local pName    {%1};
+    #local pClass   {%2};
+
+    #info function save;
+
+    #local idx {};
+    #foreach {*{info[FUNCTIONS][]}} {idx} {
+        #local name  {$info[FUNCTIONS][$idx][arg1]};
+        #local class {$info[FUNCTIONS][$idx][class]};
+
+        #if { "$name" == "$pName" && ( "$pClass" == "" || "$class" == "$pClass" ) } {
+            #return {true};
+        };
+    };
+
+    #return {false};
+};
+
+#function {existsFile} {
+    #local file {%1};
+    #local output {};
+    #script output {test -f $file && echo true || test -f var/$file && echo true || echo false};
+    #return $output[1];
+};
+
+#function {existsJobPlugin} {
+    #local job {%1};
+    #return {@existsPlugin{job/$job}};
+};
+
+#function {existsPlugin} {
+    #local plugin {%1};
+    #return {@existsFile{plugins/$plugin.tin}};
+};
+
+#function {mkdir} {
+    #local dir {%1};
+    #local output {};
+    #script output {mkdir -p $dir 2>/dev/null && test -d $dir && echo true || echo false};
+    #return $output[1];
+};
+
+#function {uuid} {
+    #local now      {};
+    #local random   {};
+
+    #math random {1d1000};
+    #format random {%%03d} {$random};
+    #format now {%U};
+
+    #return {${now}.$random};
+};

+ 44 - 0
ids/DEFAULT

@@ -0,0 +1,44 @@
+#nop vim: filetype=tt
+#nop 模块名称:默认环境信息;
+#nop 模块说明:本文件属于框架代码的一部分,一般情况不建议修改。如果需要定制,请在用户配置里进行。;
+#nop           用户配置文件的模版请参见 ids/EXAMPLE
+#nop 版权声明:本文件属于《担子炮 TinTin 套装》的一部分;
+#nop ===========;
+#nop 《担子炮 TinTin 套装》的所有版权均由 © 2020 担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利;
+#nop 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。;
+#nop ===========;
+
+#nop session 的名字,没特殊需求不建议修改;
+#var session[name]  pkuxkx;
+
+#nop session 的主机名,海外用户需要用 pkuxkx.com 或 hk.pkuxkx.com;
+#var session[host]  hk.pkuxkx.com;
+#var session[host]  pkuxkx.com;
+#var session[host]  mud.pkuxkx.net;
+
+#nop session 的端口,按照论坛上面的地址修改;
+#var session[port]  8081;
+#var session[UTF8]  true;
+
+#nop 连接断开时,间隔多少秒自动重连;
+#var session[reconnect]         12;     #nop 正常间隔;
+#var session[reconnect_slow]    600;    #nop 维护模式下的间隔;
+#var session[remote_maint]      false;  #nop 维护模式开关;
+
+#nop 可以通过触发来开启维护模式;
+#action {^%*(%*)告诉你:开启远程维护$} {#var session[remote_maint] {true}};
+
+#nop 你的账号英文 ID,请在单独的文件里设置,这里只是占个位置;
+#var user[id]               bot;
+
+#nop 你的账号密码,请在单独的文件里设置,这里只是占个位置;
+#var user[passwd]           123456;
+
+#nop 默认的食物和饮料;
+#var char[favorite][food]   {gan liang};
+#var char[favorite][water]  {niurou tang};
+
+#nop 用户上线之后想要自动执行的代码写这里;
+#alias {user-online} {
+    back;
+};

+ 33 - 0
ids/EXAMPLE

@@ -0,0 +1,33 @@
+#!/usr/bin/env tt++
+#nop vim: filetype=tt
+
+#nop 模块名称:用户配置文件模版;
+#nop 模块说明:本文件是《担子炮 TinTin 套装》的命令行入口程序;
+#nop 版权声明:本文件属于《担子炮 TinTin 套装》的一部分;
+#nop ===========;
+#nop 《担子炮 TinTin 套装》的所有版权均由 © 2020 担子炮(dzp <danzipao@gmail.com>) 享有并保留一切法律权利;
+#nop 你可以在遵照 GPLv3 协议的基础之上使用、修改及重新分发本程序。;
+#nop ===========;
+
+#nop 载入担子炮的 TinTin 脚本框架;
+#read {framework/main.tin};
+
+#nop 你要连哪个接入;
+#var session[host]  mud.pkuxkx.net;
+#nop 你的账号英文 ID;
+#var user[id]       foo;
+#nop 你的大名;
+#var user[name]     小虾米;
+#nop 你的账号密码;
+#var user[passwd]   bar;
+
+#nop XXX: 所有 ids/DEFAULT 里的内容都可以在这里定制;
+
+#nop 用户上线之后想要自动执行的代码写这里;
+#alias {user-online} {
+    chat* back;
+};
+
+#nop 也可以在下面继续定义自己的别名和触发,均在 #gts 生效(会继承到所有 session);
+
+#alias {hello} {hi};

+ 70 - 0
plugins/basic/login.tin

@@ -0,0 +1,70 @@
+#class login kill;
+#class login open;
+
+#var login[session]     {};
+#var login[user]        {};
+#var login[autoexec]    {};
+
+#alias {login} {
+    #var    login[session]  {%1};
+    #var    login[user]     {%2};
+    #var    login[autoexec] {%3};
+    #session {$session[name]} {$session[host]} {$session[port]};
+};
+
+#event {SESSION CONNECTED} {
+    #if { "%0" == "$login[session][name]" } {
+        #class auto-login kill;
+        #class auto-login open;
+
+        #if { "$session[UTF8]" == "true" } {
+            #config {charset} {UTF-8};
+        };
+        #else {
+            #config {charset} {GBK1TOUTF8};
+        };
+
+        #line oneshot #action {^Input 1 for GBK, 2 for UTF8, 3 for BIG5$} {
+            #nop;
+        };
+
+        #line oneshot #action {^您的英文名字(要注册新人物请输入new。):$} {
+            #if { "$session[UTF8]" == "false" } {
+                #delay 0 #send {2};
+            };
+
+            #delay 0 {
+                #echo {\n};
+                #config {charset} {UTF-8};
+                #send $login[user][id];
+            };
+        };
+
+        #line oneshot #action {^您的英文名字:$} {
+            #delay 0 {#echo {\n}};
+        };
+
+        #line oneshot #action {^此ID档案已存在,请输入密码:$} {
+            #delay 0 {#echo {\n}; #send $login[user][passwd]};
+            #delay 1 {look};
+        };
+
+        #line oneshot #action {^您要将另一个连线中的相同人物赶出去,取而代之吗?(y/n)$} {
+            #delay 0 {#echo {\n}; #send y};
+        };
+
+        #line oneshot #action {^%s{欢迎来到北大侠客行!|重新连线完毕。}%s$} {
+            #delay 0 {
+                #showme 登录成功。;
+                #local handler $login[autoexec];
+                #class auto-login kill;
+                #class login kill;
+                $handler;
+            };
+        };
+
+        #class auto-login close;
+    };
+};
+
+#class login close;

+ 178 - 0
plugins/lib/event.tin

@@ -0,0 +1,178 @@
+#function {__event_init__} {
+    #var gEventHandlers {};
+    #var gValidEvent    {};
+};
+
+#function {__xtt_event_name_is_valid__} {
+    #local name {%1};
+    #if { "$name" == "{[_a-zA-Z]([./_a-zA-Z0-9-]*[./_a-zA-Z0-9])?}" } {
+        #return {true};
+    };
+    
+    #return {false};
+};
+
+#alias {event.define} {
+    #local name     {%1};
+    #local type     {%2};
+    #local module   {%3};
+    #local desc     {%4};
+
+    #if { "@__xtt_event_name_is_valid__{{$name}}" != "true" } {
+        errLog 事件名称不是合法的标识符名称。;
+        #return;
+    };
+
+    #if { "$type" == "" } {
+        #local type {无参};
+    };
+
+    #if { "$type" != "{有参|无参}" } {
+        errLog 事件类型只能是「有参」和「无参」两者之一,省略表示「无参」。;
+        #return;
+    };
+
+    #var {gValidEvent[$name]} {
+        {type}{$type}
+        {module}{$module}
+        {desc}{$desc}
+    };
+};
+
+#alias {event.list} {
+    #local event {};
+
+    #if { &gValidEvent[] <= 0 } {
+        infoLog 尚未定义任何事件。;
+        #return;
+    };
+
+    #echo {%h} { 已经定义的事件列表 };
+
+    #echo {%-20s %-5s %-20s %s} {事件/已注册的钩子} {类型} {模块} {说明/代码};
+    #echo {%-20s %-5s %-20s %s} {--------------------} {----} {----------} {------------};
+
+    #foreach {*gValidEvent[]} {event} {
+        #local type {有参};
+        #if { "$gValidEvent[$event][type]" == "{无参|}" } {
+            #local type {无参};
+        };
+
+        #echo {%-20s %-5s %-20s %s} {$event} {$type} {$gValidEvent[$event][module]} {$gValidEvent[$event][desc]};
+        #local hook {};
+        #local count {0};
+        #foreach {*gEventHandlers[$event][]} {hook} {
+            #local len {1};
+            #format len {%L} {$hook};
+            #math len {16 - $len};
+            #math count {$count + 1};
+            #local lead {├};
+            #if { $count == &gEventHandlers[$event][] } {
+                #Local lead {╰};
+            };
+            #echo { $lead@repeat{$len;─} %s        %-20s %s}{$hook}
+                            {$gEventHandlers[$event][$hook][module]}
+                            {$gEventHandlers[$event][$hook][code]};
+        };
+    };
+
+    #echo {%h};
+};
+
+#alias {event.emit} {
+    #local name  {%1};
+    #local pHook {%2};
+    #local args  {%3};
+
+    #if { "@__xtt_event_name_is_valid__{{$name}}" != "true" } {
+        errLog 事件名称不是合法的标识符名称。;
+        #return;
+    };
+
+    #if { "$gValidEvent[$name]" == "" } {
+        errLog 未定义的事件名称: $name;
+        #return;
+    };
+
+    #if { &gEventHandlers[$name][] <= 0 } {
+        #return;
+    };
+
+    #local hook {};
+    #foreach {*gEventHandlers[$name][]} {hook} {
+        #local options  {$gEventHandlers[$name][$hook][options]};
+        #local code     {$gEventHandlers[$name][$hook][code]};
+        #nop 如果发射事件时指定了 pHook,则只唤醒指定的 hook,注意这里的 pHook 支持通配符;
+        #if { "$pHook" != "" && "$hook" != "$pHook" } {
+            #return;
+        };
+        #if { "$options[justOnce]" == "true" } {
+            #unvar {gEventHandlers[$name][$hook]};
+        };
+        #if { "$args" == "" || "$gValidEvent[$name][type]" == "无参" } {
+            $code;
+        };
+        #else {
+            $code {$args};
+        };
+    };
+};
+
+#alias {event.handle} {
+    #local name     {%1};
+    #local hook     {%2};
+    #local module   {%3};
+    #local code     {%4};
+
+    #if { "$name" == "" || "$hook" == "" || "$module" == "" || "$code" == "" } {
+        #return;
+    };
+
+    #if { "@__xtt_event_name_is_valid__{{$name}}" != "true" } {
+        errLog 事件名称不是合法的标识符名称。;
+        #return;
+    };
+
+    #var {gEventHandlers[$name][$hook]} {
+        {module}{$module}
+        {code}{$code}
+    };
+};
+
+#alias {event.unhandle} {
+    #local name {%1};
+    #local hook {%2};
+
+    #if { "$name" == "" || "$hook" == "" } {
+        #return;
+    };
+
+    #if { "@__xtt_event_name_is_valid__{{$name}}" != "true" } {
+        errLog 事件名称不是合法的标识符名称。;
+        #return;
+    };
+
+    #unvar {gEventHandlers[$name][$hook]};
+};
+
+#alias {event.handleOnce} {
+    #local name     {%1};
+    #local hook     {%2};
+    #local module   {%3};
+    #local code     {%4};
+
+    #if { "$name" == "" || "$hook" == "" || "$module" == "" || "$code" == "" } {
+        #return;
+    };
+
+    #if { "@__xtt_event_name_is_valid__{{$name}}" != "true" } {
+        errLog 事件名称不是合法的标识符名称。;
+        #return;
+    };
+
+    #var {gEventHandlers[$name][$hook]} {
+        {options}{{justOnce}{true}}
+        {module}{$module}
+        {code}{$code}
+    };
+};

+ 145 - 0
plugins/lib/option.tin

@@ -0,0 +1,145 @@
+#var lib_option[META] {
+    {NAME}      {全局选项管理}
+    {DESC}      {为全局选项提供了一个储存位置,并提供一些实用的 API。}
+    {AUTHOR}    {担子炮}
+};
+
+#function {lib_option.Init} {
+    #var gOptions {};
+    #return {true};
+};
+
+#alias {option.Define} {
+    #local option   {%1};
+    #local type     {%2};
+    #local meaning  {%3};
+    #local default  {%4};
+
+    #if { "$gOptions[$option]" != "" } {
+        errLog 选项「$option」已存在,请检查代码。;
+        #return;
+    };
+
+    #local types {Bool};
+    #if { "$type" != "{$types}" } {
+        errLog 不能识别的选项类型: 「$type」,目前仅支持选项类型:{$types};
+        #return;
+    };
+
+    #if { "$type" == "Bool" && "$default" != "{true|false}" } {
+        errLog Bool 型的选项值只能是 true 或者 false。;
+        #return;
+    };
+
+    #var gOptions[$option] {
+        {name}      {$option}
+        {type}      {$type}
+        {meaning}   {$meaning}
+        {value}     {$default}
+    };
+};
+
+#alias {option.List} {
+    #echo {};
+	#echo {<128>%+20s %+10s %+30s %+20s} {选项名称} {选项类型} {选项含义} {选项当前值};
+	#draw Yellow scroll line 1 1 1 83;
+
+    #local option {};
+	#foreach {*gOptions[]} {option} {
+		#echo {%+20s %+10s %+30s %+20s}
+            {$gOptions[$option][name]}
+            {$gOptions[$option][type]}
+            {$gOptions[$option][meaning]}
+            {$gOptions[$option][value]};
+	};
+    #echo {};
+};
+
+#function {option.Get} {
+    #local {option} {%1};
+
+    #if { "$gOptions[$option]" == "" } {
+        errLog 不存在选项「$option」,请先定义后再使用。;
+        #return {};
+    };
+
+    #return {$gOptions[$option][value]};
+};
+
+#function {option.Set} {
+    #local {option} {%1};
+    #local {value}  {%2};
+
+    #local old {$xttOptions[$option]};
+    #if { "$old" == "" } {
+        errLog 不存在选项「$option」,请先定义后再使用。;
+        #return {};
+    };
+
+    #var {gOptions[$option][value]} {$value};
+
+    #return {$old};
+};
+
+#function {option.IsEnable} {
+    #local option {%1};
+    #local value {@option.Get{$option}};
+    #if { "$value" == "true" } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+#function {option.IsDisable} {
+    #local option {%1};
+    #local value {@option.Get{$option}};
+    #if { "$value" == "false" } {
+        #return 1;
+    };
+    #else {
+        #return 0;
+    };
+};
+
+#alias {option.Set} {
+    #local {option} {%1};
+    #local {value}  {%2};
+
+    #local _ {@option.Set{$option;{$value}}};
+};
+
+#function {option.Toggle} {
+    #local {option} {%1};
+
+    #local value {@option.Get{$option}};
+    #if { "$value" == "true" } {
+        option.Disable $option;
+        infoLog <160>选项「<130>$option<160>」 <110>已禁用。<070>;
+    };
+    #elseif { "$value" == "false" } {
+        option.Enable $option;
+        infoLog <160>选项「<130>$option<160>」 <120>已启用。<070>;
+    };
+
+    #return {$value};
+};
+
+#alias {option.Toggle} {
+    #local {option} {%1};
+
+    #local _ {@option.Toggle{$option}};
+};
+
+#alias {option.Enable} {
+    #local {option} {%1};
+
+    option.Set $option {true};
+};
+
+#alias {option.Disable} {
+    #local {option} {%1};
+
+    option.Set $option {false};
+};

+ 109 - 0
plugins/lib/ui/beautify.tin

@@ -0,0 +1,109 @@
+#var lib_ui_beautify[META] {
+    {NAME}      {美化插件}
+    {DESC}      {美化你的 TinTin++,对齐你的表格。}
+    {AUTHOR}    {担子炮}
+};
+
+#function {lib_ui_beautify.Init} {
+    #var beautify-switch {};
+    beautify.On;
+    #return true;
+};
+
+#var beautify-table {
+    {┌|┎|╭|╓|└|┖|╰|╙}                                   {─}
+    {─|┬|┭|┰|┱|╥}                                         {─}
+    {├|┞|┟|┠|╟}                                            {─}
+    {┼|╁|╀|╂|┽|╃|╅|╉|╫}                                {─}
+    {┴|┵|┸|┹|╨}                                            {─}
+    {┏|┍|┗|┕}                                               {━}
+    {━|┳|┲|┯|┮}                                            {━}
+    {┣|┢|┡|┝}                                               {━}
+    {╋|╇|╈|┿|╊|╆|╄|┾}                                   {━}
+    {┻|┺|┷|┶}                                               {━}
+    {╔|╦|╠|╬|╚|╩|═|╒|╤|╞|╪|╘|╧}                    {═}
+
+    {│|┃|║|┆|┇|┊|┋|┤|┨|┫|╣|╢|╡}                    {left-align}
+    {┐|┓|┒|╮|╗|┘|┛|┚|╯|╝|╲|╱}                       {left-align}
+
+    {■|‖|▉|▊|▋|▌|▍|▎|▏|◢|◣|▓}                       {left-align}
+    {▁|▂|▃|▄|▅|▆|▇|█|▀|▔|┄|┅|┈|┉|—}              {double}
+
+    {▼|△|□|◇|◆|☆|★|◎|⊙|○|●|Θ|⊕|×|·|∶|∴|∷}     {left-align}
+    {≤|≧|≥|≮|≯|↙|↘|↗|↖|←|↑|→|↓|∨|∧|≌|╳}        {left-align}
+    {※|Ψ|Ж|ξ|ф|∩|⊥|♀|∞|≈|√|⌒|Ω|¤|ō||ψ|ζ}     {left-align}
+    {①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩}                             {left-align}
+
+    {“|‘}                                                     {right-align}
+    {”|’|…}                                                  {left-align}
+
+    {⭕️|🐎|🌊|🔥|🎭|🐀|🔅}                                      {left-align}
+};
+
+#alias {beautify.On} {
+    #class beautify-sub kill;
+    #class beautify-sub open;
+
+    #local charset {};
+    #foreach {*{beautify-table[]}} {charset} {
+        #local type {${beautify-table[$charset]}};
+        #switch {"$type"} {
+            #case {"double"}        {#substitute {{$charset}} {%%1%%1}};
+            #case {"left-align"}    {#substitute {{$charset}} {%%1 }};
+            #case {"right-align"}   {#substitute {{$charset}} { %%1}};
+            #default                {#line sub var #substitute {{$charset}} {%%1$type}};
+        };
+    };
+
+    #substitute {%S%!s{https?://[[:graph:]]+}\s*{\S|$}} {%%1 %%2 %%3};
+
+    #class beautify-sub close;
+
+    #var beautify-switch {ON};
+    okLog 宽字符美化已启用。;
+    warnLog 出于美化需要,接下来你在屏幕上看到的内容可能和服务器实际传送的内容不一致。;
+    warnLog 这可能会给编写触发带来困扰,此时你可以通过快捷键或者 beautify.Off 暂时禁用美化。;
+
+    prompt.Set {{beautify}{<120>已启用}};
+    prompt.refresh;
+};
+
+#alias {beautify.Off} {
+    #class beautify-sub kill;
+
+    #var beautify-switch {OFF};
+    warnLog 宽字符美化已禁用。你可以通过快捷键或者 beautify.On 重新启用。;
+
+    prompt.Set {{beautify}{<110>已禁用}};
+    prompt.refresh;
+};
+
+#function {Beautify} {
+    #local text {%1};
+
+    #if { "${beautify-switch}" != "ON" } {
+        #return {$text};
+    };
+
+    #local charset {};
+    #foreach {*{beautify-table[]}} {charset} {
+        #local type {${beautify-table[$charset]}};
+        #switch {"$type"} {
+            #case {"double"}        {#replace {text} {{$charset}} {&1&1}};
+            #case {"left-align"}    {#replace {text} {{$charset}} {&1 }};
+            #case {"right-align"}   {#replace {text} {{$charset}} { &1}};
+            #default                {#line sub var #replace {text} {{$charset}} {&1$type}};
+        };
+    };
+
+    #return {$text};
+};
+
+#alias {beautify.ToggleSwitch} {
+    #if { "${beautify-switch}" == "ON" } {
+        beautify.Off;
+    };
+    #else {
+        beautify.On;
+    };
+};

+ 651 - 0
plugins/lib/ui/prompt.tin

@@ -0,0 +1,651 @@
+#var lib_ui_prompt[META] {
+    {NAME}      {提示栏插件}
+    {DESC}      {支持丰富的自定义选项,用户可用此模块定制自己的 UI。}
+    {AUTHOR}    {担子炮}
+};
+
+/*
+prompt 插件把整个屏幕划分为七个区域,从上到下依次为:
+
+Top line1
+Top line2
+...
+Top lineN
+----------- TopSepBar --------------
+(游戏区)
+----------- MidSepBar --------------
+Bot line1
+Bot line2
+...
+Bot lineN
+----------- BotSepBar --------------
+<提示符>:(输入区)
+
+输入区前面的提示符部分可以用 API prompt.Change 来修改。
+
+除输入区和游戏区之外,剩下五个区域都可以定制,用来显示信息内容(以下称为字段)。
+每一行都可以有多个字段。字段在视觉上由两部分组成,字段标签和字段内容。
+其显示样式分别可以控制,并受到几个 prompt 特性的影响,下面会分别予以介绍。
+
+字段的编排是高度可定制的,每个字段的标签、内容、颜色、显示风格、行为属性,
+都可以通过自己修改 #list prompt-fields 来修改。下面是对 #list prompt-fields
+的格式说明,但不建议直接在此处修改变量,请通过 etc/ui-settings.tin 来修改。
+prompt 模块在启动时会自动加载该文件。
+
+#list prompt-fields 是一个列表,其中中每个元素是一个 tt++ table,代表一个字段。
+tt++ table 由一组选项和与之对应的选项值来组成,用来说明想要定制的字段的选项。
+
+下面是所有可供设置的字段选项:
+
+1. place 枚举值 可选项: {Top|TopSepBar|MidSepBar|Bot|BotSepBar}
+  希望将字段显示在屏幕上的什么位置。
+  其中 {Top} 和 {Bot} 支持多行,其它三个只有一行。
+  默认为 BotSepBar。
+
+2. line 自然数
+   只有 place={Top|Bot} 才支持 line,代表第几行,最上面是第一行。
+   默认为 1。
+
+3. order 自然数
+   在同一行中的次序,默认按照在 list 中出现的顺序来排序。
+
+4. label 中英文字符串
+   字段标签,每个字段都可以有一个标签,显示在内容前面。
+   默认为空白。
+
+5. color tintin 格式的颜色代码,例如 <ddd>
+   字段值的颜色,通过该选项可以为字段值指定一个与配色主题不一样的颜色。
+   一般来说只有极个别选项才需要特别的颜色,没必要为每个选项都指定颜色,
+   因为那样的话你还不如去设置配色主题,参见下面配色主题的设置。
+   默认为空白,代表采用配色主题。
+
+6. name 英文字符串 不能为空
+   字段名称,必须唯一,不能重复。
+
+7. width 整数
+   字段内容的宽度,如果字段内容长度变化较大,可以设置一个固定宽度,以免晃动。
+   默认为 0,表示按照内容长度自动适配。
+
+8. visibility 枚举值 {HideEmpty|HideCool|HideZero|HideLabel|Always}
+   含义如下:
+    * HideEmpty 如果字段内容为空则不予显示。
+    * HideCool  如果内容已陈旧则不予显示。请参考 cooldown 选项。
+    * HideZero  如果倒计时归零则不予显示。请参考 countdown 选项。
+    * HideLabel 仅显示字段内容,不显示字段标签。
+    * Always    总是显示该字段的标签和内容。
+   所有的 Hide* 选项自动拥有 HideEmpty 语义。
+   默认为 HideEmpty。
+
+9. cooldown 非负整数
+   该字段的有效时间。超过有效时间没有更新的字段将在视觉上予以弱化显示,以提醒
+   用户。也就是说,如果 cooldown 不为 0,则该字段被更新时其标签将会以颜色主题
+   中 HotLabel 所指定的风格显示(意指新鲜的内容),之后持续若干秒后,转为由
+   CoolLabel 所指定的风格显示(意指陈旧的内容)。
+   持续时间由 cooldown 字段的值来决定,单位为秒。
+   陈旧的内容遇到 visibility=HideCool 时则不予显示。
+   如果 cooldown 为 0,则不会以 CoolLabel 风格显示。
+   默认值为 0。
+
+10. countdown 枚举值 {Auto|Clock|Seconds}
+   倒计时类型的字段。其内容为一个非负整数,代表需要倒计时的秒数,或者也可以夹杂
+   于文字内容当中,此时文字内容中所有形如 (%d) 的内容将会被替换成倒计时显示。
+   本插件会自动为该字段更新其内容,使得用户能够看到倒计时的效果。
+   倒计时有两种显示样式,本选项设置为 Clock 时,显示为时钟样式(N天HH:MM:SS),
+   设置为 Seconds 时,显示为读秒样式(N秒)。设置为 Auto 时则根据剩余时间长短,
+   自动切换两种显示效果。
+   倒计时类型的字段,如果同时设置了 visibility=HideZero,那么倒计时归零后会自动
+   隐藏该字段。
+   在倒计时存续期间,该字段的 cooldown 属性将会被抑制,直到倒计时归零后才起作用。
+   这条规则确保倒计时字段即使设置了 visibility=HideCool,也至少会完成倒计时,而
+   倒计时结束后,如果 cooldown 时间比倒计时时间还要长,则还会继续显示一段时间之
+   后才会被隐藏。
+*/
+#list prompt-fields create {};
+
+#nop 配色主题,注意这里不要直接嵌入 SGR(ansi codes),否则计算宽度时会有问题。;
+#var prompt-theme {
+    {Disable}{<bbc>}
+    {BusyColor}{<030>}
+    {BattleColor}{<110>}
+    {BattleBusyColor}{<500><110>}
+    {TopSepBar}{<020>}
+    {MidSepBar}{<020>}
+    {BotSepBar}{<020>}
+    {HotLabel}{<100><F399>}
+    {CoolLabel}{<100>}
+    {Value}{<070>}
+};
+
+#nop 自定义图标;
+#var prompt-icon {
+    {DisableRefresh}{🚫}
+};
+
+#nop 热键绑定;
+#list global-key-bindings create {
+    {{key}{\cs}    {action}{prompt.ToggleSwitch}}
+};
+
+#var prompt-top-max-line {0};
+#var prompt-bot-max-line {0};
+
+#nop 所有的字段值的字典;
+#var prompt-dict {};
+
+#nop 上次实际绘制屏幕的时间;
+#var prompt-refresh {
+    {global}{false}
+    {lines} {false}
+    {prompt}{false}
+};
+
+#nop 提示符;
+#var prompt-prompt {Input};
+
+#function {lib_ui_prompt.Init} {
+    load-file {etc/ui-settings.tin};
+    prompt.Set {};
+    prompt.bindKey;
+    prompt.Enable;
+
+    #return true;
+};
+
+#nop 设置字段值,被设置的字段值将立即显示在屏幕上;
+#alias {prompt.Set} {
+    #local fields   {%1};
+
+    #local now {};
+    #format now {%T};
+
+    #local field    {};
+    #foreach {*fields[]} {field} {
+        #var prompt-dict[$field] {
+            {updateTime}{$now}
+            {showTime}{$now}
+            {value}{$fields[$field]}
+        };
+    };
+};
+
+#nop 设置提示符,提示符位于屏幕最下方输入区的首部,用作指示整个游戏所处的状态;
+#alias {prompt.Change} {
+    #local text {%1};
+
+    #var prompt-prompt {$text};
+
+    prompt.refresh;
+};
+
+#nop 设置字段值,被设置的字段值将立即显示在屏幕上,但会在一段时间后消失;
+#alias {prompt.SetAwhile} {
+    #local id    {%1};
+    #local value {%2};
+    #local secs  {%3};
+
+    #if { "$secs" == "" } {
+        #local secs {5};
+    };
+
+    prompt.Set {{$id}{$value}};
+
+    #line sub {VARIABLES} {
+        #delay prompt-set-awhile-$id {
+            prompt.Set {{$id}{}};
+        } {$secs};
+    };
+};
+
+#nop 显示字段值;
+#alias {prompt.refresh} {
+    #local topMaxLine   {0};
+    #local botMaxLine   {0};
+
+    #local topLines     {};
+    #local botLines     {};
+    #local topSepBar    {};
+    #local midSepBar    {};
+    #local botSepBar    {};
+
+    #foreach {*{prompt-fields[]}} {idx} {
+        #local field {${prompt-fields[$idx]}};
+        #if { "$field[visibility]" == "" } {
+            #local field[visibility] {HideEmpty};
+        };
+        #switch {"$field[place]"} {
+            #case {"Top"} {
+                #while {1} {
+                    #if { $field[line] <= $topMaxLine } {
+                        #break;
+                    };
+                    #math topMaxLine {$topMaxLine + 1};
+                    #local topLines[$topMaxLine] {};
+                };
+                #if { "$field[order]" == "" } {
+                    #local field[order] {@eval{ @max{0;*topLines[$field[line]][]} + 1 }};
+                };
+                #local topLines[$field[line]][$field[order]] {$field};
+            };
+            #case {"Bot"} {
+                #while {1} {
+                    #if { $field[line] <= $botMaxLine } {
+                        #break;
+                    };
+                    #math botMaxLine {$botMaxLine + 1};
+                    #local botLines[$botMaxLine] {};
+                };
+                #if { "$field[order]" == "" } {
+                    #local field[order] {@eval{ @max{0;*botLines[$field[line]][]} + 1 }};
+                };
+                #local botLines[$field[line]][$field[order]] {$field};
+            };
+            #case {"TopSepBar"} {
+                #if { "$field[order]" == "" } {
+                    #local field[order] {@eval{ @max{0;*topSepBar[]} + 1 }};
+                };
+                #local topSepBar[$field[order]] {$field};
+            };
+            #case {"MidSepBar"} {
+                #if { "$field[order]" == "" } {
+                    #local field[order] {@eval{ @max{0;*midSepBar[]} + 1 }};
+                };
+                #local midSepBar[$field[order]] {$field};
+            };
+            #case {"BotSepBar"} {
+                #if { "$field[order]" == "" } {
+                    #local field[order] {@eval{ @max{0;*botSepBar[]} + 1 }};
+                };
+                #local botSepBar[$field[order]] {$field};
+            };
+            #default {
+                #echo {配置有误,请检查。place=[%s]} {$field[place]};
+            };
+        };
+    };
+
+    #local allBarColor {};
+    #if { "${prompt-dict[busy][value]}" == "true" } {
+        #local allBarColor {${prompt-theme[BusyColor]}};
+    };
+    #if { "${prompt-dict[battle][value]}" == "true" } {
+        #local allBarColor {${prompt-theme[BattleColor]}};
+    };
+    #if { "${prompt-dict[battle][value]}" == "true" && "${prompt-dict[busy][value]}" == "true" } {
+        #local allBarColor {${prompt-theme[BattleBusyColor]}};
+    };
+    #if { "${prompt-dict[disable][value]}" != "" } {
+        #local allBarColor {${prompt-theme[Disable]}};
+    };
+
+    #if { "$topSepBar" != "" } {
+        #math topMaxLine {$topMaxLine + 1};
+    };
+
+    #if { $botMaxLine == 0 && ( "$midSepBar" == "" || "$botSepBar" == "" ) } {
+        #local botMaxLine {1};
+    };
+    #else {
+        #math botMaxLine {$botMaxLine + 2};
+    };
+
+    #local content {};
+    #list content create {};
+
+    #local line {};
+    #local delta {0};
+    #foreach {*topLines[]} {line} {
+        #local fields {$topLines[$line]};
+        #local text {@__prompt_build_line__{{$fields}}};
+        #if { $text[width] > 0 } {
+            #local realLine {};
+            #math realLine {$line - $delta};
+            #list content {add} {{{line}{$realLine}{text}{$text[text]}}};
+        };
+        #else {
+            #math delta {$delta + 1};
+            #math topMaxLine {$topMaxLine - 1};
+        };
+    };
+
+    #if { "$topSepBar" != "" } {
+        #local text {@__prompt_build_line__{{$topSepBar}}};
+        #if { $text[width] == 0 && $topMaxLine == 1 } {
+            #math topMaxLine {$topMaxLine - 1};
+        };
+        #else {
+            #local barColor {${prompt-theme[TopSepBar]}};
+            #if { "$allBarColor" != "" } {#local barColor {$allBarColor}};
+            #local text {@__prompt_fill_line__{{$text[text]};{$text[width]};$barColor}};
+            #list content {add} {{{line}{$topMaxLine}{text}{$text}}};
+        };
+    };
+
+    #if { "$botSepBar" != "" || $botMaxLine > 0 } {
+        #local text {@__prompt_build_line__{{$botSepBar}}};
+        #local barColor {${prompt-theme[BotSepBar]}};
+        #if { "$allBarColor" != "" } {#local barColor {$allBarColor}};
+        #local text {@__prompt_fill_line__{{$text[text]};{$text[width]};$barColor}};
+        #list content {add} {{{line}{-2}{text}{$text}}};
+    };
+
+    #local delta {0};
+    #local line {0};
+    #if { &botLines[] > 0 } {
+        #loop {&botLines[]} {1} {line} {
+            #local fields {$botLines[$line]};
+            #local text {@__prompt_build_line__{{$fields}}};
+            #if { $text[width] > 0 } {
+                #math realLine {$line - $botMaxLine - 1};
+                #list content {add} {{{line}{$realLine}{text}{$text[text]}}};
+            };
+            #else {
+                #math botMaxLine {$botMaxLine - 1};
+            };
+        };
+    };
+
+    #if { "$midSepBar" != "" || $botMaxLine > 0 } {
+        #local text {@__prompt_build_line__{{$midSepBar}}};
+        #local barColor {${prompt-theme[MidSepBar]}};
+        #if { "$allBarColor" != "" } {#local barColor {$allBarColor}};
+        #local text {@__prompt_fill_line__{{$text[text]};{$text[width]};$barColor}};
+        #math line {$botMaxLine + 1};
+        #list content {add} {{{line}{-$line}{text}{$text}}};
+    };
+
+    #if { 1 && ( "${prompt-top-max-line}" != "$topMaxLine" || "${prompt-bot-max-line}" != "$botMaxLine" ) } {
+        #local lineWidth {};
+        #screen get COLS lineWidth;
+        #format spaceLine {%-${lineWidth}s} {};
+
+        #local newMax {$topMaxLine};
+        #while { $newMax < ${prompt-top-max-line} } {
+            #math newMax {$newMax + 1};
+            #echo {{$spaceLine}{$newMax}};
+        };
+        #local newMax {$botMaxLine + 1};
+        #while { $newMax < ${prompt-bot-max-line} } {
+            #math newMax {$newMax + 1};
+            #echo {{$spaceLine}{-$newMax}};
+        };
+
+        #var prompt-top-max-line {$topMaxLine};
+        #var prompt-bot-max-line {$botMaxLine};
+        #split {$topMaxLine} {$botMaxLine};
+    };
+
+    #local idx {};
+    #foreach {*content[]} {idx} {
+        #local line {$content[$idx]};
+        #echo {{%s}{$line[line]}} {$line[text]};
+    };
+
+    #local prompt {${prompt-prompt}};
+    #if { "$prompt" != "" } {
+        #local prompt {$prompt: };
+        #echo {{$prompt}{-1}};
+    };
+};
+
+#function {__prompt_build_line__} {
+    #local fields       {%1};
+    #local text         {};
+    #local order        {};
+    #local lineWidth    {0};
+    #foreach {*fields[]} {order} {
+        #local field {$fields[$order]};
+        #local name  {$field[name]};
+        #local label {$field[label]};
+        #local color {$field[color]};
+        #local width {$field[width]};
+        #local value {${prompt-dict[$name]}};
+        #local now {};
+        #format now {%T};
+
+        #nop 所有的 Hide* 选项自动拥有 HideEmpty 语义。;
+        #if { "$field[visibility]" == "Hide%*" && "$value[value]" == "" } {
+            #continue;
+        };
+
+        #nop 检查是否是倒计时,以及倒计时是否已经归零;
+        #local zero {undef};
+        #if { "$field[countdown]" != "" } {
+            #local seconds {};
+            #math seconds {$now - ${prompt-dict[$name][showTime]}};
+            #replace value[value] {^%+1..d$} {@__prompt_countdown__{&1;$seconds}};
+            #replace value[value] {(%+1..d)} {(@__prompt_countdown__{&1;$seconds})};
+            #if { "$value[value]" != "{[1-9][0-9]*|.*\([1-9][0-9]*\).*}" } {
+                #local zero {true};
+            };
+            #else {
+                #local zero {false};
+            };
+            #var {prompt-dict[$name][showTime]} {$now};
+            #var {prompt-dict[$name][value]} {$value[value]};
+            #replace value[value] {^%+1..d$} {@__prompt_show_countdown__{$field[countdown];&1}};
+            #replace value[value] {(%+1..d)} {(@__prompt_show_countdown__{$field[countdown];&1})};
+        };
+
+        #nop 如果设置了 HideZero,那么倒计时归零时,不予显示该字段。;
+        #if { "$field[visibility]" == "HideZero" && "$zero" == "true" } {
+            #continue;
+        };
+
+        #nop 检查是否已经是冷却了的字段,倒计时字段当倒计时持续时不会变为冷却;
+        #local cool {false};
+        #if { $field[cooldown] > 0 && $now - $value[updateTime] > $field[cooldown] && "$zero" != "false" } {
+            #local cool {true};
+        };
+
+        #nop 如果设置了 HideCool,那么内容冷却之后则不予显示;
+        #if { "$field[visibility]" == "HideCool" && "$cool" == "true" } {
+            #continue;
+        };
+
+        #nop 如果设置了 HideLabel,那么不显示标签;
+        #if { "$field[visibility]" == "HideLabel" } {
+            #local label {};
+        };
+
+        #if { "$label" != "" } {
+            #nop 如果全局开关已经禁用,则忽略所有配色,全部显示为禁用色;
+            #if { "${prompt-dict[disable][value]}" != "" } {
+                #replace label {^<{[a-zA-Z0-9]+}>} {};
+                #local label {${prompt-theme[Disable]}$label};
+            };
+            #else {
+                #nop 否则根据内容的新鲜程度自动改变颜色;
+                #if { "$cool" == "true" } {
+                    #replace label {^<{[a-zA-Z0-9]+}>} {};
+                    #local label {${prompt-theme[CoolLabel]}$label};
+                };
+                #else {
+                    #local label {${prompt-theme[HotLabel]}$label};
+                };
+            };
+
+            #local label {$label<070> };
+        };
+
+        #if { "$field[visibility]" == "HideLabel" } {
+            #local label {};
+        };
+
+        #if { "$color" == "" } {
+            #local color {${prompt-theme[Value]}};
+        };
+
+        #format value {$color%-${width}s<070>} {$value[value]};
+        #math lineWidth {$lineWidth + @strWidth{$label} + @strWidth{$value}};
+        #if { "$text" == "" } {
+            #local text {$label$value};
+        };
+        #elseif { "$label$value" != "" } {
+            #local text {$text $label$value};
+            #math lineWidth {$lineWidth + 1};
+        };
+    };
+
+    #return {{width}{$lineWidth}{text}{$text}};
+};
+
+#function {__prompt_countdown__} {
+    #local value    {%1};
+    #local count    {%2};
+
+    #math value {$value - $count};
+    #if { $value < 0 } {
+        #local value {0};
+    };
+
+    #return {$value};
+};
+
+#function {__prompt_show_countdown__} {
+    #local type     {%1};
+    #local secs     {%2};
+    #local ret      {};
+
+    #if { "$type" == "Auto" } {
+        #if { $secs < 600 } {
+            #local type {Seconds};
+        };
+        #else {
+            #local type {Clock};
+        };
+    };
+
+    #if { "$type" == "Seconds" } {
+        #local ret {${secs}s};
+    };
+    #elseif { "$type" == "Clock" } {
+        #if { $secs > 86400 } {
+            #math ret {$secs / 86400};
+            #local ret {$ret天};
+            #math secs {$secs % 86400};
+        };
+
+        #local hour {};
+        #local mins {};
+        #math hour  {$secs / 3600};
+        #math secs  {$secs % 3600};
+        #math mins  {$secs / 60};
+        #math secs  {$secs % 60};
+        #format ret {%s%%02d:%%02d:%%02d} {$ret} {$hour} {$mins} {$secs};
+    };
+
+    #return {$ret};
+};
+
+#function {__prompt_fill_line__} {
+    #local text     {%1};
+    #local width    {%2};
+    #local color    {%3};
+
+    #local newText {@trim{$text}};
+    #math width {$width + @strWidth{$newText} - @strWidth{$text}};
+
+    #local screenWidth {0};
+    #screen get COLS screenWidth;
+
+    #local leftLen {0};
+    #local rightLen {0};
+    #math leftLen {($screenWidth - $width) / 2 - 1};
+    #math rightLen {$screenWidth - $leftLen - $width - 2};
+
+    #local left {};
+    #format {left} {%${leftLen}s} {};
+    #replace {left} { } {─};
+
+    #local right {};
+    #format {right} {%${rightLen}s} {};
+    #replace {right} { } {─};
+
+    #if { $leftLen < 0 } {
+        #format {newText} {%$screenWidth.${screenWidth}s} {$newText};
+        #return {$newText};
+    };
+    #elseif { "$newText" == "" } {
+        #return {$color$left──$right<070>};
+    };
+    #else {
+        #return {$color$left<070> $newText $color$right<070>};
+    };
+};
+
+#event {SCREEN RESIZE} {
+    #var prompt-top-max-line {0};
+    #var prompt-bot-max-line {0};
+    prompt.refresh;
+};
+
+#event {CATCH IAC SB NAWS} {#0};
+
+#alias {prompt.UpdateHP} {
+    prompt.Set {
+        {exp}{$char[HP][经验显示]}
+        {pot}{$char[HP][潜能显示]}
+        {battle}{$char[HP][战斗中]}
+        {busy}{$char[HP][忙]}
+    }
+};
+
+#alias {prompt.UpdateSM} {
+    #local effect {$char[STATUS][持续效果]};
+    #replace effect {秒)} {)};
+
+    prompt.Set {
+        {yunqi}{$char[STATUS][气血恢复]}
+        {status}{$char[STATUS][健康状态]}
+        {persist}{$effect}
+    }
+};
+
+#alias {prompt.Disable} {
+    prompt.Set {{disable}{${prompt-icon[DisableRefresh]}}};
+    prompt.refresh;
+    #untick prompt.refresh;
+};
+
+#alias {prompt.Enable} {
+    prompt.Set {{disable}{}};
+    Tick prompt.refresh {prompt.refresh} 1;
+};
+
+#alias {prompt.ToggleSwitch} {
+    #if { "${prompt-dict[disable][value]}" == "" } {
+        prompt.Disable;
+    };
+    #else {
+        prompt.Enable;
+    };
+};
+
+#alias {prompt.bindKey} {
+    #local idx {};
+    #foreach {*{global-key-bindings[]}} {idx} {
+        #local key  {${global-key-bindings[$idx][key]}};
+        #local code {${global-key-bindings[$idx][action]}};
+        #line sub var #macro {$key} {$code};
+    };
+};
+
+#alias {prompt.test} {
+    #local fullme {<010>很久没有进行机器人检查(fullme)了,任务奖励将受到影响。<070>};
+    #local fullme {http://pkuxkx.com/antirobot/robot.php?filename=1576762922984895};
+    prompt.Set { {URL}{$fullme} };
+    prompt.Set {
+        {pot}{357.75万}
+        {exp}{7510.56万}
+        {expSpd}{11.88万/小时}
+        {profit}{308金币/小时}
+    };
+    prompt.Set {
+        {job}{<171>乔峰}
+        {stage}{寻找NPC}
+        {area}{建康}
+        {room}{中城}
+        {npc}{庞九公}
+        {type}{杀死}
+    };
+};

+ 5 - 0
plugins/lib/xtintin/__init__.tin

@@ -0,0 +1,5 @@
+#nop xtintin: 为了方便 TinTin++ 使用而增加的一些语法扩展。;
+
+load-file plugins/lib/xtintin/funcs.tin;
+load-file plugins/lib/xtintin/cmds.tin;
+load-file plugins/lib/xtintin/debug.tin;

+ 24 - 0
plugins/lib/xtintin/cmds.tin

@@ -0,0 +1,24 @@
+#alias {Tick} {xtt.Tick};
+#alias {xtt.Tick} {
+    #local id       {%1};
+    #local code     {%2};
+    #local interval {%3};
+
+    #line sub var #untick {$id};
+    #line sub var #tick {$id} {$code} $interval;
+    $code;
+};
+
+#alias {xtt.Send} {
+    #if { "$xttOptions[DisableOutput]" == "true" } {
+        #echo {<160>命令已被抑制: <420>%p<070>} {%0};
+        #return;
+    };
+
+    #send %0
+};
+
+#alias {xtt.SendAtOnce} {
+    #local cmds {%1};
+    #send {#$cmds#};
+};

+ 26 - 0
plugins/lib/xtintin/debug.tin

@@ -0,0 +1,26 @@
+#nop ############################ 调试开关 #################################;
+
+#var XTinTin[debug] {false};
+
+#alias {xtt.ToggleDebug} {
+    #if { "$XTinTin[debug]" == "false" } {
+        beautify.Off;
+        prompt.Disable;
+        xtt.DebugOn;
+    };
+    #else {
+        xtt.DebugOff;
+        prompt.Enable;
+        beautify.On;
+    };
+};
+
+#alias {xtt.DebugOn} {
+    #var XTinTin[debug] {true};
+    #debug all on;
+};
+
+#alias {xtt.DebugOff} {
+    #var XTinTin[debug] {false};
+    #debug all off;
+};

+ 205 - 0
plugins/lib/xtintin/doc.tin

@@ -0,0 +1,205 @@
+#nop ############################ 系统工具-帮助文档和参数类型检查 #################################;
+
+#alias {xtt.PrintHelpDoc} {
+    #local aliasList    {%1};
+    #local funcList     {%2};
+    #local what         {%3};
+
+    #echo {%c%h} {light cyan};
+
+    #if { "$what" == "" } {
+        #echo {%c%s} {light yellow} {别名:};
+
+        #foreach {$aliasList} {alias} {
+            #if { "$alias" == "" } {
+                #continue;
+            };
+
+            #var __XTinTinAPIDoc__ {};
+            $alias 'XTinTinAPI;
+            #if { "$__XTinTinAPIDoc__" == "" } {
+                #echo {    %c%-20s --- %s} {light cyan} {$alias} {(没有文档)};
+            };
+            #else {
+                #echo {    %c%-20s --- %s} {light cyan} {$alias} {$__XTinTinAPIDoc__[desc]};
+            };
+        };
+
+        #echo {%c%s} {light yellow} {函数:};
+
+        #local func {};
+        #foreach {$funcList} {func} {
+            #if { "$func" == "" } {
+                #continue;
+            };
+
+            #var __XTinTinAPIDoc__ {};
+            #local {funcCall} {#local _ {@$func{'XTinTinAPI}}};
+            $funcCall;
+            #if { "$__XTinTinAPIDoc__" == "" } {
+                #echo {    %c%-20s --- %s} {light cyan} {$func} {(没有文档)};
+            };
+            #else {
+                #echo {    %c%-20s --- %s} {light cyan} {$func} {$__XTinTinAPIDoc__[desc]};
+            };
+        };
+    };
+    #elseif { @indexOfStrList{{$aliasList}; $what} > 0 } {
+        #var __XTinTinAPIDoc__ {};
+        $what 'XTinTinAPI;
+        #if { "$__XTinTinAPIDoc__" == "" } {
+            #echo {%c%%s} {light cyan} {没有关于 $alias 的文档。};
+        };
+        #else {
+            #local _ @xtt.GenHelpDoc{{$__XTinTinAPIDoc__}};
+        };
+    };
+    #elseif { @indexOfStrList{{$funcList}; $what} > 0 } {
+        #var __XTinTinAPIDoc__ {};
+        #local {funcCall} {#local _ {@$what{'XTinTinAPI}}};
+        $funcCall;
+        #if { "$__XTinTinAPIDoc__" == "" } {
+            #echo {    %c%-20s --- %s} {light cyan} {$func} {(没有文档)};
+        };
+        #else {
+            #local _ @xtt.GenHelpDoc{{$__XTinTinAPIDoc__}};
+        };
+    };
+
+    #echo {%c%h} {light cyan};
+};
+
+#function {xtt.GenHelpDoc} {
+    #local args {%1};
+
+    #if { "$args[check]" == "'XTinTinAPI" } {
+        #return {
+            {name}{$args[name]}
+            {type}{$args[type]}
+            {desc}{$args[desc]}
+            {args}{$args[args]}
+            {check}{}
+        };
+    };
+
+    #if { "$args[type]" == "alias" } {
+        #echo {%c别名用法: %c%s 参数1 参数2...} {light cyan} {light yellow} {$args[name]};
+    };
+    #elseif { "$args[type]" == "function" } {
+        #echo {%c函数用法: %c@%s{ {参数1}{值1} {参数2}{值2}... }} {light cyan} {light yellow} {$args[name]};
+    };
+    #else {
+        #echo {%c不明类型的标识符: %c%s} {light red} {light yellow} {$args[name]};
+        #echo {%c%h} {light cyan};
+        #return;
+    };
+    #echo {%c说明:%s} {light cyan} {$args[desc]};
+    #echo {%c参数列表(!必选,?可选):} {light cyan};
+    #foreach *args[args][] {idx} {
+        #local key  {};
+        #local desc {};
+
+        #nop 注意别名和函数收集参数名称的方法不同;
+        #if { "$args[type]" == "alias" } {
+            #list tmp create $args[args][$idx];
+            #format key  {%s} {$tmp[1]};
+            #format desc {%s} {$tmp[2]};
+        };
+        #else {
+            #format key  {%s} {$idx};
+            #format desc {%s} $args[args][$idx];
+        };
+
+        #echo {%c    %-16s -- %s} {light cyan} {$key} {$desc};
+    };
+};
+
+/*
+用法:将下面这段代码作为模版,插入到想要生成文档的别名或者函数开头,就可以了。
+
+    #local ok @xtt.HelpDoc{{
+        {type}{function}
+        {name}{helpDoc}
+        {desc}{为函数或别名提供代码注释,生成帮助文档,进行参数检查}
+        {check}{%0}
+        {args}{
+            {!name}{函数或别名的名称}
+            {!desc}{函数或别名的一句话说明}
+            {!check}{需要检查的变量,别名用 %0,函数建议用 %1}
+            {!args}{函数或别名的参数说明。}
+        }
+    }}
+
+    #if { "$ok" != "true" } {
+         #return "$ok"
+    }
+
+以上,截止到此处为止。
+
+你可能已经注意到了,上面的代码调用了 xtt.HelpDoc 函数来检查参数并生成文档。
+而 xtt.HelpDoc 本身就支持用以上语法来检查并生成文档。因此上面的代码本身也演示了如何调用函数。
+注意第二个 args 有两种写法,如果是函数就用键值对,如果是别名就用列表(前面加{1}{2}{3})。
+其中参数名用感叹号打头表示必选,问号打头表示可选,默认必选。
+*/
+#function {xtt.HelpDoc} {
+    #local args {%1};
+
+    #if { "$args[check]" == "'XTinTinAPI" } {
+        #return @XTinTinGenHelpDoc{{$args}};
+    };
+
+    #if { "$XTinTin[debug]" != "true" } {
+        #return {true};
+    };
+
+    #local ok {true};
+
+    #local realArgs {};
+    #list  realArgs create {$args[check]};
+    #foreach {*args[args][]} {idx} {
+        #nop 这个 key 必须要用 var,因为后面 regex 里面不支持 local;
+        #var key {};
+
+        #nop 注意别名和函数收集参数名称的方法不同;
+        #if { "$args[type]" == "alias" } {
+            #list tmp create $args[args][$idx];
+            #format key {%s} {$tmp[1]};
+        };
+        #else {
+            #format key {%s} {$idx};
+        };
+
+        #var optional {false};
+        #regex {$key} {{!|\?}%%2} {
+            #format key {%s} {&2};
+            #if { "&1" == "?" } {
+                #var optional {true};
+            };
+        }{};
+
+        #nop 注意别名和函数收集参数值的方法不同;
+        #local value {};
+        #if { "$args[type]" == "alias" } {
+            #format value {%s} {$realArgs[$idx]};
+        };
+        #else {
+            #format value {%s} {$check[$key]};
+        };
+
+        #if { "$optional" == "false" && "$value" == "" } {
+            #echo {%c函数或别名在调用时缺少必要的参数: %s} {light red} $key;
+            #format ok {false};
+        };
+    };
+
+    #unvar key;
+
+    #if { "$ok" != "true" } {
+        #echo {%c%h} {light cyan};
+        #var _ @XTinTinGenHelpDoc{{$args}};
+        #echo {%c-----\n%s} {light cyan} {实际传递的参数信息: \n$args[check]};
+        #echo {%c%h} {light cyan};
+    };
+
+    #return $ok;
+};

+ 216 - 0
plugins/lib/xtintin/funcs.tin

@@ -0,0 +1,216 @@
+#nop ############################ 小工具-方向处理 #################################;
+
+#function {reverseCmds} {
+    #local cmds  {%1};
+    #local newCmds {};
+    #foreach {$cmds} {item} {
+        #format {newCmds} {%s;%s} @reverseDir{$item} {$newCmds};
+    };
+
+    #return {$newCmds};
+};
+
+#function {dirName}     { #return @convertDir{name; %1; %2}; };
+#function {shortDir}    { #return @convertDir{short; %1; %2}; };
+#function {longDir}     { #return @convertDir{long; %1; %2}; };
+#function {reverseDir}  { #return @convertDir{reverse; %1; %2}; };
+
+#var xtt.dir.table {
+    {east}      {{name}{正东}   {short}{e}  {long}{east}        {reverse}{west}     }
+    {west}      {{name}{正西}   {short}{w}  {long}{west}        {reverse}{east}     }
+    {south}     {{name}{正南}   {short}{s}  {long}{south}       {reverse}{north}    }
+    {north}     {{name}{正北}   {short}{n}  {long}{north}       {reverse}{south}    }
+
+    {e}         {{name}{正东}   {short}{e}  {long}{east}        {reverse}{w}        }
+    {w}         {{name}{正西}   {short}{w}  {long}{west}        {reverse}{e}        }
+    {s}         {{name}{正南}   {short}{s}  {long}{south}       {reverse}{n}        }
+    {n}         {{name}{正北}   {short}{n}  {long}{north}       {reverse}{s}        }
+
+    {northeast} {{name}{东北}   {short}{ne} {long}{northeast}   {reverse}{southwest}}
+    {southeast} {{name}{东南}   {short}{se} {long}{southeast}   {reverse}{northwest}}
+    {northwest} {{name}{西北}   {short}{nw} {long}{northwest}   {reverse}{southeast}}
+    {southwest} {{name}{西南}   {short}{sw} {long}{southwest}   {reverse}{northeast}}
+
+    {ne}        {{name}{东北}   {short}{ne} {long}{northeast}   {reverse}{sw}       }
+    {se}        {{name}{东南}   {short}{se} {long}{southeast}   {reverse}{nw}       }
+    {nw}        {{name}{西北}   {short}{nw} {long}{northwest}   {reverse}{se}       }
+    {sw}        {{name}{西南}   {short}{sw} {long}{southwest}   {reverse}{ne}       }
+
+    {up}        {{name}{正上}   {short}{u}  {long}{up}          {reverse}{down}     }
+    {down}      {{name}{正下}   {short}{d}  {long}{down}        {reverse}{up}       }
+    {u}         {{name}{正上}   {short}{u}  {long}{up}          {reverse}{d}        }
+    {d}         {{name}{正下}   {short}{d}  {long}{down}        {reverse}{u}        }
+
+    {eastup}    {{name}{东上}   {short}{eu} {long}{eastup}      {reverse}{westdown} }
+    {westup}    {{name}{西上}   {short}{wu} {long}{westup}      {reverse}{eastdown} }
+    {southup}   {{name}{南上}   {short}{su} {long}{southup}     {reverse}{northdown}}
+    {northup}   {{name}{北上}   {short}{nu} {long}{northup}     {reverse}{southdown}}
+
+    {eu}        {{name}{东上}   {short}{eu} {long}{eastup}      {reverse}{wd}       }
+    {wu}        {{name}{西上}   {short}{wu} {long}{westup}      {reverse}{ed}       }
+    {su}        {{name}{南上}   {short}{su} {long}{southup}     {reverse}{nd}       }
+    {nu}        {{name}{北上}   {short}{nu} {long}{northup}     {reverse}{sd}       }
+
+    {eastdown}  {{name}{东下}   {short}{ed} {long}{eastdown}    {reverse}{westup}   }
+    {westdown}  {{name}{西下}   {short}{wd} {long}{westdown}    {reverse}{eastup}   }
+    {southdown} {{name}{南下}   {short}{sd} {long}{southdown}   {reverse}{northup}  }
+    {northdown} {{name}{北下}   {short}{nd} {long}{northdown}   {reverse}{southup}  }
+
+    {ed}        {{name}{东下}   {short}{ed} {long}{eastdown}    {reverse}{wu}       }
+    {wd}        {{name}{西下}   {short}{wd} {long}{westdown}    {reverse}{eu}       }
+    {sd}        {{name}{南下}   {short}{sd} {long}{southdown}   {reverse}{nu}       }
+    {nd}        {{name}{北下}   {short}{nd} {long}{northdown}   {reverse}{su}       }
+
+    {out}       {{name}{出去}   {short}{out}    {long}{out}     {reverse}{enter}    }
+    {enter}     {{name}{进去}   {short}{enter}  {long}{enter}   {reverse}{out}      }
+};
+
+#function {convertDir} {
+    #local field        {%1};
+    #local dir          {%2};
+    #local restricted   {%2};
+
+    #if { "$dir" == "" } {
+        #return "";
+    };
+
+    #local entry ${xtt.dir.table[$dir]};
+
+    #if { "$restricted" == "true" && "$entry" == "" } {
+        #return {};
+    };
+    #else {
+        #return {$entry[$field]};
+    };
+};
+
+#nop ############################ 小工具-语法增强 #################################;
+
+#function {boolAnd} {
+    #if { "%1" == "true" && "%2" == "true" } {
+        #return {true};
+    };
+
+    #return {false};
+};
+
+#function {reverseList} {
+    #var l {%1};
+
+    #list {l} size len;
+    #list {result} create {};
+
+    #loop $len 1 {idx} {
+        #list {result} add $l[$idx];
+    };
+};
+
+#function {eval}            {#math result {%1}};
+#function {space}           {#var result {@repeat{%1;{ }}}};
+#function {repeat}          {#var result {}; #loop 1 %1 tmp {#var result {${result}%2}}};
+#function {string2list}     {#list result {create} {%1}};
+#function {list2string}     {#var l {%1}; #list l simplify; #return {$l}};
+#function {strListSize}     {#list l create {%1}; #list l size result};
+#function {indexOfStrList}  {#list l create {%1}; #list l find {%2} result};
+#function {indexOf}         {#var l {%1}; #list l find {%2} result};
+#function {trim}            {#format {result} {%p} {%1}};
+#function {trimAll}         {#var result {%1}; #replace {result} { } {}};
+#function {sort}            {#var l {%0}; #var result {}; #foreach {$l} {tmp} {#list result {sort} {$tmp}}; #return {@list2string{{$result}}}};
+#function {sortList}        {#var l {@list2string{{%0}}}; #var result {}; #foreach {$l} {tmp} {#list result {sort} {$tmp}}};
+#function {listSize}        {#var l {%1}; #list l size result};
+#function {listSet}         {#var l {%1}; #loc idx {%2}; #loc data {%3}; #var l @listExtend{{$l};$idx}; #list l set {$idx} {$data}; #return {$l} };
+#function {listExtend}      {#var l {%1}; #loc len {%2}; #loc size {@listSize{{$l}}}; #math len {$len - $size}; #if { $len > 0 } { #loop {1} {$len} {id} { #list l add {{}} } }; #return {$l} };
+#function {len}             {#format result {%L} {%1}};
+#function {toLower}         {#format result {%l} {%1}};
+#function {capital}         {#format result {%n} {%1}};
+#function {replace}         {#var result %1;#replace result {%2} {%3}};
+#function {inList}          {#math result { @indexOf{{%1};{%2}} > 0 }};
+#function {center}          {#format len {%L} {%1}; #math left {(%2 - $len) / 2 + $len}; #math right {%2 - $left}; #format result {%${left}s%${right}s} {%1} {}};
+
+#function {max}             {#var result {%1}; #foreach {%0} {i} {#if { $i > $result} { #var result {$i}}}};
+#function {min}             {#var result {%1}; #foreach {%0} {i} {#if { $i < $result} { #var result {$i}}}};
+
+#function {parseTime} {
+    #local {timeStr} {%1};
+
+    #nop 注意这里用了个小技巧,末尾的空格不要去掉;
+    #replace timeStr {%S小时}   {@c2d{&1}*3600+ };
+    #replace timeStr {%S分}     {@c2d{&1}*60+ };
+    #replace timeStr {%S秒}     {@c2d{&1}};
+
+    #local time {};
+    #math time {$timeStr + 0};
+
+    #return {$time};
+};
+
+#function {c2d} {
+    #local string {%1};
+
+    #local number1  {};
+    #local number2  {0};
+    #local number3  {0};
+
+    #local chr      {};
+
+    #local ch {};
+	#parse {$string} {ch} {
+        #if { "$ch" == "{1|2|3|4|5|6|7|8|9|0|\.}" } {
+            #format number1 {%s%s} {$number1} {$ch};
+            #continue;
+        };
+
+        #switch {"$ch"} {
+            #case {"零"}    { #format number1 {0}   };
+            #case {"一"}    { #format number1 {1}   };
+            #case {"二"}    { #format number1 {2}   };
+            #case {"三"}    { #format number1 {3}   };
+            #case {"四"}    { #format number1 {4}   };
+            #case {"五"}    { #format number1 {5}   };
+            #case {"六"}    { #format number1 {6}   };
+            #case {"七"}    { #format number1 {7}   };
+            #case {"八"}    { #format number1 {8}   };
+            #case {"九"}    { #format number1 {9}   };
+            #case {"十"}    {
+                #if { "$number1" == "" } {
+                    #format number1 {1};
+                };
+                #math number2 {$number2 + $number1 * 10};
+                #format number1 {};
+            };
+            #case {"百"}    {
+                #math number2 {$number2 + $number1 * 100};
+                #format number1 {};
+            };
+            #case {"千"}    {
+                #math number2 {$number2 + $number1 * 1000};
+                #format number1 {};
+            };
+            #case {"万"}    {
+                #math number3 {($number2 + $number1) * 10000};
+                #format number1 {};
+                #format number2 {0};
+            };
+            #case {"亿"}    {
+                #math number3 {($number2 + $number1) * 100000000};
+                #format number1 {};
+                #format number2 {0};
+            };
+        };
+    };
+
+    #if { "$number1" == "" } {
+        #format number1 {0};
+    };
+
+    #local number {};
+    #math number {$number1 + $number2 + $number3};
+    #return $number;
+};
+
+#function {strWidth} {
+    #local str {%1};
+    #replace {str} {<{[gG0-9A-Fa-f]{1,8}}>} {};
+    #replace {str} {{\e\[[0-9;]+m}} {};
+    #return {@len{$str}};
+};