跳转至内容
  • 0 赞同
    1 帖子
    14 浏览
    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可能还会有一些细节上的调整。 移动失败处理的主要处理方式就是 重试。 调整上下文,重新规划,避免失败出口。 挂起移动,解决意外,还原移动并继续,
  • 0 赞同
    1 帖子
    8 浏览
    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") }) ...
  • 0 赞同
    1 帖子
    7 浏览
    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处理函数进行处理,处理完毕后抛出事件通知需要后续操作的模块。 这样才能最大的程度的解耦合,并保证房间处理信息的内聚。 不做切割的话,很容易随着房间信息格式的调整,把整个代码搅乱。
  • 深入浅出制作全自动Mud机器人-同步

    Script脚本 全自动 机器人 代码范例
    1
    0 赞同
    1 帖子
    16 浏览
    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个空格开头(固定格式)来判断的。
  • 0 赞同
    1 帖子
    30 浏览
    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 } 任务编排带来的新问题 任务编排,本身就是为了自由度,自由扩展,解决 怎么才能完成更复杂的任务,获得更好的回报? 这个问题 同样的,也会引来新的复杂度 就是既然使用任务编排时我们一定会倾向于细化多分任务 那其他的相关设置怎么办?不如不同任务中不同的战斗设置? 所以,我们在配置时,不得不对所有相关的设置都引入条件指令格式。 增加了配置的复杂度。 鱼和熊掌不可兼得,莫过于此。
  • 0 赞同
    1 帖子
    7 浏览
    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,会直接失效,不会在下次连接时继续触发。