切换语言:

模组开发教程

From Official Factorio Wiki
Jump to navigation Jump to search


这是一个异星工厂0.15版本的模组开发教程。在这篇教程中,作者将会讲解幕后游戏运作的原理,修改异星工厂的方法,相关文档的位置,还有相关理论概念。

概览

开始教程之前,有几件事需要注意:

绿色背景的代码应被包含在本教程即将创建的模组中,如果读者遵循教程指示的话。
读者最好复制粘贴此代码,以确保复现一致的效果。

当新的代码被添加进模组时,包含对应文件名的Lua注释会写在这段代码的开头,代码应写入此文件中。
例如:
--control.lua
紫色背景的代码不应该加入我们要做的模组,这段代码仅仅有教学或举例的目的,以促进理解。

这个教程为0.15版本而写,所以未来的读者需要注意细节上的变化,并且应查看此后的更新日志。

模组开发中的术语

开始我们的教程之前,应先了解一下一些术语和定义。

模组
通过API(应用开发接口)修改游戏的一些脚本。
实体
在异星工厂脚本里,只有四种东西:实体,概念,事件,地块。实体的例子有:角色,组装机,虫子,等等。固定的机器和可以移动的人和虫子,都是实体。
角色
异星工厂世界中,玩家操纵的实体。
玩家
定义玩家的所有信息的集合。包括用户名、玩家表(译注:此处的“表”指Lua语言的一种数据结构,后文中许多涉及数据的表也是这个意思)中的位置等等。玩家包含角色,但角色不内含玩家。
原型
一个原型描述一类的实体,就像一个模板。其中定义了属性等。原型用于创建实体,许多功能相同的实体都会用到同一个原型。
地表
地表,就像一个二维平面。它由地形(例如草地、沙漠和水)以及地面上的所有实体组成。默认情况下,游戏中只有一个地表,内部以“nauvis”命名,或 game.surfaces[1] ,但是模组可以通过API创建更多的地表。
事件
“事件”就是由游戏本身不断触发的事件。模组可以在一些事件上添加回调函数,例如 on_entity_died 等等。在 Control脚本 部分有更多说明。
配方
和原型相似,一种数据结构,定义一种可以被合成系统处理的配方。
科技
类似配方,包含这个科技的前置科技,研究要求,解锁内容等信息。

教程以后还会介绍更多术语。

开始制作模组之前

我们开始制作模组之前,我们需要理解异星工厂是什么。你可能会用关于页面作为答案,但这是以玩家的角度来回答的。 既然我们要成为一个模组开发者,我们需要更详细的解释。 异星工厂内核用C++语言开发,由Wube(异星工厂开发团队)提供用Lua语言编写的API来修改游戏机制。 这个API允许添加脚本到游戏的初始化程序中,来修改游戏,同时避免暴露游戏源码或涉及内存修改。 这和其他的游戏提供模组制作的方式不同,这是一种更专业、更合适的支持模组开发的方式。

为了帮助大家使用这些API,开发团队热心地提供了相当全面的API文档。 我们要习惯使用这个页面,因为你制作模组时会经常访问这个页面。 这个文档包含了关于概念事件,诸多有用信息。 你将会经常需要访问这个页面,我建议你把这个页面添加到收藏夹。 除了这个页面,异星工厂社区也提供了很多有用资源,比如这篇教程。

准备

开发模组的最好方式,是在开发时有一个环境进行测试,也就是使用有一个纯净的Factorio 0.15版本,教程还会详细讲解。此外,推荐使用一个支持Lua语言的文本编辑器,而不是记事本什么的。可以用Emacs,Vim,Sublime Text,和Notepad++(译注:比较出名的代码编辑器还有微软的 Visual Studio Code 和 Github 的 Atom )。作者推荐Emacs,但用什么编辑器不影响模组本身。

异星工厂是如何加载模组的

Data 阶段

