doumimi 发表于 2024-8-7 17:09:59

【Paotin++】入门系列之二: 机器基础

本帖最后由 doumimi 于 2024-8-7 05:29 PM 编辑

【Paotin++】入门系列之一: 客户端基础 - 技术园地 - 北大侠客行MUD论坛 - Powered by Discuz! (pkuxkx.com)

上一篇文章主要介绍了PaoTin++客户端的一些基础的、含有特色功能,这一节介绍一下 开发机器的基础。
大家学习写机器,一步一步来,可以先通过简单的机器,来记录任务信息。 逐步升级到能半自动化,比如接了任务,能自动的跑到任务地点。 最后再逐渐完善,到尽量的全自动。 上来就想着全自动,不是一个新手该干的事情,一口气吃不成胖子!


一:PaoTin++目录结构

在安装完pt之后,应该会有两个目录 paotin以及 my-paotin
paotin:这个是客户端的目录,里面的内容千万不要改动。 里面包含了很多代码,比如逍遥行,path.Walk的实现等, 可以用来参考学习。
my-paotin: 这个是用户自己的目录, my-paotin/data放数据,my-paotin/etc放配置, my-paotin/log放日志, my-paotin/plugins 放机器。   example : my-paotin/plugins/xuexi.tin

假如你已经是个高手, 感觉paotin下面的一些内容不符合心意,那么依然千万不要动paotin原本的代码。HELP load-module 可以看到机器加载的顺序,以及原理,可以通过覆盖加载的方式来实现你的目的。

二:代码执行顺序

本节内容可以先大致看一眼,有个概念。后面介绍具体内容的时候,还会提到。

1. 用户在控制台上输入的命令,或者机器发送的命令, 先经过客户端处理。
2. 客户端进行命令的解析,别名的替换,白名单检查等工作。
- 解析:比如如果输入 #help,那么这就是一个tintin的客户端命令, 客户端直接处理,不会发送给北侠服务器。如果不是客户端命令,那么认为是用户要发送给服务器的。
- 别名:进行命令的替换。
- 白名单:Paotin++特有的功能,拦截明显异常的命令来大大减少北侠服务器的压力。
3. 客户端发送命令到服务器。
4. 服务器对命令进行处理,如果不识别,返回【什么?】, 如果识别,那么处理后返回结果,比如 移动、背包、kill之类、战斗信息等结果。
5. 结果到达客户端, 客户端进行处理, 用户可以通过action来进行下一步逻辑、 美化(pt框架做了一些)、日志分类(pt框架做了一些)等。



doumimi 发表于 2024-8-7 17:13:17

本帖最后由 doumimi 于 2024-8-7 05:20 PM 编辑

三:基础语法
Paotin本身是基于tintin的, 只是对tintin的命令封装成了更加易用的功能, 所以推荐大家完整的看一遍Tintin的中文手册,有个基本的概念, 本文只介绍最常用的几个功能,会用这几个功能,就能看懂、开发游戏中至少80%的机器。

#alias 别名
语法: #alias {名称} {命令} {优先级}
用法:是一个基础的重要功能,逻辑的封装,命令的简化都靠alias了。
#alias acd {ask you about 闯荡江湖};
acd;
#nop 等于 ;

#alias acd2 {ask you about %1};
acd2 闯荡江湖;
#nop 等于 。此时 %1 就代表 闯荡江湖;

#alias acd3 {ask you about %1 %2 %3 %4};
acd3 闯荡江湖 哈哈 嘿嘿 吼吼;
#nop 等于 。<font color="#ff0000">此时%1 %2 %3 %4分别代表 闯荡江湖 哈哈 嘿嘿 吼吼.;</font>

#alias acd4 {ask you about %1 %2 %3; #local temp %2; okLog $temp};
acd4 闯荡江湖 {哈哈 嘿嘿} 吼吼;
#nop 注意和acd3的不同, 其中 第二个参数通过大括号括起来了, <font color="#ff0000">此时 %1表示闯荡江湖 %2表示[哈哈 嘿嘿] %3表示 吼吼.;</font>

#alias {acd3 %1 abt %2} {ask %1 about %2};
#nop 别名还有这种用法,不过萌新完全忽略就行,完全可以用方式2代替;
取消别名通过#unalias 名称 来实现。 比如 #unalias acd; #unalias acd3;


doumimi 发表于 2024-8-7 17:13:53

本帖最后由 doumimi 于 2024-8-20 07:38 PM 编辑

