跳转至内容
  • 1 帖子
    51 浏览
    jarlyynJ
    在移动模块介绍时我们说过,整个移动模块是Mud机器中最复杂的模块。 同时,移动模块又是Mud机器人中最底层最常用的模块,所以,怎么合适的设计移动模块是整个全自动机器人的核心问题之一。 我在不同时代的机器中使用了不同的架构模式(helllua,pkuxkx.noob,newhelljs) 最后采用的是 一个基于组合模式和选项模式的架构。 将移动的需求拆解为不同的处理函数,用默认值组合成一个标准移动 通过同一的选项模式进行设置,这样可以通过预设置的配置,迅速组建成一个可用的复杂移动 拆解 首先,我将所有移动统一,拆解为基础功能,以及关键点的处理函数。 把所有的处理函数综合起来,就是一个移动 范例代码 class Move { StartCommand = "" Data = {} Retry = DefaultMoveRetry Next = DefaultMoveNext OnRoom = DefaultMoveOnRoom OnArrive = DefaultMoveOnArrive Vehicle = DefaultVehicle OnFinish = module.DefaultOnFinish OnCancel = module.DefaultOnCancel OnInitTags = DefaultOnInitTags OnStepTimeout = DefaultOnStepTimeout OnStepFinsih = DefaultMoveOnStepFinish MapperOptionCreator = DefaultMapperOptionCreator Option = new Option() ... } 非常明显,我将所有的方法的默认方法,汇总成了一个 Move类。这样只要替换对应的方法,就能实现不同的移动了。 整个 Move类相当于一个乐高积木,通过不同的积木进行拼装,然后我的Map类有一个StartMove方法 StartMove(move) { this.Move = move this.MovePosition.StartNewTerm() this.ChangeMode("") move.Walk(this) } 将拼装好的Move类传入,就能开始移动了 配置 在配置上,我采用了Go语言中十分流行的选项(Option)设计模式 也就是,通过一个选项列表对Move进行设置,每个选项代表一套预定义的函数包,应用完后就是我们需要的Move对象。 对于选项列表,我创建一个路书(Route)对象来保存。 这样,所有的移动,就是一套预设的路书,初始化为移动,交由Map对象执行。 我预设的移动选项包括 To 定点移动 Path 固定路线遍历 Ordered 顺序遍历房间 Rooms 房间遍历 SingleStep 单步移动 StartCommand 开始指令 Option 地图选项 Tag 移动标签 这些就能决定移动的 类型/形态 然后在消费的模块,加入目的型的选项 比如core/zone.js的 App.Zone.SearchRooms let move = wanted.Ordered ? App.Move.NewOrderedCommand(rooms, App.Zone.Finder) : App.Move.NewRoomsCommand(rooms, App.Zone.Finder) 就很简单的拼装出了一个便利搜索的移动。 通过 基础模块(生产端)的 移动形态/类型 选项,和任务(消费端),快速的组合出需要的移动模块。
  • 深入浅出制作全自动Mud机器人-惯性导航

    Script脚本 mud机器人 全自动
    1
    1 帖子
    47 浏览
    jarlyynJ
    所谓的惯性导航,其实就是在机器人进行移动时,能推断出自己目前的位置。这样在移动中和移动结束时不会因为丢失位置信息而需要重新定位。 实现惯性导航的方式我见过不少。有根据地图计算下一步的房间的,甚至有监听发送的指令来进行计算的。 从我的理解来看,惯性导航,和真实的导航APP一样,代表一种预期。就是我走到这一步后,我应该在什么位置。 这是在路径计算时,已经计算好的预期。 如果行进中发生了意外,比如路线错误/被随机移动/被挡路,则应该将条件设置好后重新计算路线。 就如同导航APP的"已经选择新路线,新路线快/慢XX分钟"那样。 所以,在路线规划时,不管是移动,还是遍历,不光光应该获取到指令列表,还要获取到每个指令对应的目标,这样才能更好的进行位置计算。 当然,基于预计算的惯性导航也容易出问题,典型的就是位置计算错误后,会卡在某地。 但个人认为,显性的Bug比隐性的Bug好,容易复现的Bug会更容易解决。
  • 1 帖子
    57 浏览
    jarlyynJ
    在开始制作一个Mud机器人的移动模块之前,首先我们先要解决移动确认,也就是判断成功进入一个房间的问题。 稍微延展点的话,就是解析当前房间的信息 确定房间信息开始 抓取房间名和其他信息 抓取房间描述 抓取房间出口 抓取房间内对象 确定房间信息结束 本文章以我newhelljs的解析房间代码为例 代码地址 房间信息开始 一般房间信息开始未必是一个很明确的信号。 部分mud可能对房间有特殊的格式。但很多Mud单纯就是一个顶格不带特殊标点的短剧。 所以我在处理房间时是分为两个部分 第一是普通房间,比如 ^[^,。!:.『』【】…“”??>.]{2,10}$ 然后这里我使用了一个Filter的概念,就是不直接抛事件,而是在进行了预处理后,符合条件时才抛事件。对应的代码是: 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) } }) 进行了简单的视觉判断,单色,非普通色,加粗。 除了这个通用标准外,有特殊的出发可以直接抛房间名事件。 在这里很明显,我是一个可能判断,并不能保证100%匹配。 所以我并没有确定出现疑似房间名的信息就进入房间。 出现疑似房间名我只是把已经抓取的房间描述清空,记录最后一个房间名,直到确定进入房间在把这些转化为当前房间信息。 过滤干扰项 我在代码里把所有季节相关的干扰项都记录在了"data/natures.txt"文件里,并进行了排除。 这是为了抓取数据做快照而作的准备。 如果你不需要抓取描述,可以不做这个处理。 出口信息 大部分Mud,出口信息都是比较统一,标准的,所以可以作为真正确认进入房间的信号 我的代码里,就是在抓取到出口信息后,确认之前抓起的房间名和描述有效,开始抓取房间对象列表。 如果你玩的Mud的代码比较复杂,这里可能有更多的处理。 对象列表 我这里主要是将对象可能的几个形态都列了出来,统一插入Room对象的 Object列表中。 遇到的第一个不符合条件的行,就确认房间结束。 这个主要还是看wiz怎么处理,有时候还会需要做特殊的处理。 App.BindEvent("core.onexit", App.Core.Room.OnExit) let matcherOnHeal = /^ (\S{2,8})正坐在地下(.+)。$/ let matcherOnYanlian = /^ (\S{2,8})正在演练招式。$/ let matcherOnObj = /^ ((\S+) )?(\S*[「\(].+[\)」])?(\S+)\(([^\(\)]+)\)( \[.+\])?(( <.+>)*)$/ //处理得到出口之后的信息(npc和道具列表)的计划 var PlanOnExit = new App.Plan(App.Positions.Connect, function (task) { task.AddTrigger(matcherOnObj, function (trigger, result, event) { let item = new objectModule.Object(result[4], result[5], App.History.CurrentOutput). WithParam("身份", result[2]). WithParam("外号", result[3]). WithParam("描述", result[6] || ""). WithParam("状态", result[7] || ""). WithParam("动作", "") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnHeal, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", result[2]) App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnYanlian, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", "演练招式") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddCatcher("line", function (catcher, event) { let output = event.Data.Output if (output.length > 2 && output.startsWith(" ") && output[2] != " ") { return true } //未匹配过的行代表npc和道具结束 return event.Context.Get("core.room.onobject") }) }, function (result) { if (result.Type != "cancel") { if (App.Map.Room.Name && !App.Map.Room.ID) { let idlist = App.Map.Data.RoomsByName[App.Map.Room.Name] if (idlist && idlist.length == 1) { App.Map.Room.ID = idlist[0] } } App.RaiseEvent(new App.Event("core.roomentry")) } }) 忽略信号 一般来说,会有两种情况需要忽略移动确认的信号。 第一种是MUD的干扰。Mud的Wiz可能能会设置一个类似进入房间的干扰选项。这个需要在代码里进行排除,具体实现要看Mud的设置。 第二种是主动的Look。 我在我的移动库做了特殊处理 EnterNewRoom(room) { if (!room) { room = new Room() } let oroom = this.Room this.Room = room if (oroom.Keep) { if (oroom.ID) { this.Room.ID = oroom.ID } this.Room.Keeping = true } this.Room.Keep = false return this.Room } OnWalking() { if (!this.Room.Keeping) { this.Position.StartNewTerm() } if (this.Move != null) { this.Move.OnWalking(this) } 在Room有Keep属性时,不做彻底的清理处理,并不会触发对应的移动动作。 封装事件 由于Room处理是一个很复杂的工作。 所以正常情况下,有一个专门的Room处理函数进行处理,处理完毕后抛出事件通知需要后续操作的模块。 这样才能最大的程度的解耦合,并保证房间处理信息的内聚。 不做切割的话,很容易随着房间信息格式的调整,把整个代码搅乱。
  • hellmapmanager.ts项目介绍

    已固定 HellMapManager地图编辑器
    1
    1 帖子
    99 浏览
    jarlyynJ
    hellmapamanager.ts是一款将HellMapManager中C#的数据维护/调用代码用Typescript重写的项目。 用于在不使用http接口的情况下,使用v8/luajit等高性能脚本引擎直接调用HellMapmanager中的相应算法。 项目支持 hmm格式文件的维护 编译到javascript/lua格式 项目地址为 https://github.com/hellclient-scripts/hellmapmanager.ts
  • HellMapManager软件介绍

    已固定 HellMapManager地图编辑器
    1
    1 帖子
    52 浏览
    jarlyynJ
    HellMapManager是一款使用c#语言开发,支持 Windows/Linux/MacOS的地图编辑/路径规划软件。 软件支持 hmm/hmz格式地图信息 地图信息维护 地图版本对比 补丁生成和选择性应用 通过http接口方式进行调用 无头模式 等功能 项目地址为 https://github.com/hellclient-scripts/hellmapmanager
  • 深入浅出制作全自动Mud机器人-移动系统

    Script脚本 mud机器人 全自动
    1
    1 帖子
    59 浏览
    jarlyynJ
    在mud系统中,地图系统是最复杂的一个系统(相对于任务系统,心跳/属性系统,战斗系统)。 而且一般的中文Mud系统有没有普遍的Busy机制,部分Mud的wiz对添加地图也没有克制力,使得地图整个地图系统往往有超过手动玩家能力的复杂性。 所以移动系统也是整个Mud机器人中最重要和复杂的系统。 在这里我们也会分很多的章节去描述怎么做好Mud机器人里的移动系统。 当然,我们先要明白我们需要一个怎么样的移动系统 伪动态规划的移动系统 由于Mud地图的特性,在每个出口上经常有各种条件和限制。 地图的任务和NPC也经常有区域性的限制,所以我们往往需要有一个伪动态规划的地图系统。 为此我特地写了HellMapManager和hellmapmanger.ts 两个项目来更好的实现这点。 关于伪动态规划的内容参见这个链接 兼容迷宫的移动系统 一般我们地图规划其实是基于 一串固定指令对应一次移动的。 但很多时候我们需要引入迷宫系统,就是需要判断,处理,多次移动来构成一次虚拟移动的情况。 我称之为迷宫系统。 怎么兼容迷宫系统,怎么把移动移动和普通移动统一起来,这也是一个重要的课题。 可重计算重试的移动系统。 对于Mud机器人来说,一次移动其实也是一次预期。 预期的路线,和实际能行舟的路线可能是有区别的。 所以我们的移动系统应该是可重试,对不可进入的房间能屏蔽,能动态调整移动参数后继续尝试的系统。
  • 深入浅出制作全自动Mud机器人-同步

    Script脚本 全自动 机器人 代码范例
    1
    1 帖子
    64 浏览
    jarlyynJ
    同步是和异步相对的概念,一般来说,编码里的同步和异步的区别是在于是否阻塞。 同步的代码会阻塞并等待任务的返回结果。 异步的代码一般只发起任务,不关心任务是否完成,可以通过回调/通道之类的方法获取处理结果。 在我们的机器人中,一般会用异步的方式抛出一系列的指令,然后需要一个同步的指令,阻塞任务队列,保证不会被其他的信息污染,来进行更详细的指令判断。 比如,我newhelljjs中有一个炼丹模块,代码如下 $.PushCommands( $.To("1387"), $.Do("give cao yao to xiao tong"), $.To("1389"), $.Do("i"), $.Sync(), $.Plan(PlanLiandan), $.Sync(), $.To("1388"), $.Ask("yao chun", "炼丹"), ... ) 就很明显,用了两个$.Sync的同步指令,确保在新指令执行时,之前的指令已经处理完毕。 比如第一个$.Sync(),就是确保i指令已经从Mud服务端获取到了完整的回复,更新了角色的当前物品清单信息。 选择合适的指令 首先,我们的代码会需要选择一个合适的同步指令。这个指令要满足以下的要求: 什么时候都能使用 返回固定,没有消耗 对服务器压力低 平时不会使用,避免误触发 那么,对于没有提供对应低消耗指令的mud,我们一般会找这样的指令: 1.已经废弃的指令,使用时只有废弃提示。 2.有特殊格式要求的,不带参数会报错的。 注意,一个正经的机器人我们还是要注意服务器消耗的。不光光是公德心的问题,毕竟写的太丑的机器人也丢自己脸是不? 另外,除了同步指令,一般建议再找一个判忙的指令,要求和同步的指令类似,区别是在忙和不忙得情况下,回复会有不同。 毕竟忙(busy)也是很多mud的重要状态之一。而且很多时候,判忙也能代替同步指令的作用(更复杂的同步)。 合理的使用 使用同步/忙指令主要有以下特殊点: 避免连续重复使用,容易导致信号失真,特别是有可能进入状态重置的情况。 2.在无法避免信号失真的情况下,要保证机器能在短暂的失效后能重回正轨,有一定的容错性。 3.对于复杂场景,要有个统一的失效机制。也就是不要在等到同步信号之前就开始执行指令,在使用了同步机制后,要统一的在同步完成后再开始小一步。 举个例子,由于我的机器采用的是预期管理的模型驱动的,我特地建立一个Response的作用范围,然后效果如下: let PlanTimes = new App.Plan( App.Positions["Response"], (task) => { task.AddTrigger(matcherTimes, (tri, result) => { if (result[1] == San.Data.WeaponName) { task.Data = result[2] } }) App.Send(`l ${San.Data.Weapon}`) App.Sync() }, (result) => { if (result.Task.Data) { San.Data.Times = App.CNumber.ParseNumber(result.Task.Data) App.HUD.Update() } App.Next() } ) 利用我的期望失效的机制,强制在App.Sync()后,接收到同步信号时,通过Task.Data来判断结果和下一步的动作,避免会在同步信号前再发送一个同步指令,造成混乱。 避免滥用同步指令 同步指令一般只用在发送多个指令后进入不可预期的状态时的确认。 很多有明确预期的指令,比如移动,遍历,不应该以来同步指令,避免对服务器的不必要的压力和效率降低。 最典型的就是判断当前房间移动结束,或者房间的物品列表。 尽量使用 格式的变化(比如第一个出现的非房间物品)来判断。 比如 App.BindEvent("core.onexit", App.Core.Room.OnExit) let matcherOnHeal = /^ (\S{2,8})正坐在地下(.+)。$/ let matcherOnYanlian = /^ (\S{2,8})正在演练招式。$/ let matcherOnObj = /^ ((\S+) )?(\S*[「\(].+[\)」])?(\S+)\(([^\(\)]+)\)( \[.+\])?(( <.+>)*)$/ //处理得到出口之后的信息(npc和道具列表)的计划 var PlanOnExit = new App.Plan(App.Positions.Connect, function (task) { task.AddTrigger(matcherOnObj, function (trigger, result, event) { let item = new objectModule.Object(result[4], result[5], App.History.CurrentOutput). WithParam("身份", result[2]). WithParam("外号", result[3]). WithParam("描述", result[6] || ""). WithParam("状态", result[7] || ""). WithParam("动作", "") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnHeal, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", result[2]) App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnYanlian, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", "演练招式") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddCatcher("line", function (catcher, event) { let output = event.Data.Output if (output.length > 4 && output.startsWith(" ") && output[4] != " ") { return true } //未匹配过的行代表npc和道具结束 return event.Context.Get("core.room.onobject") }) }, function (result) { if (result.Type != "cancel") { if (App.Map.Room.Name && !App.Map.Room.ID) { let idlist = App.Map.Data.RoomsByName[App.Map.Room.Name] if (idlist && idlist.length == 1) { App.Map.Room.ID = idlist[0] } } App.RaiseEvent(new App.Event("core.roomentry")) } }) 是通过出现的行不是以4个空格开头(固定格式)来判断的。
  • 深入浅出制作全自动Mud机器人-战斗系统

    Script脚本 全自动 机器人
    1
    1 帖子
    49 浏览
    jarlyynJ
    相对而言,我玩过的Mud中,战斗系统都是最简单的。 一般就是一个Timer搞定,复杂点就是带策略的Timer。 这倒不是Mud不能做复杂的战斗系统,反而是体现形式的制约。 毕竟Mud是纯文字展示的,一战斗就是刷刷的刷屏,想要复杂点就直接放弃手动玩的可能了。 所以,对于我而言,一般战斗都是timer实现,然后加上一些常用的变量判断。比如: Duration 持续时间 CType 战斗类型 Tag 一些标签,比如是否是偷袭之类 Life/Neili 当前属性 CQuest 当前任务 由于战斗的特殊性,我还引入了Block,就是一个配置块对应一个战斗类型。 举一个简单的配置为例 #before yun recover;yun regenerate;#wpon #start perform finger.chao and strike.qimen yun recover perform finger.chao and strike.qimen #block mq ctype mq>#apply #before yun recover;yun regenerate;#wpon #start perform finger.ding twice #start perform finger.chao and strike.qimen yun recover perform finger.ding twice perform finger.chao and strike.qimen #block 巫妖 ctype xuemo,ctag sklich>#apply #before yun recover;yun regenerate;#wpon #start perform finger.ding skeleton lich twice yun recover #start perform finger.ding skeleton lich twice perform finger.ding skeleton lich twice perform finger.chao and finger.ding skeleton lich perform finger.chao and strike.qimen #block 丁一 ctype xuemo,ctag boss>#apply #before yun recover;yun regenerate;#wpon; #start perform finger.ding ding yi twice yun recover perform finger.chao and finger.ding ding yi perform finger.chao and strike.qimen #block qinling ctype qinling>#apply #start perform finger.ding qin shihuang twice;perform finger.chao and finger.ding qin shihuang 对应的解释: 普通战斗,一个chao+qimen解决 战斗类别为mq(也就是师门任务),使用mq block 具体就是先buy一下,然后chao+qimen,相对安全点 战斗类型为xuemo,战斗标签为sklick的,使用 巫妖 block 优先busy,优先攻击skeleton lich 战斗类型为xuemo,战斗标签为boss的,使用 丁一 block 优先攻击丁一 战斗类型为qinling的 busy+输出秦始皇 然后只有攻击的一次pfm 总体来说,由于Mud表现形式的缺陷,很多Mud的战斗系统之需要一个基于Timer的复杂配置就能完成了。
  • 1 帖子
    73 浏览
    jarlyynJ
    任务编排和用户队列其实是同一生态位,类似的上层建筑。 任务编排和用户队列的区别是 任务编排是宏观战略层面的,用户队列是微观战术层面的。 任务编排是面向多行文字变量的,用户队列是面向单行用户命令行输出的。 任务编排属于长期任务有与UI的交互,用户队列是临时任务基本与UI无交互。 任务定义 任务Quest,其实是一个很轻的元素。 class Quest { constructor(id) { this.ID = id } InCooldown() { return (new Date()).getTime() < this.CooldownTo } Cooldown(interval) { this.CooldownTo = (new Date()).getTime() + (interval ? interval : 0) } CooldownTo = 0 ID = "" Name = "" Desc = "" Intro = "" Help = "" Group = "" Start = null GetReady = DefaultGetReady OnHUD = DefaultOnHUD OnSummary = DefaultOnSummary OnReport = DefaultOnReport } 可以看到,除了Cooldown相关的,以及GetReady和Start两个入口,全都是和界面交互的内容。 任务编排 在理解了任务是什么时候,我们接着看任务编排。 任务编排是通过条件指令格式,在任务变量中(newhelljs为了方便使用定义了多个任务变量)设置的规划。 任务会从上向下,依次判断 如果任务不符合条件指令的条件,则跳过 如果任务在冷却中,则跳过 如果所有任务都在冷却,等1秒重头循环 典型的任务编排,可能是把收益最高的,冷却最常的任务编排在最前。 比如 lgt qinling mq 就是依次执行 灵感塔(一天一次) 秦岭(2分钟一次) 师门任务(无冷却) 当然,有时候也会用条件变量分阶段执行任务 比如新人任务 maxexp 2000>>tiejiang maxexp 10000>>peiyao !yueli 20>>beiqi maxexp 29999>>letter !yueli 2000>>beiqi maxexp 100000>>fish quit 这个编排,是依次执行 经验不到2000做铁匠任务 经验不到10000做配药任务 (经验超过10000时)阅历不足20就做背齐任务 经验不到30000(阅历超过20)就做送信任务 阅历不足2000(经验超过30000)时继续做备齐 经验不到100000(阅历超过2000)时,钓鱼 (阅历超过2000,经验超过100000)都做完了,下线喽 任务冷却 任务的核心其实就是冷却,就是有冷却的函数。理论上,只靠冷却就能实现主要的任务编排工作。 在任务实现时,可以在接任务或者完成任务时,调用Quest.Cooldown(xxxx)来为任务设置冷却。 如果有任务联动,可以清楚别的任务的冷却时,也可以从全局注册函数Quests中获取对应变量,进行Cooldown(0)清零。 任务预分配 这是我在实际使用时,引入的新概念。 具体来说,就是由于任务是基于编排模式运行的。 所以,当前任务执行完后,下一个任务是否还是当前任务是不可知的。 这时候我们可以预分配一下下一个任务,调用下一个任务的GetReady,如果下一个任务 Ready了,返回了入口函数,同时ID和当前任务不一样,我们就可以执行当前任务的清理工作。 典型的就是师门任务如果要换任务了,就不再等信接下一个师门了 详见mq任务的mq.CanAccept方法 let ready = App.Quests.GetReady() if (ready && ready.RunningQuest && ready.RunningQuest.ID != Quest.ID) { return false } 任务编排带来的新问题 任务编排,本身就是为了自由度,自由扩展,解决 怎么才能完成更复杂的任务,获得更好的回报? 这个问题 同样的,也会引来新的复杂度 就是既然使用任务编排时我们一定会倾向于细化多分任务 那其他的相关设置怎么办?不如不同任务中不同的战斗设置? 所以,我们在配置时,不得不对所有相关的设置都引入条件指令格式。 增加了配置的复杂度。 鱼和熊掌不可兼得,莫过于此。
  • 1 帖子
    54 浏览
    jarlyynJ
    简而言之,用户队列,就是指令队列在用户输入框里的实现。 核心部分,之前都是纯代码层与用户无关的基础建筑,到了这里才算得上是上层建筑。 先看代码 代码地址 我们会发现,用户队列,Userqueue,是在Command包下的一个很简单的实现。 用队列相对于指令队列,就是额外的实现了一下的内容 Exect方法,把用户的输入分割为多个指令执行 Do和Wait的默认方法,实现最常见的发送指令和等待时间的功能(其实是Command中相应功能的封装) Register指令,注册新的指令扩展功能 Loop指令,循环执行 很明显,用户队列提供了一个让用户直接调用Command的接口,更重要的是,可以循环执行。 当我们把准备阶段也注册为指令时,可以通过#prepare和#loop循环,形成一个简陋板的任务,一个字面意义上的全自动机器人。 比如,打坐机器人 #prepare|yun recover|dazuo 50|#loop 比如,练功 #prepare|yun recover|wield long sword|lian sword 50|#loop 虽然这种用户队列只能实现简单的,没有复杂任务的机器,但这是真正意义上的全自动机器人了。 甚至可以认为,用户队列的稳定性,等以你机器的主体的稳定性。 因此,用户队列对于主任务系统是一个独立的,迷你的系统。 但对于用户手动操作,以及机器的测试开发来说,还是有很大的意义的。
  • 1 帖子
    43 浏览
    jarlyynJ
    机器人的代码组织有很多种方式。 我自己的机器人其实经历过3个阶段 helllua ,使用的是回调链 pkuxkx.noob ,试图尝试使用状态机,后来切换到指令队列 newhelljs 精简的指令队列 这里就说下我目前使用的指令队列结构。 老规矩,线上代码 代码地址 什么是指令队列 其实指令队列在Rts游戏以及衍生出的Moba游戏里是很常见的。 就是在war3,starcraft或者dota2里,按住shift,依次发布多个指令,这些指令就会进入队列,一个一个执行完。 这很策略,巧的是,写Mud全自动机器人也是个策略活。很自然的,我就引入了指令这个概念。 指令的实现 指令其实很简单 class Command { constructor(name, data) { this.Name = name this.Data = data if (module.Debug) { this.Stack = (new Error()).stack } } Context = {} Name = "" Stack = "" Data = null OnEvent = null } 主体就是Name和Data,以及后续会被队列引入的上下文Context。 那么执行的代码呢? 执行的代码是分离开的概念,放在指令执行器里. 执行其的作用是传入队列和 指令,然后初始化对应指令功能的模块 比如 module.ExecutorFunction = function (commands, running) { running.OnStart = function (arg) { running.Command.Data() } } 为什么把指令和执行其分开呢? 因为这样指令队列就是纯数据不包含代码,方便Debug和进行快照/恢复 基础队列 很明显,我们的队列,就是一个Command数组 我们可以对数组进行Insert和Append操作。 可以通过Next操作弹出第一个指令执行 然后,我们还有一个压入指令组的功能(PushCommands) 就是冻结当前队列,执行新队列,可以Pop中断继续执行前一个队列,同时切换队列的上下文(Context, 不同指令之间共享的运行时数据) 这个结构下,队列Quesus,本质是一个 Command的二位数组 控制结构 当我们有了多层的数组后,我们就可以实现简单的控制结构了。 我定义了以下控制接口 Push/PushCommands 压入Queue Pop 放弃当前Queue剩余内容,返回之前的调用队列 Snap/Rollback 做快照/回滚快照。由于 Command是纯数据结构,就是克隆一份Queue出来 Append/Insert 在当前队列的最前端/后端插入指令 Fail 报错失败,会废弃所以没有定义FailCommand的队列,直到队列全空。FailCommand相当于一般语言的try..catch FinalCommand 就算Pop中断也会执行的指令 意外应对机制 本质来说,指令队列是预先定义蓝图,在顺利的情况下完全实行。 但实际上,往往会有意外发生,需要跳过当前指令和指令队列强行执行其他的。 这时候,从逻辑上我们必须提供一个快照和回滚的机制,确保意外处理后有可能继续执行当前工作的方法。 这就是我对指令队列打的补丁。 相当于 Mud机器中不止有一个主逻辑,只是平时其他的逻辑都是蛰伏状态。当意外发生时,副逻辑介入,先保留住逻辑现场,处理完之后,再根据实际情况决定是否要恢复主逻辑运行(实际很少这样处理) 异步与同步 实际上Command本质是一个轻量级的Promise。是一个对未来的期望,是异步(在将来)执行的代码。 所以,很多时候,我们需要和实际指令进行同步,就是等待Mud服务器返回一个特殊的返回,确保之前的指令都已经执行完毕了。 我一般会用nobusy和sync 的Command来实现这个功能 典型代码 以我的炼药采买为例 Lianyao.BuyAll = (cmds) => { $.PushCommands( $.To("65"), $.Do(cmds.join("\n")), $.Do("i"), $.Wait(1000), $.Sync(), $.Function(Lianyao.Make) ) $.Next() } 这个能很明显的看出队列的用法 规划一堆指令,Sync去订都执行完毕了,然后继续后续。 最后$.Next释放控制权 指令队列的挑战 指令队列或者其他纯代码逻辑控制的驱动方式,最大的挑战其实是内存泄漏。 比如如果要循环执行指令的话,不停PushCommands会堆很多很多层空队列,直到爆栈为止。 这是必须通过在最外层Insert指令,而不是PushCommands,才能避免内存泄漏
  • 1 帖子
    45 浏览
    jarlyynJ
    条件指令解决的是复杂Mud配置的问题。 Mud全自动机器人本质是一个典型的后台服务,和后台服务器一样,配置基本是多个纯文(变量)本来进行的。 同时,由于一般的Mud客户端的变量设置都没有语法支持,基本就是纯纯文本。所以会需要一个易写易读的配置格式。 这种情况下,我引入了条件指令的格式。 代码连接 具体格式为 条件1 条件参数1,!条件2>#指令.参数 数据 逗号分隔多个条件,前置感叹号代表取反,必须所有条件都符合。 指令参数数据看具体的实现,先不深入。 具体使用什么条件,就是从上往下依次读,看条件匹配,匹配了,就执行,不继续执行(特殊配置可能继续匹配)。 不匹配看下一条。 这样人脑比较好解读。 同时,为了避免过于复杂的场合难以解读,还引入了可选的分组的概念 分组1:条件1 条件参数1,!条件2>#指令.参数 数据 这样能直观的进行组别判断。 以我最复杂的战斗设置为例 大概是这么个画风 #before #wpon;yun recover; #start yong cuff.jingang;perform unarmed.chang1;perform finger.chao and strike.qimen yun recover perform finger.chao and strike.qimen #block 秦岭 ctype qinling>#apply #before yun recover;$wpon;summon xiao;unwield xiao #start yong cuff.jingang;wield xiao;perform sword.feilong qin and finger.ding qin;get xiao;unwield xiao wield xiao;perform sword.feilong qin and finger.ding qin;get xiao;unwield xiao 注意,这里还引入了block区块的概念 条件本身是有一个全局注册的 大概代码为 //注册maxexp 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("maxexp", function (data, target) { return App.Data.Player.HP["经验"] <= (data - 0) })) //注册yueli 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("yueli", function (data, target) { return App.Data.Player.Score["阅历"] >= (data - 0) })) //注册pot 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("pot", function (data, target) { return App.Data.Player.HP["潜能"] >= (data - 0) })) //注册quest 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("quest", function (data, target) { let rq = App.Quests.Running return rq && rq.ID == data }))
  • 1 帖子
    58 浏览
    jarlyynJ
    Mud机器人有一种很常见的开发方式,就是 我预期 未来服务器可能会返回N种回复,根据不同的回复执行不同的代码。 以mush为例,最基本的就是设置一组触发器,预期进行时,开启触发组,有任何一个触发命中时,关闭本组,继续执行。 这种模式下主要的课题是失效管理,也就是什么场景下,会结束预期。 我在newhelljs中引入了 一个叫做Task/Plan的概念,对这种操作进行优化 代码地址 具体来说,我定义的预期是这样的。 在某种状态下,一定时间内,预期有N种触发或者事件可能会发生。 触发后默认预期会结束,除非显示的保活。 首先,我定义了一个预期的有效范围。 根据我的恶趣味,我起了个很蛋疼的名字叫Committee(委员会) 委员会里有很多个职位(Position) 每个职位会有Term任期。 所有的预期(task/plan)是挂在任期Term上的。 任期是一个抽象概念,比如连接,房间,任务,战斗。 当有新的连接/进入新房间/开始新人物/进入或离开战斗后,任期就会自动结束。 这样保证了预期不会泄漏长期存在。 比如我在我战斗模块的开始和结束战斗的代码 Start(id, data) { this.Position.StartNewTerm() this.Data = data this.Target = id ? id : "" this.StartAt = (new Date()).getTime() this.Position.AddTimer(this.Interval, () => { this.Ticker(this) }) this.Ticker(this) this.Plan.Execute() this.Combating = true return this } Stop(reason) { if (!this.Combating) { return } let onstop = this.OnStop this.Position.StartNewTerm() onstop(this, reason) this.Target = "" this.Combating = false this.Data = null } 会使用this.Position.StartNewTerm()来更新任期(所有相关的触发/计时器失效) 当计时器失效时,会Task的结算函数,把Task结束的原因TaskResult传入,这样就能根据不同的结果进行判断了。 至于Plan,其实就是一个Task的工厂函数,调用Plan就能常见一个新的Task。 以代码为例 let matcherEnter = /^你连线进入.+。$/ let matcherReenter = /重新连线完毕。$/ let matcherTooFast = /你距上一次退出时间只有.+秒钟,请稍候再登录。$/ let matcherTooFast2 = /你不能在.+秒钟之内连续重新连线。$/ //登录的计划 var PlanOnConnected = new App.Plan(App.Positions.Connect, function (task) { task.AddTrigger(matcherEnter).WithName("enter") task.AddTrigger(matcherReenter).WithName("reenter") task.AddTrigger(matcherTooFast).WithName("toofast") task.AddTrigger(matcherTooFast2).WithName("toofast2") task.AddTimer(5000).WithName("timeout") }, function (result) { switch (result.Type) { case "trigger": switch (result.Name) { case "enter": App.RaiseEvent(new App.Event("core.entermud", false).WithType("system")) App.RaiseEvent(new App.Event("core.relogin", false).WithType("system")) break case "reenter": App.RaiseEvent(new App.Event("core.entermud", true).WithType("system")) App.RaiseEvent(new App.Event("core.reconnect", false).WithType("system")) break case "toofast": case "toofast2": if (App.Core.Connect.Callback) { App.Core.Connect.Next = (new Date()).getTime() + 10000 Note("10秒后重试") } break } break case "timer": if (App.Core.Connect.Callback) { App.Core.Connect.Next = (new Date()).getTime() + 10000 Note("10秒后重试") } break } }) 就是一个很明显的,高内聚的,预期的组合 当前连接有效 有个不同Name的文字出发 有5秒的timeout 结束函数里通过switch语句,对不同类型的结果进行处理,抛出时间或者进行重连。 如果在这当中意外断线,由于有Connect的Position,会直接失效,不会在下次连接时继续触发。
  • 深入浅出制作全自动Mud机器人-事件总线

    Script脚本 机器人 全自动 架构
    1
    1 帖子
    48 浏览
    jarlyynJ
    事件系统是一种很常见的代码结构。从web开发,GUI软件制作到各种系统,事件系统都是重要的组成部分。 事件总线是指将所有的事件的抛出与监听都挂在一个独立的类(EventBus或者EventBridge)上。 事件系统是天然的解耦合工具,底层代码抛出事件,处理代码接受事件,让人极容易的写出互相之间不依赖的代码。 写机器,自己嵌入一套事件系统是很顺理成章的事情,甚至某些客户端会在客户端内部实现一个全局的事件系统。 但是,就如同《人月神话》中提到过“没有银弹”。极度的自由,很容易带来极度的混乱。滥用事件很容易带来代码组织架构上的混乱,虽然解耦合了,但依然维护苦难。 毕竟我们强调解耦合是为了优化结构,降低维护难度。低耦合还有下半句高内聚。事件系统虽然实现了低耦合,但在内聚上做的一塌糊涂,必须极为注意使用。 事件的处理要有流向性 处理事件最怕的是循环调用,处理程序互相之间调用,织成一张网,如同乱成一团的线头一样,让人无法入手。 从我的角度,我一直推崇代码的架构要分层,比如我使用的一种架构 代码之间,要有严格的上下级关系,调用原则,避免相互依赖,依靠各种依赖注入/装饰类/代理类来实现反向的交互。 这种情况下,我们要有自我约束,事件的监听函数的调用过程,本质应该是一个水向下流,或者水蒸汽向上飘的单向过程。 事件的生产方和事件的消费方要有严格的上下关系。 这样事件的使用就会有头,有尾,能够梳理。 事件应该是业务的封装而非调用的信号 不同系统使用事件要解决的问题是不一样的。 对于Mud来说,事件应该是对实际发生的业务的抽象与封装 比如新的文字行,比如断开连接和建立连接,比如用户的点击操作等。 事件的生产方本质是把各种现实意义发生的元素,抽象成代码,封装成同一的格式,交给流水线。 不应该觉得为了调用一个处理函数,而抛一个事件。抛事件时一定要明确,很可能没有代码接受事件,或者有乱序的多个代码会接受事件。 如果需要明确的调用代码,不应该使用消息,应该使用显性的注册(依赖注入)机制,调用注册点中注册的代码,做明确的显性的调用。 事件处理函数不要抛出事件。 完整的说,事件处理函数,不要在同一个总线(bus,事件的监听系统)上抛出一个不同组的事件。 因为,每个事件,都会有0个或者多个处理函数,在这些处理函数上再抛出事件,会无法确定是否有循环调用,顺序问题以及依赖问题。 我认为唯一适合事件再抛出事件的,是对事件细化扩展为同组的子事件。 比如登陆成功事件,可以细化为 首次登陆 和 重新登陆事件 这就是同组的事件,(任意登陆/首次登陆/重新登陆) 监听程序会明确的监听其中一个,而不会每个都监听。 其他情况我不建议事件再触发时间,混乱度会字数成长。 总结 事件总线系统,是Mud机器人中必不可少的。 事件总线系统,极为强大。 事件总线系统,过于强大,容易混乱,必须有原则有策略的去规范使用。
  • 深入浅出制作全自动Mud机器人-准备阶段

    Script脚本 全自动 机器人
    1
    1 帖子
    43 浏览
    jarlyynJ
    我觉得,全自动机器人本质是对人在Mud中行为的归纳,也就是常说的抽象。 抽象出的行为模式可能有很多种。 对于我而言,比较擅长的是 信息采集->准备阶段(状态重置)->任务执行 这样一个模型。 所以,继状态采集后,最重要的就是准备阶段。 什么是准备阶段? 准备阶段,粗泛点来说,就是通过不同的准备步骤将角色维护到一个Ready的状态。 通俗来说,其实就是一次角色状态的慢重启。 对,就是电脑出问题了先重启那个意思。 机器人的复杂性很大一部分来自于情况的复杂性。 所以,如果我们能在任务执行前,先进入一个统一的状态,类似修电脑前先重启一下,就能更容易的用更简单的代码来进行维护。 因为只要考虑一种初始状况就可以了。 怎么进行准备阶段。 对于一般机器的准备阶段,我在主逻辑部分也说了,就是一个if队列 对角色的某个属性定一个标准,检查是否符合这个标准 如果不符合这个标准,就执行相应的代码,然后重头开始进行检查。 如果符合这个标准,则当前标准通过,检查下一个标准 如果所有的标准都符合了,准备结束,状态已经重置,进入执行阶段。 所以我们提炼(抽象)一下,就是 我们有几个模块,有先说顺序。 模块有一个Check函数。 模块有一个 执行函数,Check没过就去执行。 对所有的模块可能会有个统一的环境(上下文),作为检查的一些具体细节。 我在newhelljs里就是设置了一个Proposal(提案)类,核心就是有一个Submit(提交)方法 https://github.com/hellclient-scripts/newhelljs/blob/main/script/helllibjs/proposals/proposals.js 所有的准备就是提交一个个的提案,如果提案通过,则返回要执行的方法,去执行。提案每没过,返回一个空,继续执行下一个提案 怎么组织准备阶段的步骤。 我在newhelljs里准备了一个全局注册的容器Proposals,把每个Proposal按id注册进行。这样在执行准备时可以通过一个字符串id数组就制定要进行的提案了。 同时准备了一个提案组的类型,把多个提案绑成一个新提案,比如common,commonwithstudy这样调用和维护都轻松的多。 怎么验证准备的效果 整个全自动Mud稳定的基础其实就是准备阶段。准备阶段大部分情况下是整个机器最复杂的部分,机器架构的很重要的一部分就是把复杂性统一放在准备阶段,这样任务部分就很轻很简单了。 同样,准备是一个很好的测试机器人稳定性的工具。 我们可以做一个空循环 信息采集->准备阶段->等一秒 空跑一段时间,就能验证整个机器人底层框架的稳定性,健壮性,以及是否有内存泄漏之类的问题了。
  • HellclientUI 2026春季版发布

    HellclientUI应用
    1
    1 帖子
    106 浏览
    jarlyynJ
    更新内容: 修正连接按钮状态bug 部分UI调整 iOS及MacOS版请在AppStore更新 发布地址
  • 深入浅出制作全自动Mud机器人-主逻辑

    Script脚本 全自动 机器人 架构
    1
    1 帖子
    47 浏览
    jarlyynJ
    对于一个机器人来说,并不需要一个主逻辑,完全可以多套逻辑并行作用。 但对于写机器人的你我来说,大脑还是更适合单线程的模式。所以在机器的合适的地方维护一套主逻辑,即降低开发的难度,又增加维护的效率,个人认为是性价比很高的一种方式。 主逻辑的入口 正常情况下,主逻辑是在正常运行时不调用的调用的。所以,在写主逻辑时,从什么地方进入,调用主逻辑,是我们首先要考虑的点。 定时器入口 其实,通过定时器/心跳来调用主逻辑,在我看来是正道。市面上的各种机器人,自动驾驶,本质也是在每个tick计算当前的状态和应该进行的操作,从实时性,反应,效率来看,基于定时器的主逻辑都是最强的。 但是,万恶的但是来了,对于机器来说,完全基于tick来维护太难了。这需要能很细的拆解人物,分配到每个tick,然后还要为tick建立上下文,能保证持续性动作的正常实行。 这个难度太大了,很强,但性价比极地。 所以我完全不建议机器人的主逻辑以tick的方式来实现。 当然,比较简单又要求实时性的子系统可以基于tick处理,典型的就是战斗子系统。 触发入口 做一个特殊的触发,一般是利用Mud的回显机制,然后再执行玩任务是时候,想法让mud调用这个触发。 怎么说的,从一般客户端的设计,很容易的就会形成一套以出发为入口的代码,而且在初期很行之有效。 但是,如果要做一个足够复杂的机器,触发作为入口就会很勉强了,还要面对服务器本身的限制。 回调入口 回调作为入口,就纯靠代码来控制出发主循环。优点是不依赖mud,而且性能和可控性更高。 缺点就是需要有一个完整的统一的架构,将回调和回调后执行的代码组织起来,所有的代码也不能简单的通过出发执行,需要进行一定的抽象。 我的newhelljs就是利用回调入口的机制,执行完当前代码片段后,主动调用App.Next进行控制权的释放,由代码开始调用主逻辑。 主逻辑的组织 在决定主逻辑的调用方式后,我们就要决定主逻辑的代码怎么组织了。常见的可能有以下方法 触发多米洛骨牌 通过预先设置好的一系列触发,通过开关触发的形式,一步一步依次执行。 这种组织方式,配合合适的工具,在任务的小环节上极强的,我在newhelljs里也 做一个plan/task系统来更好的保证骨牌的稳定性。 但在宏观层面,整个机器人架构的层面,这种操作太细了,无法负担起过于复杂的机器架构 判断if队列。 就是收集可靠信息,然后通过一长串if(){}eleseif{} 的形式,判断当前应该做什么。 进一步的,可以采用责任链的实际模式,为不同的判断和对应的处理代码,封装为一个个的类,然后通过id进行全局注册。 这样可以通过一个id的数组,比如["hp","money","heal"]这样的形式,快速的定义和组合不同的工作流程了。 if队列在简单任务里效果极好。但复杂任务的话,复杂度就有点超过if队列的承受范围了。 在newhellljs里,我用责任链去实现了准备模块,效果非常好。 状态机模式 状态机模式是一个很有名的模式,但是在mud机器人里并不好用。 因为Mud机器人里的状态太多了,互相之间的转化太多,用状态机来处理mud,需要维护的状态会指数上升,是一个天文数字。 导致实际使用时都会进入一个中间态,而不是直接转换,很不适用。 我在pkuxkx.noob中尝试引入状态机,结果比较失败。 当然,状态机也有其优势,主要是可配置性强,可以显示的制定进入状态和离开状态时执行的代码。同时也的确客观存在状态这个存在。 所以我在newhelljs里还是小范围的引入了状态机。我给不同的任务设置了不同的stance,在进入不同组的stance会出发stance-leave和stance-enter,这样用户能很方便的在不同类型的任务(比如战斗/非战斗)任务之间执行一定的指令,这也是一个很明显的简化状态机模型。 指令队列缓冲模式。 指令队列缓冲模式指维护一个缓冲池,在空闲时依次弹出最前面的指令执行,然后缓冲池可以进行清除,插入,追加,压入新队列,快照/还原等操作。 这种模式,弹性很大,而且比较接近于人的思维模式,分配一系列任务,依次执行,遇到意外或者判断动态的切换之后的工作。 缺点一样,更复杂,而且更重要的是需要有完善的异常介入模式,对整个机器的完成度要求更高。 newhelljs中使用了Commands模块,把大部分功能都注册为Command,通过App.Next弹出新指令的方式来运行代码。 我的建议 我的建议是,主逻辑的入口,尽量用程序回调这种依赖最少的方式调用。 具体的组织,根据代码的规模 用责任链(固定流程)或者指令队列缓冲(动态维护流程)+意外处理的方式组织。
  • HellMapManagerGUI界面数据过滤介绍

    HellMapManager地图编辑器
    1
    1 帖子
    85 浏览
    jarlyynJ
    HellMapManager的数据列表视图都会提供筛选功能。 最基本的功能就是输入一个关键字,比如一个Key,那么,所有主要属性包含这个Key的对象都会被过滤出来。 但有时候,我们希望有精确匹配,这时候我们会需要掌握一些特殊的语法 多关键字 用逗号分隔多个关键字,可以做多种筛选 比如搜索 扬州,客房 能过滤搜索所有信息中有扬州和客房的信息 注意,分割后的关键字,前置和后置的空格都会忽略 精准匹配 比如我们要搜索所有和abc匹配,但不包含abcd,0abc等信息的,可以用前置的等号进行匹配 =abc 这样,只会精准的把和abc有关的匹配出来 属性匹配 有一个常见需求,就是过滤房间key为abc的 key=abc 或者有出口到abc的 to=abc 可以在等号前加入类型。 目前支持的类型为 key 主键 name 名字 group 分组 type 类型 desc 描述 message 信息 to 目标,房间列表中包含 command 出口指令 tag 标签或者环境条件/房间条件 misc 杂项(目前是房间数据的Key和Value) 取反 关键字最前方加入英文感叹号!,就能取反,即过滤不符合条件的 !key=abc 特别的,tag和misc很特别,只要有任何一个tag/key不符合即可,基本不会使用取反。 转义 为了输入特殊字符,过滤字段支持转义 转义前 转义后 \\ \ \空格 空格 \, , \= = \! ! \n 换行
  • 通过飞书实现HellclientUI手机端通知

    HellclientUI应用 使用技巧
    1
    1 帖子
    73 浏览
    jarlyynJ
    Hellclient本质是一款自建服务端运行的Mud脚本容器服务。 所以在发生意外需要手动处理时,会有发送一个通知给到手机控制APP(HellclientUI),让用户及时介入处理。 对于自建服务来说,自己建一个APP通知服务要求过高,对APP也有保活的需求,所以我建议使用现成的APP的通知来对接HellclientUI的手机端。 主流的通知技术叫做webhook,就是在现成APP中申请账号,建立通知的频道(群聊),获取一个通知地址,就能利用这个通知地址发送通知。 下面以我使用的飞书为例,介绍开开通webhook的流程 申请飞书账号 飞书是字节集团下的协同工具,官网地址为 https://www.feishu.cn/ 我们通过 定价按钮,能查看到免费服务的使用范围包括 即时消息 云文档 视频会议 多维表格 免费邮箱 我们点击上面的 立即免费体验 按钮,就能创建免费账号了 创建群组 飞书的通知是通过群组实现的。所以我们需要在客户端里建立一个新的 通知 群组 具体参考飞书文档 添加机器人 不用担心,机器人不是Mud机器人那种需要写代码的程序,只是在群组里发送通知的一个角色。 创建了机器人后,我们会获得一个webhook的网址。这时候我们的mud脚本向这个webhook发送信息,机器人就会直接把我们的消息在飞书的群组里发送出来了。这时候如果我们设置了正确的(和hellclientuiAPP中的服务器地址一致)的地址,就能拉起HellclientUI APP,打开对应的服务器,切换到正确的游戏,直接进行用户操作了。 添加机器人参考飞书文档 脚本中调用通知 具体代码很简单可以参考我 newhelljs中的对应部分 需要注意的是,Hellclient本身有基本的安全防护,脚本调用 webhook及访问飞书服务器需要用户手动授权才可以正常运行。
  • Hellclient 2026春季版发布

    Hellclient软件
    1
    1 帖子
    132 浏览
    jarlyynJ
    版本号 1.2026.01.13 主要内容为 修复v8 内存泄漏问题 修复v8的HTTP接口内存泄漏问题 部分UI微调 下载链接:https://github.com/jarlyyn/hellclient/releases/tag/2026.01.13