Alt-F4 #18 - Clusterio 2.0 之路 2020-12-18
翻译 Ph.X
年关将至,我们在本周的第 18 期 Alt-F4 中挑选了两个与 Mod 相关的话题。首先,Hornwitser 为我们介绍了 Clusterio 2.0 漫长的开发进度以及它所带来的挑战。然后,DedlySpyder 讲述了他们开发一个简单 Mod 的过程以及面临的兼容性挑战。
Clusterio 2.0 之路 Hornwitser
我想讲讲我如何花了一年时间开发 Clusterio 2.0 的故事,它离发布还有很长的路要走。如果你之前没有听说过 Clusterio,它是由 Danielv123(以及大约 30 位贡献者)所编写的开源服务器软件,可以让 Mod 跨服务器互动。它最出名的可能是 2018 年的 Clusterio 60k 活动,在这个活动中,传送箱被用来在大约 46 个异星工厂服务器之间传送物品,以建立一个类似原版的工厂,达到每分钟可以做 60 千瓶。这些传送箱的工作原理就像主动供货箱和集货箱一样,一个从游戏中取出物品,并将它们放在共享的云存储中,另一个则从该云存储中取出请求的物品,并将它们放在游戏中。
Clusterio 一直由两部分组成:在 Mod 代码中实现并在游戏内运行的游戏交互部分,以及处理不同游戏服务器之间数据移动的服务器端基础设施部分。一开始,服务器端是围绕着处理传送箱子进行编码的,但随着开发的深入,越来越多的功能被添加进来,Clusterio 是 什么的想法也从传送箱子变成了一个模块化的服务器端平台,旨在制作这种跨服务器的游戏元素。
2019 年 7 月,举办了“Gridlock Cluster”活动。在服务器之间运输物品的不是传送箱,而是利用传送火车站将列车从一个服务器的地图边缘传送到另一个服务器的地图边缘。传送列车的代码是由 Godmave 作为 Clusterio 的插件实现的。
遗憾的是,代码遇到了很多问题,这也正是我加入项目的地方。
筚路蓝缕
早在 2019 年 7 月,我就开始鼓捣 Clusterio 的代码库,作为试图帮助 Gridlock 团队解决他们的许多问题的一部分。服务器时不时瘫痪,玩家遇到问题,新的 Bug 和问题似乎每小时都在出现。虽然很忙碌,但也有不少乐趣可言。这次活动激发了我对 Clusterio 背后的代码的兴趣,活动结束后,我主动为下一次活动改进这段代码。事实证明,这个项目的规模远超我的想象。
我已经在 Clusterio 2.0 上稳定地工作了 16 个月左右,而我对它完成的时间的估计还是和一开始一样的“就几个月”。尽管如此,我继续工作的动力依然很强,我觉得特别有动力的一件事就是通过组织我自己的 Clusterio 活动来检验这些工作。我已经在 Reddit 上发了预告来讲述我内心的想法,现在的目标是明年年初举办。可能是 1 月份,不过只有时间才能告诉我们什么时候准备好。
但回到我的起点。我在我的服务器上安装 Clusterio,试图建立我自己的测试集群,以解决在 Gridlock Cluster 中遇到的问题。我首先注意到的是它拉来的一千多个包作为依赖项,占用了 300 MB 以上的磁盘空间。对于这个项目来说,需要这么多的包来运作似乎很荒谬。对我而言 Node.js 当时是个新事物,虽然我现在已经了解到,对于一个 Node.js 应用来说,这其实并不是一个非常不合理的依赖包数量,但还是太多了。这表明了这个项目所采用的开发风格:一种自顶向下的方法,即以任何一种最简单、最快速的方式添加功能。
这种开发风格导致了大量技术债务的积累,大量。技术债务是软件开发中经常被抛出的一个词。就是说在开发中为了节省时间而选择捷径,往往在维护和扩展代码库时,会给日后带来更多的工作。在某些方面,你可以说 Clusterio 更像是一个堆积在彼此之上的黑客技术,而不是一个经过深思熟虑和结构化的项目。一个令人难忘的例子是在同一个源文件中包含和使用了 4 个不同的 HTTP 客户端。通常 1 个这样的客户端对整个项目来说就足够了,但大概是某些事情用某个客户端会比其他客户端更容易做,随着时间的推移,不同的客户端堆积起来。
所以,我开始着手改进和清理 Clusterio 的代码库。我做的第一件事就是筛除这一千多个依赖关系。结果发现,大部分包都不是运行 Clusterio 所必需的。大约一半是不需要安装在生产环境中的开发工具,四分之一是我所描述的快速解决方案:拉进来一个大型包只为使用其中的一个函数。这些包中的许多用处都微不足道,要么通过在本地重新实现函数,要么通过使用另一个已经是项目依赖的包来代替。最后,我设法删除了大约 700 个包(244 MB),尽管应该注意的是,其中大部分是依赖包的依赖包。
接下来我解决的问题是自动化测试。如果你对自动化测试不熟悉,那就是编写代码来验证主代码能否正常工作,并且不会因未来的变化而故障。自动化测试算是编写可靠代码的基石,虽然在某些时候设置了大量的测试,但当我进入项目时,它们并没有发挥作用。这是另一个技术债务重新抬头的例子。维护测试,以及添加新的测试来覆盖新的代码,是额外的工作;跳过这些工作是一条捷径。
在修复测试后,我的重点转向清理代码本身。做了一些事情,比如修复坏掉的代码,删除未使用的或过时的代码,并将坏的代码重构成不那么坏的代码。其中一个开始成形的变化是将传送箱的代码从主代码库中移出,移到一个单独的插件中。由于 Clusterio 首先是使跨服务器 Mod 交互成为可能的服务器软件,当我们越来越期待 Clusterio 会被用来做其他事情而不是这些箱子的时候,让这些传送箱子就让 Clusterio 的代码就显得很混乱。所以我们也决定将“通过魔法箱子进行物品传送”的功能重新命名为“亚空间存储”。趁着这段时间,我还决定用更适合的亚空间仓库来取代那些古怪的天空箱子和收集网精灵。
不过它们更多的还是一个占位符,因为在纹理和机械建模方面,我不是一个 3D 艺术家。我花时间用 Blender 设置了一个自动化的工具链来渲染、裁剪和输出精灵到 Mod 中。你知道程序员是怎么做的:把所有的事情都自动化。
存档补丁
随着我的工作继续进行,我所做的第一个重大改进是存档补丁,但在我谈论它之前,我想给它所要解决的问题提供一些背景。游戏引擎允许通过用 Mod 和/或场景中的 Lua 代码来修改游戏行为。Mod 在游戏启动时被加载,更新它们需要重新启动游戏。场景是与游戏保存一起打包的 Lua 代码,改变到不同的场景代码只需要加载不同的存档。
当改变游戏的行为被放入场景代码中时,它通常被称为软修改,因为你不需要下载任何 Mod 和重启异星工厂来加入使用这种场景代码的服务器。虽然更新 Mod 和继续现有的存档很容易,但使用场景代码就没那么简单了。基本上有三种方法可以更新存档中的场景代码,我将按照实现的难易程度大致列出:
- 对于通过 Mod 分发的场景,可以在 Mod 中添加一个迁移脚本,在 Mod 更新时更新场景。虽然这样做很简单,但它有一个很大的缺点,就是需要安装 Mod 才能运行迁移。
- 你可以在游戏未运行时替换保存中存储的场景代码。这就是我所说的存档补丁,它的操作比较简单,因为存档文件是普通的压缩文件,而 Lua 代码是以普通文本文件的形式存储在其中的。
- 你也可以利用 Lua 的动态特性,在游戏和场景运行时加载并执行新的代码。这个选项是迄今为止最复杂的,但具有能够在地图运行时对游戏进行修复的能力。缺点是它的实现和正确性很复杂,增加了出错的几率。此外,向正在运行的游戏发送数据的唯一方法是通过命令,当命令很长时就会出现问题。
对于 Gridlock Cluster 来说,第三种选择是通过一个名为热更新(Hotpatch,也称为服务器端多 Mod 场景)的场景来完成的。从概念上来说,热更新是一个非常酷的东西,它可以让你在游戏运行时加载类似 Mod 的代码,并且它会在一个模拟异星工厂 Mod 环境的环境中执行这些代码。但是在使用热更新的过程中存在着很大的问题:它的文档很差,使得它很难正确使用;它的实现不完整,而且存在 Bug;最麻烦的问题是,更新后的场景代码在启动时被当作长命令发送。这意味着,如果玩家在服务器启动并发送这些长命令更新场景时加入服务器,事情就会变得一团糟,这是导致 Gridlock 的服务器崩溃的众多方式之一。
虽然热更新的许多问题已经解决,但使用它的复杂性和困难给我上了宝贵的一课:拥有先进的能力,如能够在运行时修复代码,或任何形式的技术奇迹,并不总是证明这种先进系统所面临的复杂性和问题是合理的。当我试图修复热更新所导致的问题时,我亲身体验到了这一点:团队中的每个人(包括我自己)都在努力理解这个系统,以及如何解决它的问题。
出于这个原因,我决定用更简单的东西取代热更新在 Clusterio 中的功能:存档补丁。这是一个能力较差的解决方案,对代码的编写方式有更多的限制,但它工作方式的简单性足以弥补这一点。
打破一切
在我实施了保存补丁之后,很明显需要对代码进行一次大的改革。关于 Clusterio 的一个痛点是缺乏远程管理。如果你想启动作为集群一部分的异星工厂服务器,你必须登录到托管它的计算机,并通过你使用的进程管理器手动启动它,如果你想改变该服务器的任何设置,也是一样的。用这种方式管理集群是很痛苦的,这是在 Clusterio 60k 活动中得到的惨痛教训。
对于 Gridlock 来说,他们用 Pterodactyl 游戏服务器管理面板来远程管理服务器;一个好主意,结果却成为很多问题的原因。但这是另一个故事了。
在 Clusterio 中拥有远程管理异星工厂服务器的能力是一个众望所归的功能,并且曾经尝试过实现它。不过这些尝试更多的是事后的想法,由于代码的结构方式(每个 Node.js 进程运行一个异星工厂服务器),如果不对代码进行大修,不在这个过程中破坏一切,就很难实现任何合理的远程管理。
很自然的我选择打破一切,来实现远程管理。
Clusterio 2.0 的工作方式是,在你想要托管异星工厂服务器的每台计算机上都运行一个从进程。这些异星工厂服务器在 Clusterio 中被称为实例,从进程连接到主服务器,并监听创建和启动实例的命令。在一个从进程上可以同时运行多个实例,这意味着你只需要为每一台想要托管异星工厂服务器的计算机设置一个从进程,而且在这些计算机上只需要启动一个 Node.js 进程。
另一件不得不改变的事情是 Clusterio 在计算机之间的通信方式。在第 1 版中,这大部分是由主服务器托管一个 HTTP 服务器并响应其上的请求来处理的。这有一个问题,主服务器不能向其他计算机发送消息,只能响应其他计算机向它发送的请求,这就是 HTTP 的工作原理。为了解决这个问题,我用一个基于 WebSocket 使用 JSON 作为载荷的简单协议代替了 HTTP。WebSocket 与 HTTP 不同,它允许连接的双方随时向对方发送消息。
现在所有的东西都不能用了,这也算是成为了 2.0 开发的真正起点。在随后的几个月里,我利用这个机会重新开始了很多事情。
希望你喜欢这篇关于 Clusterio 2.0 发展的一瞥。正如你所想象的那样,在过去的 16 个月里,还有很多关于 2.0 的事情发生,足够我们写更多的文章来讨论这个问题。请注意,2.0 还没有准备好投入生产环境,不过如果你对开发感兴趣并想测试它,可以查看我们的 Discord 服务器和 GitHub 仓库。
可修改性:一个 Mod 的诞生 DedlySpyer
Kovarex 在 FFF-363 说的一些话让我印象深刻:
这是一个我不得不做的功能的例子,因为一旦我意识到这个功能可能存在,我就想使用它,并为它尚不存在的现状而烦恼。
This is an example of a feature, that I just HAD TO DO, because once I realised that the feature could be there, I was almost trying to use it and was annoyed by the fact that it wasn’t there.
— kovarex
我在异星工厂中沉浸了六年左右,但自从我开始做 Mod 的时候,我一直喜欢在游戏中修修补补。有时候,当我玩的时候,我最终会看到一些新的东西,只是让我有点困扰,而对于这些东西,却没有 Mod 来修复它。最后达到某个极限后,我就自己去修改它。通常情况下,这将导致我放弃我当前的异星工厂游戏,主要是因为,对我来说,修改游戏和实际玩游戏一样,爽到。
在1.0推出不久,我又发生了这种情况。我拿起最新版本的 Krastorio 2,到了能量装甲的阶段,我很奇怪为什么我不能旋转装备。当然,我可能可以在我的盔甲中仔细安排装备,但有时我只是想按 R
,然后用最小的努力把东西塞到地方。在 Mod 门户上的快速搜索让我看到了 GotLag 的可旋转电池(Rotatable Batteries);所以这是可能的,但它还没有被实现让所有的东西都可旋转。
让一个 Mod 在每一种情况下都能正常工作,有时是一件很麻烦的事情。处理每一种情况的安全方法是为每一种情况硬编码你的改动。这肯定会有效,但需要不断地监控。我在过去做过类似的事情,知道这可能会变得相当笨重和难以阅读。另外,如果那些其他的 Mod 改变了一些东西,我的实现要么直接破坏,要么与“支持的”Mod 不一致。所以,我最近变得很喜欢尝试让我的 Mod 尽可能的动态化以避免这种情况。理论上,这应该也能节省大量的时间,但这并不总能成功。
不过这种现实是我在异星工厂中喜欢的,那种”哦,但我需要做这件事”。如果只是通过在列表中添加一些字符串来增加另一个 Mod 的依赖性,那就不好玩了。所以,为了开始一个新的 Mod,目的是能够旋转 任何 装备,而不需要我不断地维护它,我需要了解异星工厂是如何加载 Mod 的。
在其他游戏中,如果你想添加 Mod,你会有某种形式的 Mod 顺序列表。你作为玩家,或者由 Mod 作者创建的程序,需要告诉游戏以什么顺序加载 Mod,以确保所有的东西都能很好的融合在一起而不至于爆炸。异星工厂通过 Mod 依赖关系来实现这个顺序,但它也更进一步。异星工厂不仅仅是按顺序加载一次所有的 Mod,它还会按顺序加载它们 三次。
三次?好像有点过分,对吧?其实,这是一个奇妙的想法。Wiki 对此有更详细的解释,但我在这里快速解释一下。每个 Mod,按照加载顺序,都有一个设置阶段,然后是数据阶段。设置阶段是相当自明的,数据阶段是原型数据,如物品、实体和配方。然后这个循环再重复两次。Mod 指定在循环的每一次迭代中要加载什么。Mod 制作惯例建议在这个过程中尽可能早地将原型全部加入。这样可以让想要隐性依赖其他 Mod 的 Mod 不需要知道它们的存在就可以做到。例如,异星工厂本体就为 Mod 流体的装桶做了这样的工作。
这就是社区里的大改 Mod 可以完全重做所有配方的原因,他们只是在后期数据阶段对游戏中的每一个配方进行了重做。不需要维护的 Mod 列表,没有“这个 Mod 需要 最后加载”。
这就是我如何让我的 Mod 能够旋转任何装备。我只需将我对需要轮换版本的装备的检查移到稍后的数据阶段,它就_应该_可以隐含地覆盖游戏中的任何装备。不需要我把 Mod X、Y 和 Z 命名为依赖关系,也不需要玩家在他们那边管理任何东西,它就能正常生效。不需要不断地管理名称的变化,除非有一个更复杂的问题,我会喜欢追踪。
抱着这样的想法,花费几天时间和一个玩一半的 Krastorio 存档,可旋转装备(Rotatable Equipment)就诞生了。
征稿
一如既往的,我们正在召集任何想要为 Alt-F4 做出贡献的人,无论是提交文章还是帮助翻译都可以。如果您有些有趣的想法,并乐于与社区分享,这里就是一个好地方。如果您没有太大把握,我们会很乐意帮助您讨论内容创意和结构问题。如果您有意参与,从加入 Discord 开始吧!
由于下周五恰逢圣诞节,我们决定在那一周暂停一期,这意味着本篇将是 Alt-F4 今年的最后一期!我们将在 1 月 1 日光荣回归,并在特别节目中回顾项目发展至今的情况,从各个团队成员的角度讲述他们的工作。应该会很有趣。