#action 触发
语法: #action {文本} {命令} {优先级}
用法:比较核心的命令, 因为mud文字游戏,你要进行的游戏逻辑就是靠服务器给你的反馈来进行的。 直接看example
#act {^刑捕头在你的耳边悄声说道:我们的线人在%*的%*留下了一幅图,看清楚之后找画师画好一样的送到%*的%*。} {okLog 图画:%1的%2。 线人:%3的%4}
里面这个就是系统发过来的一个内容, 是一个任务npc给我的任务。 在action中,还有几个关键信息通过正则表达式匹配了下来。 在后面使用的时候,也分别是%1,%2,%3,%4 这几个参数。

哪些内容能触发?
所有服务器返回的内容,都能够进行action的触发, 比如npc的对话, hp命令的返回状态面板,look命令的返回地图信息, 打架时返回的战斗回合信息等。 具体请参考上面代码执行顺序的第5步。
但是, 屏幕上看到的信息,除了服务器返回的,还有一些Pt框架给出的信息, 比如【准备保卫!!!】, 或者一些你自己通过okLog打印的消息, 这些内容是不会被触发的。

触发的写法的区别对比:
假如我们要匹配的一个npc的原话如下 【刑捕头在你的耳边悄声说道:我们的线人在扬州的中央广场留下了一幅图】
#act {^刑捕头在你的耳边悄声说道:我们的线人在%*的%*留下了一幅图$} {}
#nop 这种是最完整的写法, 比较特殊的就是开头和结尾,分别有个 ^和$符号, 这两个符号表示匹配范围的开始和结束,也就是说, 在“邢捕头”这三个字之前,如果有其他字符,那么就无法匹配到这个action, 结尾同样,如果【图】后面有任意的内容,也匹配不到。

#act {刑捕头在你的耳边悄声说道:我们的线人在%*的%*留下了一幅图} {}
#nop 这种写法和上面那种对比,去掉了开头和结尾的限制, 效果就是哪怕前面和后面加上内容,也能进行触发。

#act {%*线人在%*的%*留下了一幅图%*} {}
#nop 这是第三种,前后都换成了%*,并且缩短了匹配的内容。这种肯定也能匹配到,就是可能会由于%*能匹配到的内容太多导致 误触发。

#act {线人在%*的%*留下了一幅图} {}
#nop 同样的文本,如果方法3能触发,那么这种写法也能触发,他俩的触发机制是一致的。 区别在于:这种写法的捕获的内容更少。大家可以okLog %0看一下, 他只捕获了匹配那里面写的内容, 前后都是不知道的。这四种写法,推荐的顺序就是 1、2、3、4. 因为写的越完整,误触发的几率就越小。

action的取消方式:
1. 使用#unaction 来取消某个 action。平时我也很少用,因为action和其他的几种语法的有个重要的区别就是触发没有名字,需要完整的写出来具体的action才行。比如#unaction {^刑捕头在你的耳边悄声说道:我们的线人在%*的%*留下了一幅图$};
2. 如果写在了顶级块里面,可以通过KM来取消action, 如果是写在次级块里面,可以通过 kill class的方式来取消。 具体关于顶级块和次级块的概念,参见后面 代码块章节。

另外,推荐action的命令里面,只写简单的内容,比如一个oklog以及一个alias,把逻辑都封装到alias里面,方便管理。

$E是炮式触发,萌新可以先忽略,完全不用也不影响游戏体验, 具体想了解的 请看 HELP STYLE 第五节 。 这个主要就是为了方便查错, 防止冲突,并非功能上的新用法。



doumimi 发表于 2024-8-7 17:14:24

本帖最后由 doumimi 于 2024-8-7 05:23 PM 编辑

#delay 延迟执行
语法:#delay {名称} {命令} {延迟时间}   或者#delay {延迟时间} {命令}   
用法:delay也比较简单, 就是在一定时间之后,执行什么事情, 最低可以0.01秒。
#delay {1} {ask han about job};这个就是1秒之后 执行ask han about job。

值得注意的是, #delay 命令是不会阻塞后面的命令的执行的, 给两个例子。
#alias testdelay {north; #delay 1 {south}; east; #delay 1.5 {west}};
#nop 上面这个命令,实际的执行顺序如下 先执行 north和east, 等1秒后执行 south, 再等0.5秒后执行west;

