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 is has its own page in the official modding guide, so there's really not much excuse for getting it wrong. 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. The most common error message to result from this is:
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 include:
"cannot serialize table with metatable" - probably a reference to an instance of a class (like Champion)
"could not look up class name" - same as above
"cannot serialize variable of type X" - self-explanatory
"cannot serialize a function with upvalues" - generally this means you used the "local" keyword on a permanent variable which turns it into an upvalue; in Grimrock "local" may only be used for true temporary variables. See Addendum: Upvalues.
"unknown value type" - If you manage to get this error to appear at all, you should already know what it means.
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!
2. Referencing a function that needs an environment, in multiple environments
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 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. The details of that are left as an exercise for the reader. (It is not a very good way to do it, however; for a better method, see minimalSaveState below.)
I mentioned that this also happens for tables. However, the environment of a table is only relevant if there is a function inside the table that requires it. This is not something you are likely to encounter.
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. Almost all released Grimrock 2 mods are broken because of their authors ignoring this simple fact. Don't become one of those authors!
If you're still confused, look at the asset pack and use it as a guide: note how objects like trees, floors, etc. that will never change state have minimalSaveState (if they didn't saving would be too expensive), and note how objects like altars and monsters don't have minimalSaveState because their state can change during gameplay.
Most importantly, 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, or the mistake of placing thousands of instances of an object that lacks minimalSaveState. 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. This is useful when you are manipulating sounds and music.
As the official page says, upvalues cannot be serialized, and if the saving code reaches an upvalue, it will throw an error. The implication of this is that you should ensure any local variable is not used after its block exits. In other words, you need to avoid constructs like:
Code: Select all
function makePrinter(str) return function() print(str) end end
As long as you use local and function parameters only for actual local variables, and not for upvalues, you don't need to worry about this. Note that I can only think of two situations in either Grimrock game where the user can create an upvalue:
1. using the local keyword inside a ScriptComponent, but not inside any block. This is the example given on the official page. There is no good reason to use local here in the first place, so just don't do it.
2. a block that creates a function that uses a local variable from inside the block, like the example I gave above. There is no good reason to do this in Grimrock mods, either.