当异星工厂最早初始化时,它仅初始化自身一部分,然后开始寻找模组。 游戏先根据依赖关系,然后是字母序,来加载模组。 这一点非常重要,因为如果你在忽视它的情况下添加模组间的交互,可能会引发问题。

游戏内有两种模组间的依赖,必要依赖和可选依赖。 必要依赖将总是提前被加载。如果定义了必要依赖,但缺少对应模组,游戏将无法初始化。 可选依赖也将被提前加载,但缺少也没有关系。 这个功能对可以协作的模组很有帮助,例如开启额外的功能。 必要依赖应该用来作为模组代码库,以及类似架构。

这是异星工厂初始化时限制最大的一个阶段,你除了声明科技和实体等的原型外,基本做不了什么。操作文件、影响世界等功能都无法使用。 事实上,此lua环节(session)结束后,函数调用和修改将被丢弃。 你也不能操作数据表,否则会报错或被忽略。 使用 data:extend({}) 这样的代码,来向游戏中添加修改时,它需求一个特定的格式,之后会详细谈及。

进行此阶段时,游戏搜寻所有模组的 data.lua 文件并执行,接着是 data-updates.lua ,最后是 data-final-fixes.lua 。 其它应被加载的文件需要在代码中进行导入,之后会详细谈及如何导入文件。 此阶段执行的所有文件应当仅包含原型定义及产生原型定义的代码。

Migration 阶段

Migration(迁移) 指,模组更新后,用来“修复”存档的脚本。 每当模组修改了原型,必须要进行迁移,来修正世界中所有依据该原型创建的实体。 此项工作必须对所有更新过的实体进行,否则未修正的实体将从世界中被移除,是很业余的表现,会导致用户讨厌你。 虽然本文不准备讨论迁移,但你在社区和API站点中可以找到许多有关迁移的资料。

为了避免需要写迁移代码的情况,就需要避免修改原型的名字、类型、配方和科技。 这些事物不能在运行时被修改,你还可能需要重置科技树或配方。 在公开发布模组后,尽量避免这类修改。尽可能想出一个能作为模组基础的原型的最终版本。 当然,如果用户在模组每次更新时,都生成新地图的话,就不需要进行迁移了,但不要指望社区会这么做。

Control 阶段

大多数模组都包含一个名为 control.lua 的文件。 这个文件含有能让模组在游戏中做一些事的脚本,而不是单纯地往游戏中添加实体。 在此阶段中,每个模组的 control.lua 都在各自的lua实例中被运行(这意味着未额外配置则没有模组间的互通),所拥有的lua实例持续整场游戏直至结束。 因为每次存档或读挡时都会发生上述过程,你不必重启游戏来查看 control.lua 的更改。简单地开始新游戏或读档便会重新运行此阶段。 此阶段还有一些其它的注意事项,阅览API站点上的数据生命周期来获得更好的概览。

Runtime 阶段

此阶段中,模组已准备完毕,存档已运行。游戏所提供的全部表已经可以由事件处理器访问。(下文会详细谈及这些)

异星工厂模组的主要组成

对于一般模组,有几个基本组件使其能正常运作。

定义新实体的模组需要在 data.luadata-updates.luadata-final-fixes.lua这三个文件,以及由这其 require 而导入的文件中,声明这些实体。

对游戏时有影响的模组还需要一个 control.lua 文件,来添加脚本。

支持用户自定义设置的模组,需要 settings.lua 来描述这些设定。

对游戏元素定义了可读名称的模组,可以提供一个 locale 文件夹和以语种命名的子文件夹,在其中放入名称、描述等的翻译文本。

本教程编写的模组,data.lua 和 control.lua 两者都将包含,以帮助你熟悉它们。

随着时间的推移,社区定下了一些有关 模组文件夹结构 的惯例。没有必要完全照做,但这样会简化一些事情,以及方便与其它开发者交流。下文会详细谈及文件夹结构。

开始编写模组