#alias testdelay {
    north;
    #delay 1 {
      south;
      east;
      #delay 1.5 {
            west
      };
    };
};
#nop 上面这种delay的嵌套写法,可以实现,按照代码的前后顺序来执行,也就是 先执行north, 等1秒执行south,east. 在等1.5秒后执行west;
取消delay的用法:
#undelay {delay名称};

#nop 例子;
#delay abc.delay {haha} 3; #undelay abc.delay;



doumimi 发表于 2024-8-7 17:14:55

本帖最后由 doumimi 于 2024-8-7 05:24 PM 编辑

#ticker 定时执行

语法:#ticker {名称} {命令} {间隔时间}
作用:就是定时执行。也比较简单, 间隔时间间隔最低0.1
#ticker ttk {#showme 123} 1;
#nop 设置一个定时器,名称叫ttk, 每隔1秒后会执行 #showme 123;

#untick ttk;
类似于action, ticker的命令也推荐只写一个别名, 具体的逻辑封装到别名里面。


doumimi 发表于 2024-8-7 17:15:26

本帖最后由 doumimi 于 2024-11-5 07:07 PM 编辑

#var 变量
语法:#var {变量名称} {变量值};
作用:定义一个全局变量, 变量的作用通常是用来记录信息、流程判断。 注意!#var 一旦定义就是全局的,所以在控制台,或者任意机器里面定义的var。 都能在其他机器访问到。
如果不想定义全局的变量,可以用#local 来定义变量。 #loacal 和 #var的唯一区别就是生命周期范围不同, 本篇文档主要是给萌新入门用的, 所以不细讲,在前期哪怕所有的机器都不用#local 都没有问题。(后面技术好了之后,最佳实践还是多用local,具体可以多学习pt源码)

Tintin的变量最常见的有4种类型,或者说4种用法:
1. 字符串。
2. 数字。
3. 列表。
4. 表。

字符串类型
#action {^%*说道:你要去%*的%*找一个叫%*的人$} {
    okLog 捕捉;
    #var npc-name %1;
    #var task-area %2;
    #var task-room %3;
    #var task-target %4;
};

#alias taskinfo {
    okLog name: $npc-name;
    okLog area: $task-area;
    okLog room: $task-room;
    okLog target: $task-target;
};

#show {张三丰说道:你要去扬州的当铺找一个叫张全蛋的人};
taskinfo;

上面是一个最常用的使用变量的例子,就是将任务文本,或者提示给抓到,并且将其中的重要信息提取到变量中, 大家自己先试一下,有几点说明一下:
1. 代码的执行顺序就是从上到下的,先声明了一个action(此时里面的内容还没有执行), 然后声明了一个别名taskinfo(此时里面的内容也没有执行)。 然后执行#show。 此时show的内容会触发action,然后执行action里面的赋值代码。 然后调用taskinfo这个别名, 开始执行taskinfo里面定义的4个okLog.
2. #var 定义的变量是全局有效的。如果 $npc-name 已经赋值好了。 那么在另外一个机器里面,也能通过 $npc-name来获取到变量。

数字类型
#var count 1;
#math count {$count + 2};
okLog count: $count;数字类型就是可以通过 #math来对变量进行运算。 比如有些任务要执行一个动作多次, 或者进行一个循环的处理判断,都可以用数字类型。

列表类型
列表的定义,以及列表的操作方式。
#var t-list {};
#list t-list create {a;b;c};
okLog &t-list[]; #nop 输出列表元素数量 3;

okLog $t-list[]; #nop 输出列表的所有值 abc;
okLog $t-list; #nop 输出列表的第二个值 b;
okLog $t-list[-1]; #nop 输出列表的最后一个值 c;

okLog *t-list[]; #nop 输出列表每个元素的key,也就是 123;

上面这段代码创建了一个列表, 然后列表中的元素有3个,abc。直接访问列表的话,有3个符号很重要, 分别是 &取数量, $取value,*取key。

三种遍历方法:
okLog 遍历列表;
#foreach {$t-list[]} {value} {
    okLog $value;
};

#foreach {*t-list[]} {index} {
    okLog $index;
    okLog $t-list[$index];
};

#loop 1 &t-list[] {index} {
    okLog $index;
    okLog $t-list[$index];
};
列表的最常用的几个方法:
create , add , find, delete
#var t-list {};
#list t-list create {a;b;c};
#list t-list add {d}; #nop 增加一个元素;
#var t-list; #nop 打印这个列表,查看结果;
#var t-index aaaa;
#list t-list find {b} {t-index}; #nop 查找是否含有b这个元素,并且把这个元素的index赋值给t-index这个变量, 此时应该为2;
okLog $t-index;

