跳转至内容
  • Hellclient.net重建记录

    Mud客户端倒腾
    3
    3 帖子
    53 浏览
    jarlyynJ
    先把代码总体架构给组织理顺了。 之前的代码走的按功能按组件划分。现在将每个模块分了3个层,按层整理代码。 这样的优点是有测试的可能。hellclient本身因为都是散布的组建,其实很难做分块的测试。 同时找代码也更方便一点。 毕竟hellclient是从0开始长起来的,互相之间的依赖肯定更重。 hellclient.net是重构项目,能花时间考虑怎么组织和布局。
  • hellclient.net实验性项目开启

    Hellclient软件
    1
    1 帖子
    40 浏览
    jarlyynJ
    此项目是备用计划,最终可能会废弃。 主要是hellclient在实际使用一段时间,发生了一些不可调和的问题。 hellclient采用go语言开发,一开始的目标是纯go,无额外依赖,外部扩展的方式。 但因为js实际的性能问题,引入了v8引擎。 在windows下go嵌入v8引擎十分麻烦,使用的v8go包最后一次更新在2023年,而且很早的版本就放弃windows支持了。可见的短时间内不可能有windows版本的更新。这导致hellclient为了跨端兼容智能使用很老版本的v8go,而且与linux-arm版本冲突。 所以开一个备用计划,看看是否能依托微软的clearscript库,实现一个完全兼容hellclient当前版本的实现。
  • 深入浅出制作全自动Mud机器人-整体架构

    Script脚本
    1
    1 帖子
    156 浏览
    jarlyynJ
    以最靠近Mud服务器到最靠近用户排序 适配器层 做机器人的第一步就是在机器和MUD实际的交互中加一层适配器层,机器人只与适配器层交互,避免与客户端/Mud的过度耦合。适配器层包括两个组件 事件系统。将所有的Mud的回显/断线连线封装成行事件。这样遇到需要修正Mud信息,应对Mud更新时只需要修正相应的事件就行,也提供在事件中加入额外处理的加入空间 发送队列。绝大部分发送的内容压入队列,并在队列里进行解析/别名注册管理,使得机器不需要考虑发送的细节 信息配置管理层 配置使用基本统一的格式,由默认值/变量/数据文件一次覆盖获得最终结果,使得配置模块与业务逻辑独立,业务逻辑之需要注册新的参数,由具体的UI/变量负责加载和解析 驱动模式层 我整个机器是使用的 类似 Promise的Command队列进行管理,并提供必须的队列控制指令,比如Flush,Insert,Append,Snapshot,Rollback,提供统一的驱动引擎 工作编排系统 提供了#start+quest变量,以及#do的用户指令队列两种形式,使得用户能在不修改代码的情况下,尽可能的进行自定义流程管控。配合信息配置层的设置,尽量在变量和输入层面就能取代大部分代码工作 用户交互 通过助理按钮和别名系统,提供尽可能方便的设置功能,便于用户使用机器,以及在手机上进行操作。
  • 深入浅出制作全自动Mud机器人-图形任务

    Script脚本
    1
    1 帖子
    71 浏览
    jarlyynJ
    有一小部分Mud会有试图用字符图形化传达信息的任务。 这种任务,如果少量,算挺有情趣。如果出现大量,那就基本是一个纯机器向的MUD给机器制作者分级的机制了。 图形任务,有各种各样的形式,难以做一个统一的介绍,但可以整理下思路。 一般来说,图形任务的生成是走以下流程 生成随机任务信息 加入干扰元素 渲染 所以,对于图形化任务的处理,基本上也是按这个顺序倒过来依次处理。 提取信息。利用问与答章节中的方法,提取到代表图形的多行文本,并从多行string转换为2维数组。 标准信息化,将渲染后的信息,替换和维护2维数组,有效信息用自己的个固定符号(token)替换 去干扰。根据不同的任务形式会有不同。可能是对比,可能是可行读判断等等。 解析出任务信息。开始任务。 对于图形任务,已经脱离了对手动/体验型玩家的基本尊重了,很容易成为服务器LPC对抗VPS的脚本对拼,个人建议,不要投入太大精力。毕竟做机器只是为了游戏本身的乐趣或者体验变成的快乐,而不是对抗某个后者某群WIZ的自我满足感。所以,建议点到为止即可。
  • HellMapManager 2026.05.11发布

    HellMapManager地图编辑器
    1
    1 帖子
    165 浏览
    jarlyynJ
    升级Avalonia版本,提升界面性能 下载地址
  • 深入浅出制作全自动Mud机器人-延时响应

    Script脚本 全自动
    1
    1 帖子
    105 浏览
    jarlyynJ
    延时响应是Mud解密中很常见的一个状态。 指当你发出指令后,游戏中的NPC或者场景会在一定时间后,或者断断续续的做出响应。 一般来说,延迟响应为了考虑到玩家的体验,服务器的响应需要进行特别复杂的处理,使用简单的匹配和Mode标记就可以处理。 之所以单独作为一个课题讨论,是因为延时响应的核心处理点是 失效管理。 普通解密,触发器一般是即用即关的,不需要考虑太多的意外状态,通过触发组就能很好的解决了。 而延时解密,因为要考虑到可能会有终止/意外/并行的情况,需要手动的对出发考虑失效和重复生效、计时器延时等状况,触发器组就很捉襟见肘了,一般需要更多的代码工具去进行管理。 我这里是通过自己的Task/Plan模块,将所有的触发/计时器/事件处理封装在一个Task对象内,并强制使用一个作用范围强制失效来解决这个问题的。 参考代码: let baohuwait = 28 * 1000 let matcherKill = /^你对(.+)的(黑衣人|邪派高手|绝世高手)喝道:大胆狂徒,竟敢在这撒野!!/ //等待NPC出现的计划 let PlanProtect = new App.Plan( App.Positions["Quest"], (task) => { task.AddTrigger(matcherKill, (tri, result) => { if (result[1] != App.Data.Player.Score.名字) { return true; } App.Send("halt") let id switch (result[2]) { case "黑衣人": id = "heiyi ren" break case "邪派高手": id = "xiepai gaoshou" break case "绝世高手": id = "jueshi gaoshou" break } Baohu.Data.ID = id Baohu.Data.Type = result[2] }).WithName("ok") let wait = Baohu.Data.Start + baohuwait - $.Now() if (wait > 0) { task.AddTimer(wait, (timer) => { Note("准备迎敌") App.Send("halt") App.Core.Heal.TryTouch() $.RaiseStage("prepare") $.RaiseStage("baohu-ready") return true }).WithNoRepeat(true) } task.AddTimer(1100, () => { if (App.Core.Weapon.Touch) { if (($.Now() - Baohu.Data.Start) < baohuwait) { App.Send("halt") App.Core.Heal.TryTouch() $.RaiseStage("pause") $.RaiseStage("wait") } } return true }) task.AddTimer(3000, () => { let d = (($.Now() - Baohu.Data.Start) / 1000).toFixed(0) Note(`保护开始${d}秒`) return true }) task.AddTimer(60000, () => { App.Log("保护等待超时") return false }).WithName("timeout") $.RaiseStage("wait") }, (result) => { if (result.Name == "ok") { $.PushCommands( $.CounterAttack(`${GetVariable("id")}'s ${Baohu.Data.ID}`, App.NewCombat("baohu").WithTags(`baohu-${Baohu.Data.Type}`).WithPlan(PlanCombat)), $.Function(Baohu.Finish), ) $.Next() return } App.Send("halt") App.Log(`保护NPC${Baohu.Data.NPC.ID}失败`) Baohu.Fail() } ) 代码地址 可以看到,我使用了一个叫做PlanProtect的计划(task工厂),创建了一个基于Quest的封装,所有的触发和计时器都绑定在这个Task封装上,并在Quest发生变化时强制失效。 预期管理工具Plan/Task的详细介绍 由于我一直认为Mud机器人是一个工程问题,难点在于可维护性和代码失控。所以,我认为,选用或者开发一个触发失效工具,是十分重要的,也应该是整个机器人的核心驱动逻辑之一。
  • 深入浅出制作全自动Mud机器人-随机迷宫地图

    Script脚本 全自动
    1
    1 帖子
    56 浏览
    jarlyynJ
    随机迷宫以及地图是很多Mud引入的新任务类型。 比如下面就是一个典型的迷宫地图 ┌─┬─┬─┬─┬─┬─┬─┬─┐ │★│     │       │ ├ ┼─┼ ┼─┼ ┼─┼─┼ ┤ │ │ │   │     │ │ ├ ┼ ┼─┼ ┼ ┼─┼─┼ ┤ │ │         │ │ │ ├ ┼─┼ ┼─┼ ┼─┼ ┼ ┤ │   │ │     │   │ ├─┼ ┼ ┼─┼─┼─┼ ┼ ┤ │ │   │ │ │ │ │ │ ├ ┼ ┼ ┼ ┼ ┼ ┼ ┼─┤ │ │ │     │ │ │ │ ├ ┼ ┼─┼─┼─┼ ┼ ┼ ┤ │   │         │ │ ├─┼─┼ ┼─┼ ┼─┼ ┼ ┤ │     │     │   │ └─┴─┴─┴─┴─┴─┴─┴─┘ 这种地图本质就是在服务器创建了一系列的虚拟房间,并建立了房间中之间的联系,最后将这些方向和关系再打印成文字图案。 那么,处理方式也比较直接。 先将文字图标准化,化成比较简单的单字节图,然后再遍历建立临时房间和出口信息就可以。 参考HellMapManager.Dll的范例代码 local hmmlib=require('hmm') local hmm=hmmlib.new() hmm.DllEncoding=0 --0 for utf-8, 1 for gbk local json=require('json') local file=assert(io.open("hongchen.hmm","r")) local data=file:read("*a") file:close(); hmm:call("import",data) local extrooms={} local extpaths={} local function buildMyRoom(entry,roomid,roomname) local myroom1=hmmlib.Room.new() myroom1.Key="myroom-entry" myroom1.Name=roomname.."大厅" local myroomexit1=hmmlib.Exit.new() myroomexit1.Command="open gate;n" myroomexit1.To="myroom-home" local myroomexit2=hmmlib.Exit.new() myroomexit2.Command="out" myroomexit2.To=entry myroom1.Exits={myroomexit1,myroomexit2} local myroom2=hmmlib.Room.new() myroom2.Key="myroom-home" myroom2.Name=roomname.."卧室" local myroomexit3=hmmlib.Exit.new() myroomexit3.Command="open gate;s" myroomexit3.To="myroom-entry" myroom2.Exits={myroomexit3} table.insert(extrooms, myroom1) table.insert(extrooms, myroom2) local entrypath=hmmlib.Path.new() entrypath.From=entry entrypath.Command="go "..roomid entrypath.To="myroom-entry" table.insert(extpaths, entrypath) end buildMyRoom("2440","myhouse","大别野") local mazemaptxt=[[ ┌─┬─┬─┬─┬─┬─┬─┬─┐ │             │ │ ├─┼ ┼─┼─┼─┼─┼ ┼ ┤ │ │   │     │ │ │ ├ ┼ ┼─┼ ┼ ┼─┼ ┼ ┤ │  ♚  │ │ │   │ │ ├─┼ ┼ ┼ ┼ ┼─┼ ┼ ┤ │   │   │ │     │ ├─┼ ┼─┼ ┼─┼─┼ ┼─┤ │ │ │   │     │ │ ├ ┼ ┼─┼─┼ ┼ ┼─┼ ┤ │ │   │   │   │ │ ├ ┼ ┼─┼─┼ ┼ ┼ ┼ ┤ │ │ │     │ │   │ ├ ┼ ┼─┼ ┼ ┼ ┼ ┼─┤ │   │   │ │ │  ★│ └─┴─┴─┴─┴─┴─┴─┴─┘ ]] --♚为当前位置,★为出口 local Maze={} Maze.__index=Maze Maze.new=function() local self=setmetatable({},Maze) self.RoomPrefix="maze-" self.RoomKeys={} self.Rooms={} self.Paths={} self.EntryRoom="" self.ExitRooms={} return self end function Maze:buildRoomKey(x,y) return self.RoomPrefix..tostring(x).."-"..tostring(y) end function Maze:buildExit(room,tokey,command) local exit1=hmmlib.Exit.new() exit1.Command=command exit1.To=tokey table.insert(room.Exits, exit1) end function Maze:readmap(maptxt) local maplines={} for line in string.gmatch(maptxt, "[^\r\n]+") do if #line>0 then --因为utf8不定长,进行处理 line = string.gsub(line," "," ") line = string.gsub(line,"│","+") line = string.gsub(line,"─","+") line = string.gsub(line,"├","+") line = string.gsub(line,"┤","+") line = string.gsub(line,"┴","+") line = string.gsub(line,"┬","+") line = string.gsub(line,"└","+") line = string.gsub(line,"┘","+") line = string.gsub(line,"┌","+") line = string.gsub(line,"┐","+") line = string.gsub(line,"┼","+") line = string.gsub(line,"♚","I") line = string.gsub(line,"★","O") table.insert(maplines, line) end end local roomwidth=(#maplines[1]-1)/2 local roomheight=(#maplines-1)/2 local currentY=1 --按行处理 while currentY<=roomheight do local currentX=1 --处理每个房间 while currentX<=roomwidth do local roomkey=self:buildRoomKey(currentX-1, currentY-1) table.insert(self.RoomKeys, roomkey) local room=hmmlib.Room.new() room.Key=roomkey if maplines[currentY*2]:sub(currentX*2, currentX*2)=="I" then self.EntryRoom=roomkey end if maplines[currentY*2]:sub(currentX*2, currentX*2)=="O" then table.insert(self.ExitRooms, roomkey) end if maplines[currentY*2]:sub(currentX*2+1, currentX*2+1)==" " then self:buildExit(room,self:buildRoomKey(currentX,currentY-1),"e") end if maplines[currentY*2]:sub(currentX*2-1, currentX*2-1)==" " then self:buildExit(room,self:buildRoomKey(currentX-2,currentY-1),"w") end if maplines[currentY*2+1]:sub(currentX*2, currentX*2)==" " then self:buildExit(room,self:buildRoomKey(currentX-1,currentY),"s") end if maplines[currentY*2-1]:sub(currentX*2, currentX*2)==" " then self:buildExit(room,self:buildRoomKey(currentX-1,currentY-2),"n") end table.insert(self.Rooms, room) currentX=currentX+1 end currentY=currentY+1 end end function Maze:applyTo(environment) for _, room in ipairs(self.Rooms) do table.insert(environment.Rooms, room) end for _, path in ipairs(self.Paths) do table.insert(environment.Paths, path) end end local mymaze=Maze.new() mymaze:readmap(mazemaptxt) local query=hmmlib.QueryPathAny.new() query.From={"2522"} query.Target={"myroom-home"} query.Environment=hmmlib.Environment.new() query.Environment.Rooms=extrooms query.Environment.Paths=extpaths local result=json.decode(hmm:call("querypathany", json.encode(query))) if (result==nil) then print("No home path found") else print("Home path found:") print(json.encode(result)) end -- 遍历迷宫后离开 local queryall=hmmlib.QueryPath.new() queryall.Start=mymaze.EntryRoom queryall.Target=mymaze.RoomKeys queryall.Environment=hmmlib.Environment.new() mymaze:applyTo(queryall.Environment) result=json.decode(hmm:call("querypathall", json.encode(queryall))) if (result==nil) then print("No full maze path found") else print("Full maze path found:") print(json.encode(result)) end --遍历完成,离开路径 query=hmmlib.QueryPathAny.new() query.From={result["To"]} query.Target=mymaze.ExitRooms query.Environment=hmmlib.Environment.new() mymaze:applyTo(query.Environment) result=json.decode(hmm:call("querypathany", json.encode(query))) if (result==nil) then print("No maze path found") else print("Maze path found:") print(json.encode(result)) end 在处理动态地图时,核心还是不要想的太复杂,直接找出Room的对应元素,以及相应的Exit,然后根据需要遍历或者导航即可。
  • 深入浅出制作全自动Mud机器人-问与答

    Script脚本 全自动
    1
    1 帖子
    76 浏览
    jarlyynJ
    问与答是一个最为典型简单的Mud解密,是大部分Mud任务的基础与核心,可以作为我们研究Mud解密的入口。 对于一个标准的问答对话来说,一般是 主动发起ask 游戏提示 你向XXXX打听关于XXXX的事情 可能有部分干扰,没听清之类 紧跟着你打听的内容,NPC会进行回复。同时NPC也会恢复其他玩家的问题,这时可能对你有所干扰。 这时,很多老玩家可能会向你介绍多行匹配这一神器。 但很可惜,这只是试图逃避正确解决问答问题的逃课行为。 多行匹配完全不适合问答解密上。因为多行匹配无法进行逻辑处理,无法引入状态。也无法标准化处理。 因此,多行匹配在问答问题上,只能算是一个补丁。如果你的机器建立在大量补丁上,很快就会带来难以解决的架构问题。 那么,怎么来进行一个标准化的问答呢? 很简单,对NPC的回答进行录制。 在ask npc时,同时发送你的ask指令和同步指令。对应的,你会预期两个服务器的回复 你想XXX大厅关于XXX的事情:开始录制,把这行之后的每一行都追加到一个数组里 你的同步响应:结束录制,ask结束,进行下一步处理。 这样,就把问答问题,转换为对一个字符串数组的按行分析。 再配合简单的状态机形式,一般就是一个局部的Mode变量,就行解决大部分的问题了。 以hongchenjs的长安任务为例,需要向梁兴禄发起ask 任务,处理的代码如下 Changan.Check = function () { if (App.Data.Ask.Answers.length) { if (App.Data.Ask.Answers[0].Line == "梁兴禄对你说道:本府不是已经给你派发了差事,完不成的话就先跟我取消。") { Changan.Fail(); return; } if (App.Data.Ask.Answers[0].Line == "梁兴禄对你说道:你的江湖经验已经很高了,再在衙门里也混不出个名堂了。") { Quest.Cooldown(10 * 60 * 60 * 1000) App.Next(); return } if (App.Data.Ask.Answers[0].Line == "梁兴禄盯着你看了看,说道:“你刚取消过一次任务,过一分钟再来吧。”") { Quest.Cooldown(60 * 1000) // App.Core.Timeslice.Change("") App.Next(); return } if (App.Data.Ask.Answers[0].Line.match(matcherTooMany)) { Quest.Cooldown(4 * 3600 * 1000) // App.Core.Timeslice.Change("") App.Next(); return } if (App.Data.Ask.Answers[0].Line == "梁兴禄对你说道:嗯,既然你有心为衙门出力,我便给你个差事。") { Changan.All++; App.Send("changanjob") if (App.Data.Ask.Answers[1].Line == "梁兴禄说着便拿出一份公函交给你。") { Changan.Gonghan() return } Changan.Check2() return } } App.Fail() } Changan.Check2 = function () { let result = App.Data.Ask.Answers[1].Line.match(matcherCode) if (result) { Changan.Data.Name = result[2]; for (var i = 3; i < App.Data.Ask.Answers.length; i++) { let token = App.Data.Ask.Answers[i].Line.match(matcherCodeToken) if (token) { Changan.Data.Code += token[1].trim() } else { break } } Changan.Data.Type = "code" Changan.DoCode() return } else { App.Log("未知的长安任务对话") Changan.Fail() return } } 代码地址 能很好的看出怎么用标准录制的问答信息,来解决复杂谜题。
  • 深入浅出制作全自动Mud机器人-解密系统

    Script脚本 全自动 架构
    1
    1 帖子
    75 浏览
    jarlyynJ
    解密系统在mud机器人中,属于比较高层的应用层。 整个解密系统的本质就是:发出指令,期待响应,从响应中解析出系统状态,进入合适的模块。 一般而言,mud的解析会分为以下几种: 单行格式化信息 多行复杂信息 延迟信息 图形化信息 多重交互流 其中单行格式化信息和多行复杂信息,是比较正常的信息交互模式。 而延迟信息,图形化信息,多重交互信息,就大概率是巫师为机器人设置的障碍了。 在开始深入解析解密系统之前,我们先要确认一点,解密系统的基础。 所有的解密,本质来说都是从两步开始的。 确认信息同步 记录调阅多行信息 确认信息同步,是为了跟信息一个明确的结束状态。一般会准备一个平时不怎么使用的指令,用来做信息结束的确认。只有看到这个指令回复出现,才进入下一步的动作。之前的所有的出发,不直接执行指令,只改变状态/属性/变量,最后才进行决策。同时,这也与mud机器的同步问题有关。 同步参考 而记录和调阅多行信息,是模拟人对mud信息的处理。机器对mud是按行进行匹配的,但人对mud是根据历史多行进行阅读的。所以,在准备解密时,必须把所有可能有用的信息记录在行数组里。再根据确定的触发,匹配合适的上下文,分析数据。 对于机器人,我的观点是 Mud应该是为人设计的。如果完全不适合人玩,那这机器也没有做的意义。 这也是我的解密模块的理论基础。
  • HellMapManager嵌入脚本Dll发布

    HellMapManager地图编辑器
    1
    1 帖子
    86 浏览
    jarlyynJ
    HMM嵌入脚本Dll是将HMM程序本地编译为AOT版本,在dll里引用。 本质是将http api接口替换为c api,通过文本进行调用的接口。 优点: 跨客户端,跨语言。可以在mush/mudlet/ls等客户端,通过lua/python的方式使用,也可以独立的使用nodejs/lua/python进行调用 毫秒级别的路径查询(取决于地图大小/路径长度/CPU性能) 完全开源。基于Mit协议的宽松开源协议,你可以自由的扩展/改写/利用代码。 有Hell和红尘两款Mud的机器深度使用本库的算法 地图文件也可以通过 hmm.ts项目,在原生的javascript/lua中使用 有跨平台(windows/macos/linux)的编辑软件支持,编辑软件支持版本分差对比。 完善的文档/单元测试 支持多点对多点寻路(querypathany)规划路线 支持模拟移动逼近的方式动态生成便利路径(quarypathall)规划路线 支持按顺序进入房间的形式(querypathordered)规划路线 支持标签,支持带值的标签,以动态的决定某个出口只能被符合某个条件,或技能不低于某个值的情况下才启用/禁用。 支持房间黑/白名单,移动指令的黑/白名单来的形式动态规划路线 支持地图膨胀,一般配合模拟逼近的形式,遍历出生于目标区域会随机移动的NPC可以到达的房间 支持临时禁用出口,应对npc拦路/技能不符无法通行的情况 支持临时房间和路径,用以解决随机迷宫,自建房屋等问题 支持 直达捷径,用于实现flyto/rideto/miss等功能 支持对房间的可变信息保留多个版本的快照,并在快照内进行搜索 支持转码,可以支持utf-8以及gbk编码 限制: 作为作者,个人对用用只有以下三个希望 希望不要以Mud owner禁止的方式使用本库 希望不要在不欢迎本库的Mud宣传本库 希望不要在禁止或者不欢迎公开传播地图文件的mud 传播本库生成的地图文件 API: API文档 代码范例 预览版本下载链接: https://github.com/hellclient-scripts/hellmapmanager/releases/tag/embedding.2026.04.22
  • [开发]hellmanager.ts lua版本放弃性能优化声明

    HellMapManager地图编辑器
    4
    4 帖子
    204 浏览
    jarlyynJ
    简单的benchmark: gowrk+httpapi: 0.5ms/req nodejs:1ms/req dll+lua:3ms/req lua5.1/luajit:10ms/req
  • HellMapManager 2026.04.16版发布

    HellMapManager地图编辑器
    1
    1 帖子
    80 浏览
    jarlyynJ
    界面优化 在关系地图中,会显示房间对应的Marker信息
  • hellclient 2026.04.16版发布

    Hellclient软件
    1
    1 帖子
    166 浏览
    jarlyynJ
    更新内容为加入脚本shared数据功能和相关api shared为使用同一个脚本的游戏可以共享读取的数据。 相对于现有的数据: 脚本数据 只读,共享。 Mod数据 只读,共享,需要额外开启 用户数据 读写,独立。 共享数据 读写,共享。
  • Hellclient UI 2026清明版发布

    HellclientUI应用
    1
    1 帖子
    216 浏览
    jarlyynJ
    升级flutter版本 修正部分文字错误
  • HellMapManger及hellmapmanager.ts 26.03.16版本发布

    HellMapManager地图编辑器
    1
    1 帖子
    89 浏览
    jarlyynJ
    API版本更新到 1006 HellMapManger 变量页面显示优化
  • 1 帖子
    155 浏览
    jarlyynJ
    对于整个移动模块来说,最核心的可能就是移动失败处理了。 本身对于大部分系统来说,失败/意外流程处理都是最复杂的。而移动模块的失败处理大部分面对的是wiz的恶意,所以更强调可扩展性和可维护性。 对于移动失败,本质来说,就是: 发出移动指令 2.预期会收到成功信号,或者各种失败信号 3.如果收到成功信号,则对当前移动的这一步进行核销。 4.如果受到的是失败信号,则根据具体的失败信号数据进行处理,重规划路线/重试/放弃失败。 因此,移动失败是一个很典型是预期管理 我做的预期管理模块有很大一部分目的就是为了移动失败处理设计的。如果是其他语言/客户端的机器,也能通过各种类库或者触发器分组的功能进行预期管理。 以newhelljs为例,代码地址 App.Map.StepPlan = new App.Plan( App.Map.Position, function (task) { App.Move.RetryStep = false let tt = task.AddTimer(App.Map.StepTimeout, function (timer) { return App.Map.OnStepTimeout() }).WithName("timeout") task.AddCatcher("core.longtimestep", function () { tt.Reset(App.Move.LongtimeStepDelay) return true }) task.AddCatcher("core.retrymove", function () { App.Move.RetryStep = true return true }) task.AddCatcher("core.movereset").WithName("movereset") task.AddCatcher("core.wrongway").WithName("wrongway") task.AddCatcher("core.walkbusy").WithName("walkbusy") task.AddCatcher("core.walkresend").WithName("walkresend") task.AddCatcher("core.walkretry").WithName("walkretry") task.AddCatcher("core.walkfail").WithName("walkfail") task.AddCatcher("core.blocked2", (catcher, event) => { if (App.Core.Room.Current.ID == "") { return true; } }).WithName("blocked2") task.AddCatcher("core.blocked", (catcher, event) => { catcher.WithData(event.Data) }).WithName("blocked") task.AddCatcher("core.needrest").WithName("needrest") }, function (result) { switch (result.Type) { case "cancel": break default: switch (result.Name) { case "timeout": break case "movereset": App.Map.Room.ID = "" App.Map.Retry() break case "wrongway": if (App.Move.RetryStep) { App.Map.Resend(0) return } App.Map.Room.ID = "" App.Sync(() => { App.Map.Retry() }) break case "walkbusy": App.Map.Resend() break case "walkresend": App.Map.Resend(0) break case "walkretry": App.Map.Retry() break case "blocked": App.Move.OnBlocker(result.Data) break case "blocked2": App.Core.Blocker.BlockStepRetry() break case "needrest": App.Move.NeedRest() break case "walkfail": App.Move.OnWalkFail() break default: } } } ) 很明显,是对于整个移动做了预期。 首先这个预期是基于App.Map.Position的 Map库中有两个Position App.Map.Position 当前房间范围 App.Map.MovePosition 当前移动的范围 所以,当成功移动时,整个移动处理会因为离开范围而被"cancel" 当没有Cacnel时,说明意外出现了。 不同的信号(事件)会根据在switch里做分支判断,调用各种异常。 这也就是整个失败处理的底层逻辑 在 完全基于hmm开发的hongchengjs中,我为了可维护性,做了些调整,将常见的移动失败作为一个文本保存,启动时调入内存放在一个Hash表里,直接做表匹配模拟事件,算是一个小的结构优化。 代码地址 在失败处理中,有四个处理是最为典型的 第一个是路线错误 也就是"core.wrongway"事件。 触发后会清空当前房间信息,调用当前移动的Retry方法进行重新规划和移动。 第二中是路线禁用 也就是"core.blocked2"事件。 出发该事件后,说明被拦截,而且不该击杀然路Npc,会把当前房间和目标房间的出口临时禁用,然后重新规划路线。 第三种是插入战斗 "core.blocked"事件 会把当前移动进行快照,然后击杀拦路npc,再对快照进行还原,继续移动。也是很经典的形式。 最后是重新规划 "walkretry"事件 这个事件和路线错误一样都是直接调用重新规划。 区别是这个事件本质是一个衍生事件,在我的事件系统中,一般会调用Filter进行处理,一般是调整当前Move的上下文标签。然后再根据最新上下文标签进行重新规划。 这四种基本就是主要的移动失败处理的内容。当然,不同的Mud可能还会有一些细节上的调整。 移动失败处理的主要处理方式就是 重试。 调整上下文,重新规划,避免失败出口。 挂起移动,解决意外,还原移动并继续,
  • 深入浅出制作全自动Mud机器人-选项模式

    已移动 Script脚本 mud机器人 全自动 架构
    1
    1 帖子
    114 浏览
    jarlyynJ
    选项(Option)模式是一种新近流行的设计模式,用于配置类,主要流行于于Go语言(golang)。 每个设计模式本质上就是正对一个场景的一个常见小招式。 选项模式也不例外。它解决的主要问题是: 复杂系统的可复用编码配置。 典型的选项模式的形式是 var system=Systm.New(opt1,opt2,opt3...) 通过opt1,opt2,opt3来对系统进行初始化。 复杂系统 选项模式正常情况下一定是解决复杂系统的问题的。 甚至可以简单粗暴的认为选项模式是一个改良的构建者(builder)模式,即配置器列表。 不复杂的系统没有使用选项模式的必要。 对于选项模式,最典型的就是和Builder模式一样,用来处理一个封装类,类里封装了大量的函数/接口,可以根据需要去把这些函数进行替换。 默认可用及扩展性 选项模式的优势其实很大一部分体现在可扩展性上。 由于选项模式的选项都是可选的,而且不可能在代码写完后自动扩展。 所以,选项模式中可以认为一定有部分选项没有被配置过。新建的封装类一定在所有选项中可以运行的默认值。 同样,因为有这个默契,大部分采用了选项模式架构的代码一定有很好的兼容行。因为添加的新的功能和实现必须与原有的行为表现一致。 可以认为这是对代码结构的一种架构上的约束。 基于功能/模块配置 选项模式的主要价值,就是体现在可以分块的,可选的进行配置。 因此,从逻辑上来说,每个选项代表了一种功能,或者模块。 具体来说,举个自理,我的go语言http请求库,默认情况下会生成一个标准请求。比如: var myreq=preset.New(Host("http://www.baidu.com/) 如果我这是一个Post请求,需要带一个纯文字body,代码就变成了 var myreq=preset.New(preset.Host("http://www.baidu.com/,preset.POST,preset.StringBody("12345")) 而这个请求如果还需要带上一个特定的请求头,那么就是 var mreq=preset.New(preset.Host("http://www.baidu.com/,preset.POST,preset.StringBody("12345"),preset.Header("auth","abc")) 从这个例子应该能喊好的看出选项模式的主要用法了。 在构建复杂系统时,按功能和模块来进行配置,同时提供很多预制件,可以直接初始化为需要的功能。 在系统进行升级扩展时,由于保持默认行为的一致性,也会有很好的兼容性。 在Mud机器人中的应用 从选项模式的特点来看 系统复杂 高扩展性 功能化,模块化 也只有移动模块这个复杂系统需要用得上选项模式。 我在newhelljs的移动模块就是使用了选项模式的架构,具体可以看相关的内容。
  • 深入浅出制作全自动Mud机器人-数模转换

    Script脚本 mud机器人 全自动 架构
    1
    1 帖子
    115 浏览
    jarlyynJ
    数模转换其实是一个很古早的概念了。 就是将原始的模拟信号,转换成数字信号,然后数字化的系统才能根据数字信号进行操作。 首先,从严格意义上说,服务器返回的文字信息本质是ansi控制文本,是一种带显示控制的控制,目的是进行一种美观的带格式的外观描述,这的确是一种 数字化程度很高的 模拟信号。 其次,对于实际的复杂业务来说,把数据抽象成业务数据,将代码隔离为业务层和交互层也是很通用的一种处理技巧,在实际需求上,的确有分割交互层(模拟信号)和业务层(数字信号)的意义。 所以,进行数模转换是一种我很推荐的架构形式。 数据标准化 数模转换而第一步是信息标准化。 也就是,将所有的信号,转为一种有约定格式和共性的基础信号。 很多时候,处理程序还需要对信号做一个2次处理,抽取和判断后生成原生信号的衍生信号。 在这个背景下,处理业务数据的代码才能做到信号形式的无关性。 具体到代码的例子。我在newhelljs里引入了 App.LineEvent = function (name) { return App.Engine.LineEvent(name) } App.FilterLineEvent = function (filtername, eventname) { return App.Engine.FilterLineEvent(filtername, eventname) } 两个方法。 App.LineEvent是直接将一个mud行的出发,抛出为一个制定name的event App.FilterLineEvent是定义一个处理器,在处理器里做预处理和判断,然后确定没问题的话再以eventname抛出事件 App.FilterLineEvent的应用范例: //处理房间名 App.Engine.SetFilter("core.normalroomname", function (event) { let words = App.History.CurrentOutput.Words if (words.length == 1 && words[0].Color != "" && words[0].Bold == true) { App.RaiseEvent(event) } }) 接到可能是房间名的信号后,判断是否符合条件,符合条件再抛出事件。 信号数据采样 在mud中,除了根据行进行分析的时间外,还有很多直接的数据信号取的护理。比如hp,score,skills,i等指令。 这时候需要将传输来的模拟信号(一般是固定格式的表格列表)转换为合适的数据模型,再加以保存。 对于能够主动触发的数据,也要做好缓存的处理。 工作重点 对于根据数模转换对代码进行分层,我们要明白主要的工作重点,这样能更好的根据实际情况进行合理规划。 我们的数模转换的核心目的是 让代码更清晰,易于维护,代码出现问题时能快速定位到相应模块 将业务与实际表现剥离,这样在Mud进行更新/更改界面时,不会需要对业务层进行调整。比较表现层比业务层测试快的多。
  • 深入浅出制作全自动Mud机器人-遍历

    Script脚本 mud机器人 全自动
    1
    1 帖子
    105 浏览
    jarlyynJ
    在Mud机器人中遍历其实有两种含义。 第一,依次经过所有给到的房间。 第二,依次经过给到的路径,在需要停止时暂停。 这里主要讨论第二种。 之前说过,每个移动是一堆和移动相关的判断函数的集合。 这里就是要实现继续移动或中止的判断。 参考我的core/zone.js,遍历而实现代码是 App.Zone.Finder = function (move, map) { wanted = App.Zone.Wanted move.Option.MultipleStep = wanted.SingleStep != true move.OnRoom = function (move, map, step) { let item = wanted.Checker(wanted) if (item) { wanted.Name = item.GetData().Name wanted.ID = item.IDLower if (map.Room.ID) { wanted.Loc = App.Map.Room.ID Note(wanted.Target + " @ " + wanted.Loc) } } } move.OnArrive = function (move, map) { if (wanted.Loc) { App.Map.FinishMove() return } wanted.Next(map, move, wanted) } } 很明显,这个Finder是用App.Zone.Wanted作为选项,对move进行了初始化。 这里覆盖了两个方法 move.OnRoom 经过房间(房间信息结束)的触发 move.OnArrive 移动结束的触发。 主要是因为可以多步移动。 代码使用 App.Zone.Wanted.Checker来判断房间中是否有遍历目标。 如果找到目标,则遍历结束(App.Map.FinishMove)。 是一个很标准的任务遍历模型。
  • 深入浅出制作全自动Mud机器人-迷宫

    Script脚本 mu机器人 全自动
    1
    1 帖子
    97 浏览
    jarlyynJ
    mud中,迷宫是一个很重要的课题。 其实这里说的迷宫,并不仅仅指迷宫这个解密形态,指的是非标准移动,动态出口。 有些需要特殊物品/ask npc才能通过的出口,其实也是迷宫的一种。 在我的第三版移动代码里 代码地址 Maze实现的依然有点丑。如果我还有第四版的移动代码的话,应该就是重写这一块了。 每个Maze类需要实现实现以下方法 CheckEnter 检查是否进入了该迷宫 CheckEscaped 检查是否离开了该迷宫 Walk 迷宫的下一步动作 OnStepFinsih 迷宫移动结束后的动作 很明显,在通过CheckEnter和CheckEscaped的组合判定迷宫生效后,通过覆盖Move的Walk和OnStepFinsih方法,来代替move的正常操作。 对于实际移动来说,根本不知道也不会去判断是路线规划返回的移动指令,还是迷宫返回的。 非常非常标准的一个补丁行为。 具体的Maze实现,可以参考 代码地址