跳转至内容
  • 深入浅出制作全自动Mud机器人-事件总线

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

    Script脚本 全自动 机器人
    1
    1 帖子
    9 浏览
    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 帖子
    61 浏览
    jarlyynJ
    更新内容: 修正连接按钮状态bug 部分UI调整 iOS及MacOS版请在AppStore更新 发布地址
  • 深入浅出制作全自动Mud机器人-主逻辑

    Script脚本 全自动 机器人 架构
    1
    1 帖子
    15 浏览
    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 帖子
    50 浏览
    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 帖子
    30 浏览
    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 帖子
    65 浏览
    jarlyynJ
    版本号 1.2026.01.13 主要内容为 修复v8 内存泄漏问题 修复v8的HTTP接口内存泄漏问题 部分UI微调 下载链接:https://github.com/jarlyyn/hellclient/releases/tag/2026.01.13
  • 1 帖子
    41 浏览
    jarlyynJ
    虽然很多客户端和教程介绍机器时,都是用触发来驱动机器。 但是,要做一个稳定,功能强大的机器,被动的靠出发来驱动明显不可行,必须要依靠全面详尽的数据来运行。 在Mud中,有很多指令会对应获取更新数据。常见比如"hp","score","i","skills"等。 我们一般都会根据这些指令的结果来更新数据。 但是,Mud又是动态的,这些数据往往又容易失效。 做任何动作之前猛刷一通指令明显不科学,浪费服务器资源,还干扰用户使用。在这个情况下,我们肯定是要面对有时效性的数据来进行编码。 也就是说,我们在收集数据时,不光光面对是一个采集的问题。还要把数据是做缓存,处理一个缓存失效的问题。 那以缓存的角度来看看到数据的话,实际是要实现以下几个地方 数据的有效期(ttl) 数据回源(发现过期后主动更新数据) 数据的强制更新(更新数据后更新数据和有效期) 数据强制失效 实现的方法其实有很多,我这里说说我的实现方法。 以我的代码为例 在处理数据时,我使用了这么一个结构 let DefaultExecute = function (check) { return check.Command } class Check { constructor(id) { this.#id = id } #id = "" Interval = 0 #last = 0 Execute = DefaultExecute Command = null WithExecute(fn) { this.Execute = fn ? fn : DefaultExecute } ID() { return this.#id } WithCommand(cmd) { if (typeof (cmd) != "function") { this.Command = function () { App.Send(cmd) } } else { this.Command = cmd } return this } WithInterval(interval) { interval = interval - 0 if (isNaN(interval)) { interval = 0 } this.Interval = interval return this } InCooldown() { return (new Date()).getTime() - this.Interval < this.#last } Reset() { this.#last = (new Date()).getTime() } Force() { this.#last = 0 } } 这个机构很明显。 #last 最后一次更新时间 Interval 重新获取数据的间隔(ttl) Execute,Command 更新数据的函数和指令 #id 缓存的 id,可以制定id进行更新 在使用时,我会以这样的形式来注册一个数据检查(更新)器 let checkerSkills = App.Checker.Register("skills", "skills", 300000) 这是注册一个 id为skills,指令为skills,数据有效期为300秒的检查器。 在匹配skill最后的代码最后调用一下Reset function (result) { checkerSkills.Reset() }) 就能按一定的有效期来保存数据了。 然后由于这个是获取技能数据的,所以我绑定了技能提升的触发事件,在技能重置时强制这个缓存数据失效 //技能升级时重置相关的checker App.BindEvent("core.skillimproved", function () { checkerHPM.Force() checkerSkills.Force() checkerJifa.Force() }) 就能比较有效的处理缓存数据了。 在可能需要处理状态的地方,我加入了检查所有预设检查器的代码 //检查的函数 App.Check = () => { App.RaiseEvent(eventBeforeCheck)//触发检查发送指令 let checks = App.Checker.Check()//获取需要执行的检查 if (checks.length == 0) {//无需检查 App.Next() return } App.PushCommands(//一次性执行所有检查 App.Commands.NewFunctionCommand(function () { checks.forEach(check => { check() }); App.Next() }), App.NewSyncCommand(), ) App.Next() } //准备函数,第一个函数时准备id,第二个时准备的上下文(环境变量) App.Prepare = function (id, context) { App.RaiseEvent(eventBeforeCheck) let checks = App.Checker.Check() if (checks.length == 0) { AfterCheck(id, context) return } App.PushCommands( App.Commands.NewFunctionCommand(function () { checks.forEach(check => { check() }); App.Next() }), App.NewSyncCommand(), App.Commands.NewFunctionCommand(function () { AfterCheck(id, context) }), ) App.Next() } 这样就能确保每次在需要数据进行决策(准备)时,使用的是最新的有效的数据。
  • 1 帖子
    24 浏览
    jarlyynJ
    单例模式,是设计模式的一种。它是一种创建型设计模式,它的核心目标是确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一的实例。 听着很牛吧?别管这个,这是被强OOP祸害的语言的鬼话。 单例模式本质就是,全局变量。 好了,使用lua的同学们放下手。全局变量不是你不用加local 来限制使用作用范围的意思。 单例可以通过全局变量来实现,但它强调的还是独一无二性,在英语里要加上the 定冠词那种(嗯,就是黑客帝国的the one)。 说明是一个特殊,独一无二的个体。 在代码开发中,很常见的一种模式是,将整个程序的功能,抽象成一个Applcation,然后建立一个全局单例 Applcation:Instance 来进行代码的开发。 在Javascript的管理,就是建立一个全局的app对象,Lua作为脚本语言,也可以适用这套模式。 那么,问题来了,我们为什么要用这个全局变量(单例)呢? 限制其他全局变量使用。 使用全局变量,就是能方便的在任何代码里进行数据分享/互相调用。 有需要就开一个全局变量,看似开发能快很多,但很快就会让代码失去可维护性,随便动一个变量都生怕整个程序崩了。 当我们有一个特殊的全局变量后,很自然的,我们需要调用的东西都会从这个全局变量(单例上找),不应该再开全局变量。 从这个角度来说,这个单例,就是我们整个机器人的the one 定义应用程序的根(root) 我看过很多全自动机器人代码。有一些,嗯,怎么说呢,让我感觉很乱,无从下手。 代码都是一个个平级的.lua/.js文件,互相之前会相互应用,要看很久才能推测出大概的功能划分。 按数据结构的说法,这些机器人的代码互相之间的关系是一个图(Graph),两两之间都可能有关联,很自由,很强大。 但对于我等普通人来说,难以驾驭。 相对而言,以我的能力,更能处理以数据结构的 树(tree) 的形式来组织的代码。 即从一个根room开始,不停分支,对应着一个一个的节点,和树杈一样。 我们在使用电脑时,最常见的树结构就是目录树,通过一个一个目录来对文件进行组织。 只有了一个固定的根,我们才能很容易的定义文件的代码的结构和组织结构。 给代码一个独一无二的挂载点。 上一个部分讲到了目录这个例子,那么我们扯远点说说Linux的文件管理。 linux下,讲究万物皆文件。不管你是设备,文件系统,文件,目录,都可以用mount的方式挂在到相对于根(root)目录的指定位置下。 那么,当我们使用单例App时,其实也是一样,把我们的代码/对外的结构,添加到单例和他的自元素上,就相当于做一个mount操作。 这时,每个挂载上去的代码,都成了一个单例(单例中的特定部分),都能很方便的找到。 在一般情况下,可能这个优势还不明显。 在javascript和lua中弱类型的脚本语言中,很难精确的定位到一个代码的位置。 那么,你觉得在你的代码中,是找到一个较 room的变量在哪里定义使用方便,还是一个App.Core.Map.Room 方便呢? 代码分组 还是文件目录的例子。 我们一般会怎么组织文件? 先把全放桌面的豪放流请下去。最常见的,其实是是按文件的用途或者特点,分为 文档,视频,照片,作业,学习资料什么的。 同样,既然我们把单例作为文件数来使用,我们也可以把代码和数据进行分区。 比如,主业务逻辑可以放在App.Core.XXX下,代码可以放在App.Data.XXX下,工具函数可以放在App.Utils.xxx下 这样在写代码时,可以一目了然,方便理解和组织。 总结 对于代码来说,全局变量(单例)是必不可少的,但是太多的单例会让代码复杂度指数上升。通过定义一个合适的根单例,我们能更好的组织代码,提可高维护性。
  • 深入浅出制作全自动Mud机器人-组件化

    Script脚本 全自动 机器人 架构
    1
    1 帖子
    14 浏览
    jarlyynJ
    在全自动Mud机器人开发,以及长期的运维更新时,我觉得引入组件化是一件非常重要的底层架构。 组件化,看起来是一个和模块化接近的概念。但本质完全不同。 在我眼里,模块化是将相关的代码进行组织和封装,对外提供统一接口的一种代码组织形式。 组件化的确要依赖于模块化是实现功能。但更重要的是,组件化的目标是 可替代可废弃性。 随着Mud的更新,代码不应该进行大幅度的调整,还是废弃部分组件,新建部分组件,调整部分组件。这样,机器可以先将重点组件进行调整,然后临时禁用不重要的组件,甚至部分影响不大的组件带病工作,实现机器的边开发边运行。 既将代码根据易变程度分为不同的组织,确保核心代码的稳定性,然后组件中可以用各种快速/不健全/临时性的机制确保特殊状态下机器的可用性,以及确保机器处于一种可以从小到大,逐渐成长的状态。 为了实现这个目的,组件化需要实现的功能是 组件标准化 同一类的组件,应该有相同的接口和核心代码进行交互。这样,核心代码才能在调整主键时不需要关注细节和深入。 以我的newhelljs为例,使用了多种组件 角色准备阶段的提案组件 通过通用的Submit参数,实现判断是否进入某个准备阶段 任务本身的[Quest组件](https://github.com/hellclient-scripts/newhelljs/blob/main/script/src/core/quests.js 通过Ready参数,Cooldown属性,OnHUD/OnSummary|OnReport等接口,实现了人物与核心代码的隔离 配置的Condition组件 使得是否接任务/是否使用某个技能等等,都与核心代码无关,完全通过定义新的Condition实现。 很明显,组件标准化和能区分组件代码和核心代码的前提。如果不对组件进行标准化,则可扩展性可维护性也成了无根之水。 控制反转 控制反转(IOC)是一个常见的程序开发思路。 所谓的反转,就是将** 主程序创建类后,初始化类** 反转为 ** 主程序只负责创建类,类自行初始化) 实现了控制反转后,才能实现组件的封装,将组件与核心代码分离。 核心代码只负责两件事情: 如果你随手搜索一下,可能会发现一大堆依赖注入,工厂模式什么的复杂东西。 不用管那些,那些主要是对强OOP,静态编译的Java强调的东西。 对于js/lua的实现,参考我上面的例子中,都会传入一个函数作为参数就可以了。 把初始化函数放在组件的制定位置,核心代码创建组件后,把配置作为参数带入初始化函数就可以了。 这里说的控制反转,只是提醒一下,不要在核心端去设置组件,让组件自包含。 全局注册 全局注册很简单,就是建立一个全局的object/table变量,给每一个组件起一个唯一的字符串主键,将组件或者组件的生成函数都放在里面就行。 全局注册的目的是 在进行组件的替换/更新甚至顺序调整时,无需调整核心代码,只需要调整某个配置即可。 通过同一的调用方式,确保不会有其他位置的代码来直接调用组件内的代码,避免组件修改时出现全局问题。 方便的管理方式,能很容易的明白有多少个可用模块,不用去各个代码目录里寻找。 使用全局注册要注意的点是 只通过 注册的表 与组件交互,不建立直接处理的代码 在多个组件交互时,可以通过给组件加接口/全局事件/核心组里注册特殊毁掉的方式来处理。 重要的点 使用组件模式,其实是按照我们最初的目的来使用,也就是 将代码分为干净的核心代码,和脏(与业务更接近)的组件代码。确保代码指令控制在一个基本的可用度上。 通过配置,设置,能动态的调整激活的模块,实现更复杂的功能。 技术只是手段,代码如何处理,不一定要强求某个模式,还是为了实际目的服务。
  • 我使用的一种机器人代码分层组织方式

    Script脚本 机器人 架构
    1
    1 帖子
    15 浏览
    jarlyynJ
    在写gui程序时,我整理了一套代码组织方式 参看 HellMapManager项目就是按这个架构来组织的。 对于Mud机器人,我觉得这个组织形式也适用。当然,由于主要业务不同,住址形式也会不同。我只是将所有的代码根据对应的层次打个标签,避免不同层级的代码过于混淆。 UI 交互层 这个完全绑定于客户端的实现,在Mud机器人里也相对不重要,可以和Service层混在一起 Service 服务器层。从字面意义上来说,就是为最终用户提供的功能的封装层。UI交互的内容最终绑定到服务层上。最大的用途是防止用户UI直接操作到业务层,做封装和拦截,以及抽象。 Core 核心(业务) 层。对于Mud机器人来说,绝大部分的代码都是核心层。所以实际写机器人的时候Core肯定还要做细分,比如底层核心和任务模块。 Helper 辅助类,业务层和数据层直接的纽带,将数据的细节对业务做一定的封闭。 Adapters 适配器层 抽象底层交互。对于Mud机器人来说,就是将客户端的触发/别名/计时器做一个抽象,以及对应的事件Event框架。引入这个层的话能提升机器人可迁移性和做测试的可能。 Model 模型 在各个业务层中共同的数据结构。在Mud中比如房间,玩家,道具等等。 utils 工具层。比如中文转数字,格式化文字等纯与业务无关的,全局都可能使用的代码
  • 深入浅出制作全自动Mud机器人-运维通知

    Script脚本 全自动 机器人
    1
    1 帖子
    13 浏览
    jarlyynJ
    在引言中,我们提到过,制作全自动机器人的一个重要课题是 怎么才能让循环更稳定,控制更多的资源和状态? 那么,提升稳定性的一个重要的工作,就是当服务(机器人)发生意外时,能及时的通知运维(开发者/用户)查看问题,进行操作。 就是当机器停顿,无法继续,甚至ID死亡时,以方便的手段通知用户,记录问题现场,恢复程序执行。 这也是一个标准的运维工作。 所以我建议参考现在主流的运维工作流程。 邮件形式 Hellclient最早是支持嵌入STMP信息,提供了内建方法可以发送电子邮件。 但在实际使用时,基本处于被废弃状态。 因为现在主流的运维通知终端是手机,安卓或者iOS。 邮件通知在手机上面临了一下问题 手机设备上,邮件客户端无法保证后台常驻,实时性往往有很大问题。 邮件客户端又有各种实现,对于链接的处理形式不同,无法保证唤起我的APP端。 邮件的安全级别很高,所有各个邮件服务器的安全设置很复杂,对用户并不友好。 通知服务 我的Hellclient UI项目曾经实通过腾讯推送实现过原生的推送功能。 但现在这个功能已经被废弃了。 通知服务会涉及到几个难点 用户识别问题。安卓客户端还能自行申请推送服务,iOS不现实,发布时就绑定应用ID。 保活问题。对于国内环境,国产安卓是绕不过去的槛。各种手机的保护和续航是很麻烦的点,只是做一个mud相关的app没法投入这么多精力去处理 这写严重的问题随着新的方案出现,对于Hellclient UI项目属于验证的得不偿失,所以废弃了这个功能。 Webhook 目前的运维主流的通知手段其实还是webhook的形式。 就是在各种工作流软件中,会提供一个标准的,地址带身份验证token的http接口。 主要对这个接口发送特殊格式的内容,就会通过服务器向对应的手机APP发送一条通知。 处于这个通知的专用型,一般都能确保拉起手机中注册过的APP直接处理。 可以说完美的适合了Mud机器人,作为一个标准意义上的服务的运维要求。 而且webhook形式的要求最低,基本不需要客户端做特殊支持,只要提供标准的HTTP接口和JSON支持就可以了。 国内比较常见的Webhook供应商为 企业微信 钉钉 飞书 国外知名的有 Slack 根据我自己的使用体验和经验,我的机器人使用的飞书通知。目前使用体验良好。 如果不喜欢单独安装App的话,也可以考虑使用企业微信。企业微信可以绑定到微信内,我个人有其他业务是使用企业微信通知的。
  • 深入浅出制作全自动Mud机器人-目录

    已固定 Script脚本 全自动 机器人
    1
    1 帖子
    57 浏览
    jarlyynJ
    序 引言 架构 全局单例 组件化 事件总线 选项模式 核心 主逻辑 数模转换 数据与缓存 准备阶段 指令队列 用户队列 任务编排 战斗子模块 战斗系统 移动子模块 移动模块 移动确认 移动失败处理 惯性导航 移动模块拆解 路线规划配置 迷宫 遍历 组件 预期管理 同步 对象列表 配置 条件指令 周边 运维通知
  • 深入浅出制作全自动Mud机器人-引言

    Script脚本 全自动 机器人
    1
    1 帖子
    25 浏览
    jarlyynJ
    全自动Mud机器人是一种很典型的完美程序 完美的功能,解放人力 高度稳定性的代表 全面处理随机和意外 可以说,是对编写代码有兴趣,有热爱的Dream Project。 很多Mud玩家成为“高手”的标志之一。 当然,也会让很多人觉得,这很难,我做不到。 其实,只要开始做,全自动Mud机器人并没有这么难。 本系列的文章,会涉及编写全自动机器人的方方面面,帮助读者写出一个属于自己的,能顺畅运行的全自动机器人。 当然,在开始前,我们先要解决一个问题: 什么是全自动机器人? 故名思议,全自动机器人就是能不需要人照顾的,自动完成某个人物的机器人。 理论上说,用一个定时器,每隔半小时在公开频道发布你要出售的道具的机器人,也算全自动机器人。 但这明显不符合我们的目标。 在此,我给全自动机器人下一个定义,全自动机器人是符合这样的两个条件而代码 在不需要人力维护的情况下,形成一种稳定循环,能控制状态和各种资源正向发展,不至于陷入困境而被动停摆 能够完成复杂任务获得回报 在这个定义下,我们能够明确的描述出我们这个系列研究的课题: 怎么才能让循环更稳定,控制更多的资源和状态? 怎么才能完成更复杂的任务,获得更好的回报?
  • 自制Mud客户端开发-基础篇

    Mud客户端倒腾 mud客户端
    1
    1 帖子
    95 浏览
    jarlyynJ
    大部分Mud玩家,在有能力制作一个可用的机器人后,都会起自己制作客户端的念头。让自己的机器人在自己的客户端里刷刷的跑,能给一个Mud爱好者带来极大的满足感。 在科技发展到现在,各种语言和框架层出不穷,电脑的硬件也极度丰富,外加AI技术的发展,写一个Mud客户端对普通爱好者已经不是一个遥不可及的事情了。 这系列文章,就是集合我在制作Hellclient时的一些心得体会,希望能给其他同好带来帮助。 而本文,个人希望,类似于武侠小说中的武功总纲,体现出大体框架,然后通过一篇篇细节文章,把整个自建客户端的关键点串起来。 首先,我们要明确,一个Mud客户端要解决几个问题。 我归纳下来,主要是以下几个方面 与Mud服务器的信息交互(telnet/ansi) 用户的快速工具(触发,别名,计时器) 合适的机器配置格式(比如变量) 脚本引擎 用户界面 扩展性 Telnet/ANSI库 telnet可以说是一个老旧的成熟的协议了。大部分的语言都有telnet库,没有telnet库的小众语言利用AI照着现成语言的库抄一个也不算难事。 对于Mud来说,主要是处理一些指令,和subnegotiation协议拓展出来的指令就行。 而ANSI的话,也是定义了另一套显示控制的指令。 从我个人经验来看,telnet和ansi的原始数据在客户端能应该需要一个中间模型(model),能快速的供界面渲染和引擎调用。有些客户端直接渲染,再从渲染完的结果倒过来取数据。个人觉得这样不好,会极大的增加脚本制作的复杂度。 触发/别名/计时器 这三个东西代表了一起机器主要要处理的问题: 服务器返回的响应 用户的输入 时间的流逝 对这个核心,我的思路也有过很多变化。就目前的思路来说: 客户端的触发/别名/计时器,应该从好用,易用的角度出发,让用户能很方便的添加临时功能。 脚本引擎应该抽象出 服务器交互/用户输入/时间层出来,不受客户端限制,不利用客户端的特殊功能。 当然,这只是建议,和一个状态下的想法。整体还是要根据你做客户端的目的来开发。 机器配置 在很多客户端里,变量是和触发/别名/计时器评级的概念。 从我的理解来说,并不是这样。 变量(机器配置)应该是和脚本直接绑定的。 按现代开发的概念,变量至少是 一个Config文件,决定了机器的启动设置。 如果是变量名对应变量值的形式,它更接近于环境变量 这个概念。 同时,个人建议,也是我Hellclient的做法,变量要有备注或注释的空间,可以让用户理解变量怎么使用。 脚本引擎 在真实世界中,嵌入式脚本引擎其实只有两个玩家,javascript和lua。 Lua是老牌玩家,各个语言都有lua的实现或者Binding,语法简单速度快。有可能的话建议使用luajit库,性能还是很不错的。 Javascript又是另一个话题。Javascript是目前使用最广泛的脚本语言,npm上有大量的js库。但嵌入js库不是很容易的一件事情。本质来说,js生态就是v8生态。但不是每个语言都能很容易的嵌入v8引擎。hellclient嵌入v8也是将一个半废弃状态的库进行了一定的魔改。还遇到了Windows下的msys和vc编译器兼容的问题。 所以,个人意见,能方便的用v8的(比如C#有微软官方ClearScript支持,建议一定要支持javascript,lua的话,建议都可以支持一下。 用户界面 就目前的现状来说,除非你使用特殊的技术栈或者技术,不然大部分情况下的软件开发都是跨平台,Windows/Linux/Mac全支持的。 具体到用户界面的形式,其实是3种 浏览器 浏览器形式的(主要是electron,少数如tarui会使用系统自带浏览器) 最大的优势是界面多样话,自定义方便,美观迅速。 缺点是非nodejs方案会多引入与浏览器的交互,性能有上限。而nodejs本身的单线程模型要改造的话工程量很大,强如微软也在vscode里堆了一堆C代码。 客户端 客户端的方案的话,其实主要是qt和skia 两个图形库的方案。qt基本绑定c++,不是那么容易上手(pyqt选手另论)。skia的话,有flutter/dart和avalonia/c# 两员大将,都是不错的方案。 客户端型界面最大的优势是性能有保证。缺点是实现美观/华丽的界面麻烦 无头模式 无头模式就是指提供接口,供网页/其他客户端显示。 理论上说,无头模式一般都会实现一个网页界面。 但他和网页界面的区别是,无头模式由于要支持多种终端,无法充分利用网页界面的华丽/可制定性强,在界面上有很大的妥协。 无头模式的特点是: 1.轻量级,可扩展性高,适合云端运行,通过网页/客户端/app等多种形式连接。 2.界面受限,需要维护多个程序。 Hellclient就是无头模式的客户端。 扩展性 扩展性其实主要就是3个方案 动态链接库(so/dll) 通过lua加载动态链接库可以很容易的对lua进行扩展。性能和功能都是最强大的。但同样的对开发人员的要求也是最高的,安全性上也容易出问题。 Com技术 如果你把自己局限在Windows平台,使用C++/C#开发,activex/com技术是极为强大的。mush本身除了lua的语言也都是通过activex来实现的。但com技术已经是一个事实上逐渐被非主流化的技术,不是很建议绑定的Com技术上 http/websocket技术 通过基于http的api 接口或者websocket实时调用。这个方案在性能上会有所损失,但同时有能够云化集群化的优点。Hellclient就是基于这种方案来提供扩展性。 我的建议 Hellclient是使用go写,采用无头模式,通过http/websocket进行扩展的。 如果让我从头来过,我可能会换成c#,因为go在windows平台下嵌入v8有点蛋疼。 我推荐的语言选型为 c# 综合的王者 flutter/dart 桌面/app全能 electorn/nodejs electorn也是目前桌面端很主流的选择 python web/pyqt方案 这个只适合自用,发布打包都太麻烦了
  • Hellclient UI介绍

    已固定 HellclientUI应用
    1
    1 帖子
    26 浏览
    jarlyynJ
    Hellclient UI是一款由Dart/Flutter 开发的,支持Android/iOS/Windows/Linux/Mac等各平台的Hellclient客户端专用控制软件。 项目地址:https://github.com/hellclient-scripts
  • Hellclient软件介绍

    已固定 Hellclient软件
    1
    1 帖子
    41 浏览
    jarlyynJ
    Hellclient是一款使用go语言开发为运行在vps上设计,支持javascript/lua脚本,通过websocket协议,通过web界面或者Hellclient UI软件访问的容器向Mud客户端。 项目地址为 https://github.com/jarlyyn/hellclient
  • 欢迎访问Hellclient社区

    已固定 已锁定 意见与反馈
    1
    1 帖子
    132 浏览
    jarlyynJ
    欢迎您访问Hellclient社区。 这里是Hellclient客户端及相关mud软件和脚本的沟通社区。欢迎在这里对Hellclient软件,和Mud相关的软件开发和脚本制作进行沟通。 在这里,我们会以下讨论的规范要求每一个用户 仅讨论软件的使用问题和Mud的技术问题。 不要对别的用户,Mud,和机器人进行评论。Mud本身已经是一个粉丝向的小圈子了,大家都是非专业的玩家,请互相保持友好。 由于 Mud 圈本身的互相借鉴和借用的现象很严重,所以本社区不支持任何资料的上传。有代码或程序分享的需要,请上传至github/coding/gitee等平台。 请不要分享破解或者攻击软件/服务器/游戏的经验。 尊重您玩的Mud的规矩,不要讨论和分享针对某个Mud使用,又不被Mud Owner允许的功能。 不建议使用您在游戏里的常用ID,把您在游戏的恩怨情仇蔓延于此。 热爱国家,遵纪守法,不违背公序良俗。 说的有点多,希望我们的社区能能给每一个访问者带来快乐和收获。