#list t-list delete 1; #nop 删除第一个元素,此时应该删除的是a;
#var t-list;

表类型
#var testvar {
    {扬州} {当铺}
    {襄阳} {酒店}
    {扬} {{x}{扬1} {m}{扬2}}
};
#var testvar[扬州];
#var testvar[襄阳];
#var testvar[扬];
#var testvar[扬];

#nop 另外一种写法;
#var testvar[扬州] 当铺;
#var testvar[襄阳] 酒店;
#var testvar[扬] 扬1;
#var testvar[扬] 扬2;

#var testvar;
上面是定义一个表的两种写法, 上面的写法就是直接写在变量里面,通过两对大括号括起来, 前面的大括号就表示一个key,后面的大括号里面的内容就是value。如果不好理解的话,直接看第二种就行,这种写法和直接定义一个变量使用起来区别不大。表类型的适用场景:
1. 明显适合用键值对 key-value这种形式的场景。
2. 表定义的变量更加的聚合, 方便把一系列相关的内容,封到一个变量里面,方便查看。 比如#var char。 大家可以看下pt对于角色信息是如何封装的。
3. 同一个任务的相关变量都可以封装到一起,比如纪晓芙任务, 可以jxf jxf jxf 。 这样完成一次任务要进行变量清空的时候,可以直接 #unvar testvar。 里面所有的内容都无法访问了。



doumimi 发表于 2024-8-7 17:16:03

本帖最后由 doumimi 于 2024-8-7 06:38 PM 编辑

#if else 逻辑分支
上面我们学会了变量的定义,那么这节讲逻辑分支,最重要的就是#if else。
#var testa 纪晓芙;
#var testb 123;
#if {$testb > 100 && "$testa" == "纪晓芙"} {
    okLog 找到了;
};
#else {
    okLog 没有找到;
};
注意几点
1. 在进行变量判断的时候, 如果是数字,那么可以用 数学相关的比较符号来进行计算。 比如大于等于。
2. 在条件中支持 与和或, 上面的例子中的 && 就是与的意思, 两边的条件都需要满足。如果是 || , 那么两边只用满足一个就行。
3. 如果要进行字符串相关的判断, 要注意 变量名称 和 最终比较的字符串,都要加上双引号。
4. if 和 else 是对应的关系, 执行了if ,就不会执行 else。 如果需要执行else, 一定不会执行if 。 其中中间也可以穿插elseif。 这里就不展开讲解了。
5. if 里面的内容也是完全支持正则表达式的,就可以实现很多丰富的功能。




doumimi 发表于 2024-8-7 17:16:35

本帖最后由 doumimi 于 2024-8-13 06:20 PM 编辑

四、 LM KM RLM 以及 代码块
    - 常用 PaoTin++ 命令介绍
      - 查看模块列表: MODS 或 list-modules,其中模块名称可以点击。
      - 查看模块详情: MOD <模块名称> 或 look-module <模块名称>,名称可以通过 MODS 查看。
      - 加载模块: LM <模块名称> 或 load-module <模块名称>,参见 HELP load-module。
      - 卸载模块: KM <模块名称> 或 kill-module <模块名称>,参见 HELP kill-module。
      - 重新加载: RLM <模块名称> 或 reload-module <模块名称>,参见 HELP reload-module。      - 顶级块:直接写在文件最外层的,不包含在任何定义块中的定义块,被称为顶级块。      - 次级块:那些被包含在别的定义块当中的嵌套定义块,被称为次级块。
关于机器的加载、卸载、重加载也是新人需要了解的,但这部分没什么好讲的,这部分就直接引用 HELP paotin的文档了,大家记着就行。

我想再说一下顶级块,次级块。
顶级块:比如直接写在文件里面的 #alias xx {}; 或者一些#action {} {}。 这些代码执行的效果就是定义、注册或者说声明了这部分内容到客户端。 你也可以比如直接写个 okLog xxx; 或者 path.Walk xxxx; 或者开启一个定时 #tick .或者 直接写个#if 判断, 这些所有直接写到外面内容,都称为顶级块, 也就是LM的时候会执行一次 (不要搞混#alias的定义,和#alias的调用)。 KM的时候,也会删除除了变量以外的所有顶级块的内容,比如定义的别名,action,定时器等。
次级块:比如 比如在 #alias 的命令里面,我再写一个#alias 或者 #action, 后面两个就被称为次级块,简单的来看就是有嵌套关系,在里面的部分是次级块。 次级块的代码,会在执行了外层的顶级块的alias或者action后才会生效,直接LM后,里面的内容是没有执行的。 同理,KM 无法对其生效。

