Difference between revisions of "Surviving a reload"
Zonkmachine (talk | contribs) (→Complete module: Fix link) |
Zonkmachine (talk | contribs) m (fixup - layout) |
||
| (9 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
| − | When a game is saved, a script is responsible for informing Pioneer exactly which of its data | + | When a game is saved, a script is responsible for informing Pioneer exactly which of its data needs to be saved. If nothing is specified, nothing will be saved at all. Similarly, after a game is loaded, a script is responsible for restoring all of its saved data; re-creating bulletin board adverts, the player's mission details, and any run-time state. |
Here is a simple form. If a new game is started, you will see this on every bulletin board that you visit: | Here is a simple form. If a new game is started, you will see this on every bulletin board that you visit: | ||
| Line 16: | Line 16: | ||
Event.Register("onCreateBB", onCreateBB) | Event.Register("onCreateBB", onCreateBB) | ||
| − | + | However, if you save the game, then reload, you will not see it on any bulletin board that had been created in the current system before you saved. onCreateBB will be triggered for any subsequently created bulletin boards, but not for those that already existed. It isn't appropriate to have scripts create new adverts just because a player has loaded; instead, it's the responsibility of the script to tell Pioneer what to save, and to use those saved data to restore all adverts after the game is loaded. The same goes for player missions, and any working data that the script is using. | |
| − | There are two things we need to do to achieve all of this | + | There are two things we need to do to achieve all of this: |
* We need to track everything that the script is doing. | * We need to track everything that the script is doing. | ||
* We need to be able to pack this away for saving, and bring it back after loading. | * We need to be able to pack this away for saving, and bring it back after loading. | ||
| + | |||
==Tracking everything that we are doing== | ==Tracking everything that we are doing== | ||
| Line 74: | Line 75: | ||
==Tracking missions== | ==Tracking missions== | ||
| − | This is less of a challenge. | + | This is less of a challenge. Missions created with <code>Mission.New()</code> are stored in the <code>PersistentCharacters.player.missions</code> table. You also need to register them locally in your script. When <code>Mission.New()</code> is called, it returns a reference to the mission. That reference can be stored in a file-local table that can be iterated through when we need to access a mission. We can wrap these functions up: |
| − | local | + | local missions = {} |
| − | local | + | local addMission = function(mission) |
| − | table.insert( | + | table.insert(missions,Mission.New(mission)) |
| − | |||
end | end | ||
| − | local | + | local removeMission = function(mref) |
| − | for i, | + | local ref |
| − | if | + | for i,v in pairs(missions) do |
| − | + | if v == mission then | |
| + | ref = i | ||
| + | break | ||
end | end | ||
end | end | ||
| − | + | mission:Remove() | |
| + | missions[ref] = nil | ||
end | end | ||
| − | Now, instead of calling <code> | + | Now, instead of directly calling <code>Mission.Add()</code> and <code>mission:Remove()</code>, we call our local functions, <code>addMission()</code> and <code>removeMission()</code>. They will send their argument on to the instance in '''Mission.lua''', and also store or remove the mission ref in the missions table. There is now a record of which missions belong to this script. <code>addMission()</code> and <code>removeMission()</code> in the example above is taken from '''SearchRescue.lua'''. Look how <code>removeMission()</code> is used in the '''[https://github.com/pioneerspacesim/pioneer/blob/646f13acb76ab6ff3e407258c598e9e4012f5008/data/modules/SearchRescue/SearchRescue.lua#L241-L252 Search and Rescue module]'''. |
==Serializing: Packing away for saving and loading== | ==Serializing: Packing away for saving and loading== | ||
| − | The <code>Serializer</code> object provides one method: <code>Register</code>. It takes three arguments. The first is a name; a simple string with which to uniquely identify this script. It's probably sensible to re-use the name that was used for the <code>Translate</code> object's flavour methods. The second | + | The <code>Serializer</code> object provides one method: <code>Register</code>. It takes three arguments. The first is a name; a simple string with which to uniquely identify this script. It's probably sensible to re-use the name that was used for the <code>Translate</code> object's flavour methods. The second and third arguments are both names of functions that each returns a single table. The second argument is your '''serializer''' function and the third is your '''unserializer''' function. By convention, we name these <code>serialize</code> and <code>unserialize</code>. |
| − | |||
| − | The second argument is your '''serializer''' function | ||
| − | <code>serialize</code> must return a table | + | <code>serialize</code> must return a table, and this table must contain everything that you need to store to get your script working after a reload. It will be run when the player saves the game. The table can contain any pure-lua types, and SystemPath, Body and SceneGraph.ModelSkin core types, anything else will explode, like a ShipDef/EquipDef or a StarSystem. |
<code>unserialize</code> accepts a table, and does something with it. It will be run by the serializer after the game is loaded, immediately before the <code>onGameStart</code> event is triggered. It is passed the data that serialize returned at save time. | <code>unserialize</code> accepts a table, and does something with it. It will be run by the serializer after the game is loaded, immediately before the <code>onGameStart</code> event is triggered. It is passed the data that serialize returned at save time. | ||
| Line 150: | Line 151: | ||
local onDelete = function (ref) | local onDelete = function (ref) | ||
| − | + | ads[ref] = nil | |
end | end | ||
| Line 156: | Line 157: | ||
local onCreateBB = function (station) | local onCreateBB = function (station) | ||
| − | + | local ad = { | |
| − | + | headline = 'Click here for a greeting', | |
| − | + | bodytext = "Hello!", | |
| − | + | station = station | |
| − | + | } | |
| − | + | -- create one per BBS | |
| − | + | local ref = station:AddAdvert({ | |
| − | + | description = ad.headline, | |
| − | + | onChat = onChat, | |
| − | + | onDelete = onDelete} | |
) | ) | ||
ads[ref] = ad | ads[ref] = ad | ||
| Line 174: | Line 175: | ||
local onGameStart = function () | local onGameStart = function () | ||
| − | + | ads = {} | |
| − | + | if not loaded_data or not loaded_data.ads then return end | |
| − | + | for k,ad in pairs(loaded_data.ads or {}) do | |
| − | + | local ref = ad.station:AddAdvert({ | |
| − | + | description = ad.headline, | |
| − | + | onChat = onChat, | |
| − | + | onDelete = onDelete}) | |
| − | + | ads[ref] = ad | |
| − | + | end | |
| − | + | loaded_data = nil | |
end | end | ||
local serialize = function () | local serialize = function () | ||
| − | + | return { ads = ads } | |
end | end | ||
local unserialize = function (data) | local unserialize = function (data) | ||
| − | + | loaded_data = data | |
end | end | ||
| Line 206: | Line 207: | ||
Although the example is functional, it has been coded with brevity alone in mind. I would not recommend using this as the basis for a mission without at least completely rewriting <code>onChat()</code>, and possibly tidying up the temporary loop variable names. | Although the example is functional, it has been coded with brevity alone in mind. I would not recommend using this as the basis for a mission without at least completely rewriting <code>onChat()</code>, and possibly tidying up the temporary loop variable names. | ||
| + | |||
| + | ==Don't serialize strings== | ||
| + | |||
| + | When we add the strings literally to the ad as we did in the example above, they will be serialized and saved on game end. Considering this is happening on all active stations, this will waste disk space. | ||
| + | |||
| + | local ad = { | ||
| + | headline = 'Click here for a greeting', | ||
| + | bodytext = 'Hello!', | ||
| + | station = station | ||
| + | } | ||
| + | |||
| + | What we want to do instead is save the information to recreate the same strings. If for instance we have a set of flavours that are selected with a random number, then generate that number and save it in a variable. The ad example above can then be rewritten as such. We only need the station and the flavour number to recreate the ad. | ||
| + | |||
| + | local ad = { | ||
| + | station = station, | ||
| + | n = n | ||
| + | } | ||
| + | |||
| + | The working example in the paragraph above can be updated in this way. When you have the translated strings in a separate module you would start the script by pulling in the strings to the script in an array or set of arrays. In the following example we instead add the strings to an array directly but it shows the general idea. | ||
| + | |||
| + | <pre> | ||
| + | local Engine = require 'Engine' | ||
| + | local Event = require 'Event' | ||
| + | local Game = require 'Game' | ||
| + | local Rand = require 'Rand' | ||
| + | local Serializer = require 'Serializer' | ||
| + | |||
| + | local ads = {} | ||
| + | |||
| + | local flavours = { | ||
| + | {flavour = 'flavour 1', title = 'title 1', text = 'text 1'}, | ||
| + | {flavour = 'flavour 2', title = 'title 2', text = 'text 2'}, | ||
| + | {flavour = 'flavour 3', title = 'title 3', text = 'text 3'} | ||
| + | } | ||
| + | local onChat = function (form, ref, option) | ||
| + | local ad = ads[ref] | ||
| + | form:Clear() | ||
| + | form:SetMessage(flavours[ad.n].text) | ||
| + | end | ||
| + | |||
| + | local onDelete = function (ref) | ||
| + | ads[ref] = nil | ||
| + | end | ||
| + | |||
| + | -- when we enter a system the BBS is created and this function is called | ||
| + | local onCreateBB = function (station) | ||
| + | |||
| + | local rand = Rand.New(Game.time) | ||
| + | local n = rand:Integer(1, #flavours) | ||
| + | |||
| + | local ad = { | ||
| + | station = station, | ||
| + | n = n | ||
| + | } | ||
| + | |||
| + | -- create one per BBS | ||
| + | local ref = station:AddAdvert({ | ||
| + | title = flavours[n].title, | ||
| + | description = flavours[n].flavour, | ||
| + | onChat = onChat, | ||
| + | onDelete = onDelete} | ||
| + | ) | ||
| + | ads[ref] = ad | ||
| + | end | ||
| + | |||
| + | local loaded_data | ||
| + | |||
| + | local onGameStart = function () | ||
| + | ads = {} | ||
| + | |||
| + | if not loaded_data or not loaded_data.ads then return end | ||
| + | |||
| + | for k,ad in pairs(loaded_data.ads or {}) do | ||
| + | local ref = ad.station:AddAdvert({ | ||
| + | title = flavours[ad.n].title, | ||
| + | description = flavours[ad.n].flavour, | ||
| + | onChat = onChat, | ||
| + | onDelete = onDelete}) | ||
| + | ads[ref] = ad | ||
| + | end | ||
| + | |||
| + | loaded_data = nil | ||
| + | end | ||
| + | |||
| + | local serialize = function () | ||
| + | return { ads = ads } | ||
| + | end | ||
| + | |||
| + | local unserialize = function (data) | ||
| + | loaded_data = data | ||
| + | end | ||
| + | |||
| + | Event.Register("onCreateBB", onCreateBB) | ||
| + | Event.Register("onGameStart", onGameStart) | ||
| + | |||
| + | Serializer:Register("message", serialize, unserialize) | ||
| + | </pre> | ||
| + | |||
The codedoc is complete and accurate, and the scripts provided with Pioneer are good examples themselves. The API is extensive, but if you find that there are additional things you would like to be able to do, or information you would like from Pioneer, the dev team are willing to extend the API to accommodate script writers. Simply create a new issue on the [https://github.com/pioneerspacesim/pioneer/issues Github issue tracker]. | The codedoc is complete and accurate, and the scripts provided with Pioneer are good examples themselves. The API is extensive, but if you find that there are additional things you would like to be able to do, or information you would like from Pioneer, the dev team are willing to extend the API to accommodate script writers. Simply create a new issue on the [https://github.com/pioneerspacesim/pioneer/issues Github issue tracker]. | ||
Help is also always available on the [https://forum.pioneerspacesim.net/ Pioneer dev forum], and many of the dev team can be found on [[IRC]] at all hours. | Help is also always available on the [https://forum.pioneerspacesim.net/ Pioneer dev forum], and many of the dev team can be found on [[IRC]] at all hours. | ||
Latest revision as of 19:38, 15 November 2025
When a game is saved, a script is responsible for informing Pioneer exactly which of its data needs to be saved. If nothing is specified, nothing will be saved at all. Similarly, after a game is loaded, a script is responsible for restoring all of its saved data; re-creating bulletin board adverts, the player's mission details, and any run-time state.
Here is a simple form. If a new game is started, you will see this on every bulletin board that you visit:
local Event = require 'Event'
local onChat = function (form, ref, option)
form:Clear()
form:SetMessage("Hello!")
end
local onCreateBB = function (station)
station:AddAdvert('Click here for a greeting', onChat)
end
Event.Register("onCreateBB", onCreateBB)
However, if you save the game, then reload, you will not see it on any bulletin board that had been created in the current system before you saved. onCreateBB will be triggered for any subsequently created bulletin boards, but not for those that already existed. It isn't appropriate to have scripts create new adverts just because a player has loaded; instead, it's the responsibility of the script to tell Pioneer what to save, and to use those saved data to restore all adverts after the game is loaded. The same goes for player missions, and any working data that the script is using.
There are two things we need to do to achieve all of this:
- We need to track everything that the script is doing.
- We need to be able to pack this away for saving, and bring it back after loading.
Contents
Tracking everything that we are doing
Any local table that is declared in file scope is visible to the entire script, without affecting any other scripts. Essential data should be kept in such tables.
Tracking adverts
It's important to be able to re-create an advert, so part of the process of making one should be storing that information. Your script will need to make a note of in which station the advert was placed, which flavour was used (if you have flavours), the face data that was used on the form and any unique information that was used, such as specific mission details, reward, etc. The simplest way to keep all of this together is in a table, gathering up the variables by name into keys of the same name. It could look something like this:
local advert = {
station = station,
flavour = flavour, -- This will be a table
client = client, -- This will be a table (a character)
target = target,
destination = destination,
reward = reward,
}
In the case of the 'greeting', the first example above, we would just have to save the message ('Hello!'), Advert string ('Click here for a greeting') and the station. It's a very simple example but there are real modules that aren't much more complex than that.
Remember, the AddAdvert() method takes three arguments: The advert text, the onChat function and the onDelete function. It also returns a unique number, which lends itself nicely to storing the information away. That same unique number is passed to the onChat function, allowing onChat to check that stored information.
The onDelete() function (again, we call it that by convention only) also accepts this reference as its argument, and is called whenever an advert is destroyed, whether that destruction be explicit (RemoveAdvert()) or implicit (the player hyperspaces away).
So, we can track adverts that have been created, and stop tracking any that have gone away. We'll do that using the reference returned by AddAdvert() as the key to a file-scoped local table:
local flavours = {}
local ads = {}
local onChat = function (form, ref, option)
form:Clear()
form:SetFace(ads[ref].character)
form:SetMessage(ads[ref].flavour.title)
end
local onDelete = function (ref)
ads[ref] = nil
end
local onCreateBB = function (station)
local flavour = flavours[Engine.rand:Integer(1, #flavours)]
ref = station:AddAdvert(flavour.title, onChat, onDelete)
ads[ref] = {
station = station,
flavour = flavour,
character = Character.new()
}
end
Event.Register("onCreateBB", onCreateBB)
Tracking missions
This is less of a challenge. Missions created with Mission.New() are stored in the PersistentCharacters.player.missions table. You also need to register them locally in your script. When Mission.New() is called, it returns a reference to the mission. That reference can be stored in a file-local table that can be iterated through when we need to access a mission. We can wrap these functions up:
local missions = {}
local addMission = function(mission)
table.insert(missions,Mission.New(mission))
end
local removeMission = function(mref)
local ref
for i,v in pairs(missions) do
if v == mission then
ref = i
break
end
end
mission:Remove()
missions[ref] = nil
end
Now, instead of directly calling Mission.Add() and mission:Remove(), we call our local functions, addMission() and removeMission(). They will send their argument on to the instance in Mission.lua, and also store or remove the mission ref in the missions table. There is now a record of which missions belong to this script. addMission() and removeMission() in the example above is taken from SearchRescue.lua. Look how removeMission() is used in the Search and Rescue module.
Serializing: Packing away for saving and loading
The Serializer object provides one method: Register. It takes three arguments. The first is a name; a simple string with which to uniquely identify this script. It's probably sensible to re-use the name that was used for the Translate object's flavour methods. The second and third arguments are both names of functions that each returns a single table. The second argument is your serializer function and the third is your unserializer function. By convention, we name these serialize and unserialize.
serialize must return a table, and this table must contain everything that you need to store to get your script working after a reload. It will be run when the player saves the game. The table can contain any pure-lua types, and SystemPath, Body and SceneGraph.ModelSkin core types, anything else will explode, like a ShipDef/EquipDef or a StarSystem.
unserialize accepts a table, and does something with it. It will be run by the serializer after the game is loaded, immediately before the onGameStart event is triggered. It is passed the data that serialize returned at save time.
The most common way to deal with this is as follows (and this is very cut down):
local table_stuff_this_script_uses = {}
local loaded_data
-- The rest of the script goes here!
local onGameStart
if loaded_data then
for k,v in loaded_data.table_stuff_this_script_uses do
table_stuff_this_script_uses[k] = v
end
loaded_data = nil
else
-- New game; do other stuff here perhaps
end
end
local serialize = function ()
return {table_stuff_this_script_uses = table_stuff_this_script_uses}
end
local unserialize = function (data)
loaded_data = data
end
Event.Register("onGameStart", onGameStart)
Serializer:Register("TestModule", serialize, unserialize)
Complete module
Now we can finally start to put together complete modules that will save, reload, and behave consistently throughout the game. Many of the modules in Pioneer don't register a mission. As an example of this, see BreakdownServicing, DonateToCranks, and CrewContracts. In the same spirit, let's complete the first example above, 'Click here for a greeting', and make it survive a reload.
local Event = require 'Event'
local Serializer = require 'Serializer'
local ads = {}
local onChat = function (form, ref, option)
local ad = ads[ref]
form:Clear()
form:SetMessage(ad.bodytext)
end
local onDelete = function (ref)
ads[ref] = nil
end
-- when we enter a system the BBS is created and this function is called
local onCreateBB = function (station)
local ad = {
headline = 'Click here for a greeting',
bodytext = "Hello!",
station = station
}
-- create one per BBS
local ref = station:AddAdvert({
description = ad.headline,
onChat = onChat,
onDelete = onDelete}
)
ads[ref] = ad
end
local loaded_data
local onGameStart = function ()
ads = {}
if not loaded_data or not loaded_data.ads then return end
for k,ad in pairs(loaded_data.ads or {}) do
local ref = ad.station:AddAdvert({
description = ad.headline,
onChat = onChat,
onDelete = onDelete})
ads[ref] = ad
end
loaded_data = nil
end
local serialize = function ()
return { ads = ads }
end
local unserialize = function (data)
loaded_data = data
end
Event.Register("onCreateBB", onCreateBB)
Event.Register("onGameStart", onGameStart)
Serializer:Register("message", serialize, unserialize)
The Advice module generously gave up parts of its code to complete this script.
Although the example is functional, it has been coded with brevity alone in mind. I would not recommend using this as the basis for a mission without at least completely rewriting onChat(), and possibly tidying up the temporary loop variable names.
Don't serialize strings
When we add the strings literally to the ad as we did in the example above, they will be serialized and saved on game end. Considering this is happening on all active stations, this will waste disk space.
local ad = {
headline = 'Click here for a greeting',
bodytext = 'Hello!',
station = station
}
What we want to do instead is save the information to recreate the same strings. If for instance we have a set of flavours that are selected with a random number, then generate that number and save it in a variable. The ad example above can then be rewritten as such. We only need the station and the flavour number to recreate the ad.
local ad = {
station = station,
n = n
}
The working example in the paragraph above can be updated in this way. When you have the translated strings in a separate module you would start the script by pulling in the strings to the script in an array or set of arrays. In the following example we instead add the strings to an array directly but it shows the general idea.
local Engine = require 'Engine'
local Event = require 'Event'
local Game = require 'Game'
local Rand = require 'Rand'
local Serializer = require 'Serializer'
local ads = {}
local flavours = {
{flavour = 'flavour 1', title = 'title 1', text = 'text 1'},
{flavour = 'flavour 2', title = 'title 2', text = 'text 2'},
{flavour = 'flavour 3', title = 'title 3', text = 'text 3'}
}
local onChat = function (form, ref, option)
local ad = ads[ref]
form:Clear()
form:SetMessage(flavours[ad.n].text)
end
local onDelete = function (ref)
ads[ref] = nil
end
-- when we enter a system the BBS is created and this function is called
local onCreateBB = function (station)
local rand = Rand.New(Game.time)
local n = rand:Integer(1, #flavours)
local ad = {
station = station,
n = n
}
-- create one per BBS
local ref = station:AddAdvert({
title = flavours[n].title,
description = flavours[n].flavour,
onChat = onChat,
onDelete = onDelete}
)
ads[ref] = ad
end
local loaded_data
local onGameStart = function ()
ads = {}
if not loaded_data or not loaded_data.ads then return end
for k,ad in pairs(loaded_data.ads or {}) do
local ref = ad.station:AddAdvert({
title = flavours[ad.n].title,
description = flavours[ad.n].flavour,
onChat = onChat,
onDelete = onDelete})
ads[ref] = ad
end
loaded_data = nil
end
local serialize = function ()
return { ads = ads }
end
local unserialize = function (data)
loaded_data = data
end
Event.Register("onCreateBB", onCreateBB)
Event.Register("onGameStart", onGameStart)
Serializer:Register("message", serialize, unserialize)
The codedoc is complete and accurate, and the scripts provided with Pioneer are good examples themselves. The API is extensive, but if you find that there are additional things you would like to be able to do, or information you would like from Pioneer, the dev team are willing to extend the API to accommodate script writers. Simply create a new issue on the Github issue tracker.
Help is also always available on the Pioneer dev forum, and many of the dev team can be found on IRC at all hours.