终于到了你期待已久的环节,让我们开始制作你的第一个模组。 你需要:

  • 安装最新版的异星工厂(文章编写时为0.15)
  • 文本编辑器,例如 Emacs、Vim、Sublime text 等等。
  • 对前文的理解
  • 了解Lua语言,知道语法和工作原理就足够了。如果你已有编程经验,现学应该也不成问题。

以上全部条件都满足后,我们就可以正式开始了。

对于这个模组,我们将创造一套能在你跑步时生成火焰的护甲,对路径上的单位造成伤害。 它将完全免疫火焰伤害,但是比起重型护甲对物理伤害更脆弱,是一种风筝用法的护甲。

创建文件夹目录结构

就像本教程之前所说的那样,有一个所谓社区标准,左右模组的目录结构。它和游戏本身所要求的目录结构相结合,影响了我们应该如何布置目录。首先,在 user data directory/mods 目录下新建一个文件夹。将这个文件夹命名为 FireArmor_0.1.0。完成后,模组文件目录看起来应该是这个样子:

🗁(用户数据目录,一般是“.factorio”)

↳ 🗁 mods
↳ 🗀 FireArmor_0.1.0

然后,在 FireArmor_0.1.0 文件夹中,新建两个新文件,info.jsondata.lua。目录现在看起来应该是这样:

🗁(用户数据目录,一般是“.factorio”)

↳ 🗁 mods
↳ 🗁 FireArmor_0.1.0
↳ 🗋 data.lua
↳ 🗋 info.json

info.json 文件

接下来,复制下面代码并粘贴到 info.json 文件中:

{
    "name": "FireArmor",
    "version": "0.1.0",
    "title": "Fire Armor",
    "author": "You",
    "contact": "",
    "homepage": "",
    "factorio_version": "0.15",
    "dependencies": ["base >= 0.15"],
    "description": "This mod adds in fire armor that leaves behind damaging fire as you walk around."
}

这段代码中每一项的解释:

条目 说明
name 这是模组的内部名称,用于在代码中识别你的模组.
version 你的模组的版本号。你可以用任意方式表示,只要是数字即可。一些模组以 0.0.1 或 0.1.0 作为初始版本号。一些模组以异星工厂的版本号为基准例如 0.15.0 (对应异星工厂的 0.15.X 版本)。
title 你的模组的展示名,将被用于游戏内的模组面板中显示,以及在 Mod portal 中的显示名。
author 你的昵称!你可以在前面的代码中修改它。
contact 联系方式,如果有人遇到问题可以通过它联系你。
homepage 把你模组的主页写在这里,如果有的话。
factorio_version 这一项告诉游戏这个模组对应的游戏版本是多少,其必须符合你的模组所应用的游戏版本,这个教程中是0.15。
dependencies 模组的依赖。这里需要以某种形式将“base”作为依赖,以让游戏内置的“base”模组首先加载。
description 对你的模组进行一段简短的描述。

这就是 info.json 的全部了!接下来,在 data.lua 文件中:

--data.lua

require("prototypes.item")

It's a pretty simple file, all we're doing here is just telling the game to execute the file called item.lua in prototypes, which we're about to create. Create a folder in FireArmor_0.1.0 called prototypes, then inside prototypes, create a file called item.lua. Your mod directory should now match this github snapshot.

Notice how our earlier require used the folder and file name in it?

Prototype creation

Now, there are two ways to create prototypes in Factorio. There's the short way, and the long way. The long way requires copying an existing definition from one of the default lua files provided with an install of Factorio, and the short way just uses a lua function to copy and modify a definition. For the sake of this tutorial, we'll do it the short way.

In item.lua, copy and paste the following:

--item.lua

local fireArmor = table.deepcopy(data.raw.armor["heavy-armor"])

fireArmor.name = "fire-armor"
fireArmor.icons= {
   {
      icon=fireArmor.icon,
      tint={r=1,g=0,b=0,a=0.3}
   },
}