下面举例说明: 假如我写了个test.in的机器。
#alias testmod1 {
    okLog testmod1;
    #alias testmod2 {
      okLog testmod2;
    };
};
首先,在不执行 LM test的时候,testmod1 和 testmod2肯定都是无法执行的。
1. 此时我们执行 LM test. 此时会从上到下执行一遍代码中的顶级块。 所以,此时会执行 #alias testmod1 {xxx} ; 这个代码的含义上面讲过,【声明一个别名】,注意,此时只是声明了,但是还没有调用。 我们可以通过#alias testmod1 以及 #alias testmod2 来验证,此时发现, testmod1已经有了,目前还没有 testmod2这个别名。
2. 此时我们执行 testmod1.那么这个别名干了啥呢? 打印一个okLog, 然后再【声明】一个testmod2的别名。 再通过上面的验证方法, 可以发现,两个现在都有了。
3. 此时我们KM test。 再验证,发现 testmod1 没了, testmod2依然可以用。

这就是为什么:新人总发现, 我把模块卸载了,为什么好像有些action还在工作,或者某些tick还在执行, 因为他们在次级块!!! 当然写次级块里面是没有问题的,好的机器就是会有嵌套。 那解决办法是什么呢? action,alias,tick之类的都可以通过class来包裹一下,及时的做好class的kill的处理。 或者写一些 untick,unaction,unalias ,比如 #alias dushu.stop {#untick tk.du};

原理进阶:tintin 是通过class来管理代码的,回到上面的实验的第二步, 此时 输入 ALIS test你会发现, 这几个alias的class不同,所以在 KM 的时候,也就无法回收了。

更详细的请仔细阅读HELP paotin。



doumimi 发表于 2024-8-7 17:17:15

本帖最后由 doumimi 于 2024-8-7 05:28 PM 编辑

# 机器的思考

到底应该先玩游戏,还是先搞机器? 总体来说还是建议以游戏为主, 尤其是刚接触的时候,不建议跟一个机器死磕, 非要写到完美。机器是用来优化游戏体验的, 可以先从抓action,然后写一些简单的alias来优化自己的体验最重要。因为随着你的游戏理解加深, 你会发现不同任务之间是有共性的, 比如你做慕容仆人的时候,你可能想着的是怎么杀死这个慕容的敌人, 等到做的任务做多的时候呢,你会想着,不同的敌人要处理的方式不一样。 等你玩的门派多的时候,你会发现你需要一个单独的管理战斗的机器模块。 再等你 四职业都体验完成之后,你会发现有些职业只需要抱头蹲着。总之,你的需求也会随着游戏理解的增加而越来越多。

1,机器,或者说写机器的水平
2,经验值,或者说对游戏的理解程度
3,游戏资产
这 123 就好像是长方体的长宽高。你会发现,只提高一方面,收益不大,三方面都提高,收益最大。
如果只提高机器水平,不提高另外两方面,你就是根筷子。
如果只提高机器水平和对游戏的理解(背 wiki),你就是张印度飞饼。
如果你三个方向一起提高,你就顶个球!
-------------from dzp
机器是用来解决游戏中的问题的, 那么针对同样的问题, 解决思路是多样的,同一种思路的实现手法也是多样的。
1. 解决思路是多样的:机器的实现不是一成不会变的, 也比如学习机器。 既可以通过 action来实现,也可以通过 tick来实现。 那两种方式其实各有优略, 比如,通过action的学习可能就非常的快, 但是问题就是有一定的可能会搞出来撞墙机器人,通过tick来弄呢,效率不如action,但是相对更稳一些,不会出现少了某个action的触发,整个机器就断掉。 也有可能两个结合起来,才是最好的。
2. 实现手段是多样的:比如我想要每隔几秒干一个事情, 除了用tick, 也可以用#delay递归的来实现。
#alias gogo {
    #if {$stop == 1} {
      okLog 该结束了;
            #return;
    };

    #delay 5 gogo;
};#nop 递归实现每隔5秒执行一次
此贴到此完结!

londey 发表于 2024-8-7 17:19:05

加油,会写事件相关的攻略吗,这个的教学没怎么看到。
页: [1] 2
查看完整版本: 【Paotin++】入门系列之二: 机器基础