The 3 serialization mistakes that will break your mod
1. Storing variables in your scripts that can't be serialized
The most common - and easily avoidable - error is attempting to serialize a type that cannot be serialized. This has its own page in the official modding guide. If you define a variable in a script without using the local keyword - even if your definition is inside a function or other block - then it will be "global" to that script, and be permanently stored. If the player attempts to save while an unserializable type is stored in this way, the game will crash. Often the crash includes this specific, rather confusing error message:
Code: Select all
[string "Script.lua"]:0: attempt to concatenate field 'id' (a nil value)
stack traceback:
[string "Script.lua"]: in function 'saveValue'
[string "Script.lua"]: in function 'saveState'
[string "GameObject.lua"]: in function 'saveState'
[string "Map.lua"]: in function 'saveState'
[string "GameMode.lua"]: in function 'saveGame'
...
Other possible errors you can cause with this, that make a little more sense, include:
"cannot serialize table with metatable" - All of Grimrock's classes, such as Champion and Map, have metatables, and can't be saved.
"could not look up class name"
"cannot serialize variable of type X"
"cannot serialize a function with upvalues" - See the "What's an upvalue?" section below.
"unknown value type" - It's hard to get this one unless you try to get it on purpose, but I'm listing it for completeness.
As of version 2.2.4, the Grimrock editor will try to catch these errors whenever you run the dungeon preview. However, it can't catch 100% of problems. Always test saving and loading in your mods!
It's worth repeating that you only need to worry about this for non-local variables. Variables defined as local are temporary variables and Grimrock will never try to save them.
"What's an upvalue?"
In Lua, if you define a function that uses a local variable from an enclosing scope, that function will be a closure. For example, let's say you have this in a ScriptComponent:
Code: Select all
local message = "eating whole raw fish is fine"
function showMessage()
hudPrint(message)
end
If you want to store a closure in a ScriptComponent so you can use it later, too bad, you can't. You'll have to come up with a different way to implement whatever you're implementing.
2. Storing a function in multiple script environments when it needs one specific environment
There is another, easier-to-miss issue that can arise from serialization if you have references to a function in more than one place. For example, if you have two script entities, script_entity_1 and script_entity_2:
Code: Select all
--Script 1
function abc() print("stuff") end
Code: Select all
-- Script 2
def = script_entity_1.script.abc
So if the function in script_entity_1 references any variables that belong to script_entity_1, it could break if the game is saved and reloaded; if its environment changes to script_entity_2's ScriptComponent, it won't be able to find the variables. If you're lucky it'll break in a way that causes a crash.
To keep your code safe from this problem, just follow these two rules:
1. If you want a function to use variables other than its own parameters and completely global variables, treat it similar to you would treat an unserializable item: except for its original, permanent definition, don't store any permanent references to it. Temporary local references are still fine, obviously.
2. If you want to store references to a function in more than one place, only reference the global environment in it. If you really need the function to reference variables in a specific ScriptComponent, write it like this:
Code: Select all
function example()
local varFromGlobalEnvironment = script_entity_1.script.var
end
You may have noticed that this behaviour can also be used to detect when the player reloads the game. However, there's a better method explained in section 3 below.
3. Not knowing what minimalSaveState does, why it's important, and which objects have it
Objects defined as having "minimalSaveState = true" will have only minimal properties saved: their name, id, x, y, elevation, and facing. When the game is loaded, they are simply re-spawned at that position with that name and id, in their default state; any changes that were made to them are lost. Because the object is spawned again, all its components are created again, meaning that all their onInit hooks will run again.
Objects that don't have minimalSaveState will have their entire state saved, including all their components and the state of all their components, so that they are preserved exactly across save/load even if changes have been made to them. Their components' onInit hooks will not run again.
minimalSaveState should be used on objects such as walls and floors that won't change during gameplay. This is important because if you don't use minimalSaveState on these objects, the game will have to save the full state of every single instance of them - which could be tens of thousands of objects. The game can easily run out of memory when saving in this case, and even if it doesn't, it will take an unnecessarily long time.
If you are changing the object in any way - using setWorldPosition, setWorldRotation, doing anything to its components such as enabling/disabling them (even in the editor), setting wall text, etc. - and the object has minimalSaveState, then these changes won't be saved, and your mod will 100% catastrophically break as soon as the player saves and loads the game. I cannot stress this enough. Tons of released Grimrock 2 mods are broken because of their authors not knowing this. Don't become one of them!
The asset pack is a good guide for this: note how objects like trees, floors, etc. that are used frequently and will never change state have minimalSaveState (if they didn't saving would be too expensive), but objects like altars and monsters don't have minimalSaveState because their state can change during gameplay.
Before you use an object, look at its definition and especially whether it has minimalSaveState or not, so that you don't make the mistake of trying to change the state of a minimalSaveState object. Remember that if an object's base_object has minimalSaveState, then that object has minimalSaveState too, inherited from the base_object!
Finally, minimalSaveState offers an easy way to detect when a player reloads the game: define a minimalSaveState object that has a component with an onInit hook and place one instance of it anywhere in your dungeon. Then, whenever that onInit hook runs, you know that the player just reloaded or just started the game.