fireArmor.resistances = {
   {
      type = "physical",
      decrease = 6,
      percent = 10
   },
   {
      type = "explosion",
      decrease = 10,
      percent = 30
   },
   {
      type = "acid",
      decrease = 5,
      percent = 30
   },
   {
      type = "fire",
      decrease = 0,
      percent = 100
   },
}

local recipe = table.deepcopy(data.raw.recipe["heavy-armor"])
recipe.enabled = true
recipe.ingredients = {{"copper-plate",200},{"steel-plate",50}}
recipe.result = "fire-armor"

data:extend{fireArmor,recipe}

What we've just done here is we've copied the definition of heavy armor, then changed it's properties, and injected it into the Factorio init with data:extend. The first line of code is probably the most interesting. table.deepcopy copies a table fully into another table. We do this from data.raw. The data part is a table, which will be used by game to setup the Factorio universe. In fact, it contains the function extend(self,prototypes) and a table called raw. The former is customary way to add new stuff to the latter. It is actually data.raw that holds the prototypes for the game. (You can view the implementation in the file /factorio/data/core/lualib/dataloader.lua). It is important to note that data.raw only exists during the data loading stage of the game. During the control stage, when the game is running and being played, you cannot read this data; instead you read processed values through the API from the various types like LuaEntityPrototype.

In addition to defining the item prototype, we also define a recipe for it. This is necessary if you want to be able to craft the thing. We also set it to enabled so it doesn't need a technology to unlock.

At this point, the mod looks like this.

More on data.raw

When Factorio initializes, all prototypes are put into a table called data.raw. This table holds all types, and within those types, individual entities. You saw earlier how we deepcopied from the definition of heavy armor, and modified some fields. In fact, let's go over each part of the deepcopy line:

local fireArmor = table.deepcopy(data.raw.armor["heavy-armor"])

We assign a variable called fireArmor that holds our copy of the heavy armor definition. Notice how in data.raw, there is a type table that holds all armors, and the specific armor we're looking for is called heavy-armor. For example, the player's prototype would be:

data.raw.player["player"]

Because the player is the player, his type matches his name. You could define a new type of player with a mod. You can see all the prototype fields for an entity in it's long declaration in the Factorio install, at (Install)/data/base/prototypes.

You may be thinking at this point, "Can I modify Factorio's existing prototypes without making new ones?" Well, the answer is yes! You would simply access the data.raw table during init, in data-final-fixes.lua, and change a property. For example, make the iron chest instead have 1000 health:

data.raw.container['iron-chest'].max_health = 1000

The reason why this code must be in data-final-fixes.lua or data-updates.lua is because that is the last file run, after all mod files have been run. This prevents (to a degree) your changes from being messed with by other mods. Of course, it is still possible to have incompatibilities. You should note any that you know of in your mod's description. Again, the dev's documentation on this should be looked at.

This can also be applied to other mods, not just Factorio's base. You could mod a mod, as long as you add the mod (that you modified with your mod) to your dependencies so it gets loaded first.

The control scripting

And now, to finalize the mod, we have to make it be more than just simple armor. Let's think about what we want the armor to do. We want the armor to periodically create fire on the ground as we walk with the armor on. The event we're going to use is called on_tick, since we want the fire to be periodically created.

In our mod folder, create a file called control.lua. The game will automatically execute this file, so requiring it in data.lua is not necessary.

Inside control.lua, copy and paste the following:

--control.lua

script.on_event({defines.events.on_tick},
   function (e)
      if e.tick % 60 == 0 then --common trick to reduce how often this runs, we don't want it running every tick, just 1/second
         for index,player in pairs(game.connected_players) do  --loop through all online players on the server
            
            --if they're wearing our armor
            if player.character and player.get_inventory(defines.inventory.player_armor).get_item_count("fire-armor") >= 1 then
               --create the fire where they're standing
               player.surface.create_entity{name="fire-flame", position=player.position, force="neutral"} 
            end
         end
      end
   end
)

I've used lua comments in the code above to explain each step. It's fairly easy to understand, and it shows how you would get the current armor that the player is wearing, with defines.inventory.player_armor, which is an inventory constant. You can read the list of defines here.

