简单的benchmark:
gowrk+httpapi: 0.5ms/req
nodejs:1ms/req
dll+lua:3ms/req
lua5.1/luajit:10ms/req
jarlyyn
帖子
-
[开发]hellmanager.ts lua版本放弃性能优化声明 -
[开发]hellmanager.ts lua版本放弃性能优化声明这次优化了下寻路算法,就正常寻路优化到了10ms左右
但是由于这是同一优化,api/node/dll模式也得到了极大的提升,所以差距还是等比例的。
因为,虽然寻路优化到了可用的范围(10ms)
但在追求性能的场合,依然无法提供性能保证。
-
HellMapManager 2026.04.16版发布- 界面优化 在关系地图中,会显示房间对应的Marker信息
-
hellclient 2026.04.16版发布更新内容为加入脚本shared数据功能和相关api
shared为使用同一个脚本的游戏可以共享读取的数据。
相对于现有的数据:
- 脚本数据 只读,共享。
- Mod数据 只读,共享,需要额外开启
- 用户数据 读写,独立。
- 共享数据 读写,共享。
-
[开发]hellmanager.ts lua版本放弃性能优化声明通过测试,将HMM编译为动态链接库,然后封装成lua库供lua调用,大概能在lua(非git)跑出45 ms的速度,也算可用了。
-
[开发]hellmanager.ts lua版本放弃性能优化声明经过实际业务的测试,很遗憾的放弃hmm.ts的lua版性能支持。
由于HellMapManager的功能日渐复杂,性能压力也越来越大。目前的HMM的性能测试数据如下
- API模式,普通寻路,9ms,十分优秀。
- TS转译js,node执行 20ms,基本可用。
- TS转译lua,luajit执行,100ms,勉强可用。
- TS转译lua,lua执行,500ms,完全不可用。
由于绝大部分客户端仅支持原生lua, 并不支持luajit,而且ts转译lua还是有不小的性能损失的。所以hmm.ts基本不可能在可见的技术情况下达到可用的程度。
因此,我们不得不遗憾的承认,没有能力让hmm.ts达到在各大客户端可用的程度。
hmm.ts的lua版本仅提供兼容性支持,提供可编程的数据转换。
如果lua机器需要使用hmm数据库,需要走api通道。
当然,如果有可能的话,我也希望能让支持lua的客户端能高效的使用全功能的hmm.ts功能。
-
Hellclient UI 2026清明版发布- 升级flutter版本
- 修正部分文字错误
-
HellMapManger及hellmapmanager.ts 26.03.16版本发布- API版本更新到 1006
- HellMapManger 变量页面显示优化
-
深入浅出制作全自动Mud机器人-移动失败处理对于整个移动模块来说,最核心的可能就是移动失败处理了。
本身对于大部分系统来说,失败/意外流程处理都是最复杂的。而移动模块的失败处理大部分面对的是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机器人-选项模式选项(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机器人-数模转换数模转换其实是一个很古早的概念了。
就是将原始的模拟信号,转换成数字信号,然后数字化的系统才能根据数字信号进行操作。
首先,从严格意义上说,服务器返回的文字信息本质是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机器人-遍历在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机器人-迷宫mud中,迷宫是一个很重要的课题。
其实这里说的迷宫,并不仅仅指迷宫这个解密形态,指的是非标准移动,动态出口。
有些需要特殊物品/ask npc才能通过的出口,其实也是迷宫的一种。
在我的第三版移动代码里
Maze实现的依然有点丑。如果我还有第四版的移动代码的话,应该就是重写这一块了。
每个Maze类需要实现实现以下方法
- CheckEnter 检查是否进入了该迷宫
- CheckEscaped 检查是否离开了该迷宫
- Walk 迷宫的下一步动作
- OnStepFinsih 迷宫移动结束后的动作
很明显,在通过CheckEnter和CheckEscaped的组合判定迷宫生效后,通过覆盖Move的Walk和OnStepFinsih方法,来代替move的正常操作。
对于实际移动来说,根本不知道也不会去判断是路线规划返回的移动指令,还是迷宫返回的。
非常非常标准的一个补丁行为。
具体的Maze实现,可以参考
-
深入浅出制作全自动Mud机器人-对象列表对象(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() }, ) ...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机器人-路线规划配置之前有提过,我的最新的机器人的路线规划都是通过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)这个超时类,以便定位时被拦能超时重试。
-
深入浅出制作全自动Mud机器人-移动模块拆解在移动模块介绍时我们说过,整个移动模块是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机器人-惯性导航所谓的惯性导航,其实就是在机器人进行移动时,能推断出自己目前的位置。这样在移动中和移动结束时不会因为丢失位置信息而需要重新定位。
实现惯性导航的方式我见过不少。有根据地图计算下一步的房间的,甚至有监听发送的指令来进行计算的。
从我的理解来看,惯性导航,和真实的导航APP一样,代表一种预期。就是我走到这一步后,我应该在什么位置。
这是在路径计算时,已经计算好的预期。
如果行进中发生了意外,比如路线错误/被随机移动/被挡路,则应该将条件设置好后重新计算路线。
就如同导航APP的"已经选择新路线,新路线快/慢XX分钟"那样。
所以,在路线规划时,不管是移动,还是遍历,不光光应该获取到指令列表,还要获取到每个指令对应的目标,这样才能更好的进行位置计算。
当然,基于预计算的惯性导航也容易出问题,典型的就是位置计算错误后,会卡在某地。
但个人认为,显性的Bug比隐性的Bug好,容易复现的Bug会更容易解决。
-
深入浅出制作全自动Mud机器人-移动确认在开始制作一个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项目介绍hellmapamanager.ts是一款将HellMapManager中C#的数据维护/调用代码用Typescript重写的项目。
用于在不使用http接口的情况下,使用v8/luajit等高性能脚本引擎直接调用HellMapmanager中的相应算法。
项目支持
- hmm格式文件的维护
- 编译到javascript/lua格式
项目地址为
-
HellMapManager软件介绍HellMapManager是一款使用c#语言开发,支持 Windows/Linux/MacOS的地图编辑/路径规划软件。
软件支持
- hmm/hmz格式地图信息
- 地图信息维护
- 地图版本对比
- 补丁生成和选择性应用
- 通过http接口方式进行调用
- 无头模式
等功能
项目地址为