跳转至内容
  • HellMapManager 2026.04.16版发布

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

    Hellclient软件
    1
    1 帖子
    11 浏览
    jarlyynJ
    更新内容为加入脚本shared数据功能和相关api shared为使用同一个脚本的游戏可以共享读取的数据。 相对于现有的数据: 脚本数据 只读,共享。 Mod数据 只读,共享,需要额外开启 用户数据 读写,独立。 共享数据 读写,共享。
  • [开发]hellmanager.ts lua版本放弃性能优化声明

    HellMapManager地图编辑器
    2
    2 帖子
    48 浏览
    jarlyynJ
    通过测试,将HMM编译为动态链接库,然后封装成lua库供lua调用,大概能在lua(非git)跑出45 ms的速度,也算可用了。
  • Hellclient UI 2026清明版发布

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

    HellMapManager地图编辑器
    1
    1 帖子
    15 浏览
    jarlyynJ
    API版本更新到 1006 HellMapManger 变量页面显示优化
  • 1 帖子
    63 浏览
    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 帖子
    47 浏览
    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 帖子
    36 浏览
    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 帖子
    35 浏览
    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 帖子
    29 浏览
    jarlyynJ
    mud中,迷宫是一个很重要的课题。 其实这里说的迷宫,并不仅仅指迷宫这个解密形态,指的是非标准移动,动态出口。 有些需要特殊物品/ask npc才能通过的出口,其实也是迷宫的一种。 在我的第三版移动代码里 代码地址 Maze实现的依然有点丑。如果我还有第四版的移动代码的话,应该就是重写这一块了。 每个Maze类需要实现实现以下方法 CheckEnter 检查是否进入了该迷宫 CheckEscaped 检查是否离开了该迷宫 Walk 迷宫的下一步动作 OnStepFinsih 迷宫移动结束后的动作 很明显,在通过CheckEnter和CheckEscaped的组合判定迷宫生效后,通过覆盖Move的Walk和OnStepFinsih方法,来代替move的正常操作。 对于实际移动来说,根本不知道也不会去判断是路线规划返回的移动指令,还是迷宫返回的。 非常非常标准的一个补丁行为。 具体的Maze实现,可以参考 代码地址
  • 1 帖子
    28 浏览
    jarlyynJ
    对象(Object)是Mud底层 的Mudos/Fluffos的核心概念。 每个房间/npc/道具都是一个Object,每个Object 内能包含一系列其他Object的列表。 所以,作为一个针对运行在Mudos/Fluffos上的机器人,必然需要面对大量的Object的处理。 我在newhelljs中创建了一个Object类来专门处理这个问题 代码地址 代码中对应了两种概念 Object对象 Object对象中包括了 Label(显示的名字),ID(物品ID),IDLower(小写化的ID),惰性加载的 数量,单位,名字,依据可选的Param 对应的就是Mud中最基本的Object概念。 不论房间中的对象,还是身上的道具,都抽象成这样的一个标准类。 List对象类表 对象列表是一个非常实用的类。 它除了用来放置所有的Object对象外,还提供了一些快速的查找过滤的方法 FindByID 通过ID(大小写敏感)查找 FindByIDLower 通过ID(大小写不敏感)查找 FindByKey 通过特意制定的Key查找 FindByLabel 通过显示的标签(带数量)查找 FindByName 通过解析后的不带数量的名字查找 SearchByName 部分匹配不带数量名字查找 SearchByLabel 部分匹配带数量名字查找 SearchByID 部分匹配id查找 ExcludeID 排除ID查找 FindByFilter 自定义过滤函数查找 以及一些快速方法 First 返回列表中的一个元素 Last 返回列表中的最后一个元素 Sum 统计列表中的数量总和 来覆盖了绝大部分Mud任务的需求。 毕竟本质上,由于Mud底层的实现,除了解密类任务,大部分任务主要就是针对道具,以及房间中的 NPC进行交互。 只要能及时把身上的道具和房间里的NPC抽象为Object和List,做对应的任务可以说易如反掌。 道具处理代码参考 let matcheritem = /^( |□|○)(.+)\(([^\(\)]+)\)$/ let matcherend = /^目前携带了(.+)件物品。$/ //道具统计的计划 let PlanOnItem = new App.Plan( App.Positions.Connect, function (task) { let data = {} task.AddTrigger(matcheritem, function (task, result, event) { let item = new objectModule.Object(result[2], result[3], App.History.CurrentOutput) let index = data[item.IDLower] if (index == null) { index = 1 } switch (result[1]) { case "□": item.Mode = 1 break case "○": item.Mode = 2 break default: item.Mode = 0 } item.WithKey(item.IDLower + " " + index) App.Data.Item.List.Append(item) data[item.IDLower] = index + 1 return true }) task.AddTrigger(matcherend, function (task, result, event) { App.Data.Item.Count = objectModule.CNumber.ParseNumber(result[1]) }) }, function (result) { checkerI.Reset() }, ) ... 房间NPC处理代码参考 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") }) ...
  • 深入浅出制作全自动Mud机器人-路线规划配置

    Script脚本 mud机器人 全自动
    1
    1 帖子
    25 浏览
    jarlyynJ
    之前有提过,我的最新的机器人的路线规划都是通过HellMapManager和hellmapmanager.ts来实现的。 在HMM中规划路线有3个方法 APIQueryPathAny 点对点规划,对应To APIQueryPathAll 多点遍历,对应Rooms APIQueryPathOrdered 顺序遍历,对应Ordered 在外加一个无视地图信息的Path,就是我的地图模块的主要路径规划方式了。 很明显,为了性能考虑,4种路线都是预先生成所有路径的。如果对应到导航App,就是一个大大通向的目标的绿线已经画好了。 但在实际使用时,我并不会把所有路径暴露给外部。 实际上,To,Rooms,Ordered,Path显示的实现了2个方法 Next(move, map) 返回下一步移动的指令 Retry(move, map) 重试(重新规划) 正是为了实现重新规划(其实还有迷宫的因素),所以路线规划时是一次全部生成,再分小步喂给行走模块,按需进行重新规划的。 这也和主流的导航 APP的操作一致。 当然,由于To,Rooms,Ordered是和当前位置相关的。 为了实现重新GPS定位,调用了内建的Locate类,因此还需要接管 OnStepTimeout(move, map) 这个超时类,以便定位时被拦能超时重试。
  • 1 帖子
    26 浏览
    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 帖子
    25 浏览
    jarlyynJ
    所谓的惯性导航,其实就是在机器人进行移动时,能推断出自己目前的位置。这样在移动中和移动结束时不会因为丢失位置信息而需要重新定位。 实现惯性导航的方式我见过不少。有根据地图计算下一步的房间的,甚至有监听发送的指令来进行计算的。 从我的理解来看,惯性导航,和真实的导航APP一样,代表一种预期。就是我走到这一步后,我应该在什么位置。 这是在路径计算时,已经计算好的预期。 如果行进中发生了意外,比如路线错误/被随机移动/被挡路,则应该将条件设置好后重新计算路线。 就如同导航APP的"已经选择新路线,新路线快/慢XX分钟"那样。 所以,在路线规划时,不管是移动,还是遍历,不光光应该获取到指令列表,还要获取到每个指令对应的目标,这样才能更好的进行位置计算。 当然,基于预计算的惯性导航也容易出问题,典型的就是位置计算错误后,会卡在某地。 但个人认为,显性的Bug比隐性的Bug好,容易复现的Bug会更容易解决。
  • 1 帖子
    28 浏览
    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 帖子
    74 浏览
    jarlyynJ
    hellmapamanager.ts是一款将HellMapManager中C#的数据维护/调用代码用Typescript重写的项目。 用于在不使用http接口的情况下,使用v8/luajit等高性能脚本引擎直接调用HellMapmanager中的相应算法。 项目支持 hmm格式文件的维护 编译到javascript/lua格式 项目地址为 https://github.com/hellclient-scripts/hellmapmanager.ts
  • HellMapManager软件介绍

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

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

    Script脚本 全自动 机器人 代码范例
    1
    1 帖子
    36 浏览
    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 帖子
    25 浏览
    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的复杂配置就能完成了。