At this point, the mod will look like this.

Locale

If you've already tried loading up Factorio and trying the mod so far (which you can at this point without it crashing), you may have noticed that the item name of the armor says "Unknown key". This means that Factorio has the internal name, but it doesn't know what it should look like to the user. So, we need to create locale for our mod.

In the mod folder, create a folder called locale, then create another folder inside that called en, then a file called config.cfg.

If you know another language, you can also translate your mod by making other language code files inside locale, such as de for German.

Inside config.cfg, paste the following:


[item-name]
fire-armor=Fire armor

[item-description]
fire-armor=An armor that seems to catch the ground itself on fire when you take a step. It's warm to the touch.

Notice how this is not a lua file. Locale is handled with C config files, so the format is different.

Finally, the mod will look like this.

The finished tutorial mod

Well, the mod is finished. Since this mod is only a tutorial, there isn't much balance to it. Additionally, don't try submitting it to the mod portal as your own, since it's from the Wiki.

However, you're free to take this mod and modify it for your own use, changing recipes, adding technologies, whatever.

Resolving common errors in modding

As you continue to write mods from scratch instead of from a tutorial, you may encounter the infamous error. There are several types of errors that you can encounter in modding Factorio, and knowing how to deal with these errors will allow you to continue working.

Syntax errors

The lua programming language expects things to be laid out a certain way. If you miss a bracket, = sign, or dot, you will encounter a syntax error. As an example, see the error below:

Failed to load mods: __FireArmor__/data.lua:1:__FireArmor__/prototypes/item.lua:36: syntax error near 'true'

As of version 0.15, you'll see an error like the one above whenever you make a syntax error within the prototype definitions. The game will offer to restart, disable the troubling mod, disable all mods, or exit. Let's dissect the error, shall we?

Right away, we see the reason why Factorio didn't start normally. "Failed to load mods:". So, we know that it's a mod that messed up, and by extension, we know it's our mod. Whenever the lua engine of Factorio has a syntax error, it will print a mini stack-trace that follows through all requires, listing the call order. First, we see that the problem was indirectly caused by line 1 of data.lua. There's no problem there, so it must be the next entry, line 36 of prototypes/item.lua. After stating where it is line-wise, it will attempt to give you an estimate of where in the line the problem is. Don't trust this estimate, only roughly trust the line number, plus or minus a few lines.

Going to line 36 of item.lua, we find:

recipe.enabled true

Hmm, that doesn't look right. Can you see what's missing? We left off an = between enabled and true. Thus, syntax error. Fixing these can be difficult for new programmers, who don't know what to look for.

Illogical actions, indexing nil

In lua, "nothing" is defined as the keyword nil. This is similar to null in other programming languages. Whenever the programmer tries to access something in a table that is nil, they will get an error like the following:

Error while running event FireArmor::on_tick (ID 0)
__FireArmor__/control.lua:3: attempt to index field '?' (a nil value)

The "attempt to index field ..." error is often caused by the modder making an assumption that didn't work out. These types of errors will always be identifiable by their signature line, "attempt to index field". If we look at line 3 of control.lua (where the error is), we see:

game.print(game.players[23])

What assumption has the modder made here? Well, there's actually two problems with this line. The first thing is that the modder has assumed that game.players[23] is a valid player, which isn't the case; this is why we get the "index field '?'" bit. The game doesn't know what the field is that we tried to index, because it hasn't been created yet. These errors are difficult to debug unless you know the ins and outs of the modding API well.

The second issue is a lot more subtle, and won't work. The modder is attempting to print a userdata table. A player is a table of several values. Trying to print it will error, instead a function to print it is needed.

Error while running event

Another common type of error in Factorio is the "Error while running event" error. This type of error only happens in control.lua scripting, and it happens when something goes wrong in an event function, such as a syntax error. Note that syntax errors in control.lua do not stop the game from starting, but may trigger after a save is loaded. There are a great deal of errors under this broad category, here's an example:


Error while running event FireArmor::on_tick (ID 0)
Unknown entity name: fire-flam
stack traceback:
__FireArmor__/control.lua:6: in function <__FireArmor__/control.lua:2>

As you saw with the prototypes syntax error, Factorio gives a small traceback and the error name itself. In this case, we've attempted to spawn an entity called "fire-flam" on line 6 of control.lua, inside of an on_tick event hook. Fire-flam isn't a real entity type, so we crashed.

These types of errors can range from being a simple fix (like the one above, add the missing e), or can be very difficult.

Internal errors

The most rare form of error and the worst form is the internal error. This is an error with the C++ code of the game, and there's nothing you can do but report it to the devs. Mods occasionally cause these, and almost all of them are considered bugs, as mods should not be able to cause these, if that makes sense. They often get thrown into the logs.

An example:

696.148 Error FlowStatistics.cpp:236: FlowStatistics attempted to save value larger than uint16 as uint16. Exiting to prevent save corruption.
Logger::writeStacktrace skipped.
696.148 Error CrashHandler.cpp:106: Map tick at moment of crash: 432029
696.148 Error Util.cpp:76: Unexpected error occurred. If you're running the latest version of the game you can help us solve the problem by posting the contents of the log file on the Factorio forums.

Multiplayer and desyncs

The reader may be wondering at this point how Factorio handles multiplayer with mods. It's fairly simple, but is still worth considering.

Factorio is deterministic, which means that when you provide a constant input, you get a constant output, with no variance. Every client and the server all reach the same points at the same time in simulation, so they all agree on what happened. When this differs, the players experience a desync.

Desync
Misalignment with server and clients. Client 1 expected A, but got B. All other clients got A. Thus, Client 1 will desync. Desync can also happen when all clients have information (for example a variable) but a client that recently joined the game doesn't. That client will be desynced.

Desyncs happen a lot to new devs of Factorio mods, because they are unaware that a particular piece of code they used causes desyncs. As a general rule, there are a few things that should never be done.

Use local variables that are not final outside of event hooks

local globalLocal = 1
script.on_event(defines.events.on_player_built_item, function()
    globalLocal = math.random()
end)


If the modder places a local variable outside of an event hook that gets changed during runtime, desyncs will happen. If making a "global" variable is necessary, place the variable in the global table instead. The game syncs this table between all clients, so they can all be aware of and reach the same conclusion as each other.

Selective requiring

if setting1 then
   require("settingOne.lua")
end

Selective requiring, aka requiring different lua files based on settings or other criteria will also cause desyncs, and in some cases can cause connection rejections as the checksum of the mods will not match, as they load different data. All clients' mods must require the same series of files.

Conditional event subscribing

Mods in factorio may subscribe to events in order to be notified when they happen. This allows mods to react to events when they occur. Typically, event subscription is done at the top level of a lua file.

Doing event subscription inside of a conditional, function, or other event is dangerous, as doing it incorrectly will lead to desyncs. Basically, since both the server and client need to reach the same conclusion after running code, conditional subscription can lead to certain clients or the server being subscribed to an event when the others are not, causing desyncs.


Improper use of on_load

Another way to cause desyncs is to make improper actions inside of an on_load call, which some players new to modding might try to do. According to the documentation, the on_load functionality is meant for 3 purposes only:

  • Re-register conditional event handlers
  • Re-setup meta tables
  • Create local references to tables stored in the global table

Doing anything else will cause desyncs. The game will catch most attempts, crashing instead and terminating the mod.

Extended learning

One of the best ways to learn how to mod beyond this is to look at other mods. As all mods can be opened and looked at, looking at the mods of experienced modders can help significantly when making your own mod.

Keeping your mod working

As Factorio evolves, things will change. Previously, you probably ignored the modding part of the changelog, you now need to read it and see if any changes affect your mod(s). If so, you'll need to fix them. If there's something wrong with your mod, the game will fail to init and explain why.

See also