Difference between revisions of "Surviving a reload"
Zonkmachine (talk | contribs) (Use 'Event.Register') |
Zonkmachine (talk | contribs) (Code examples, formatting) |
||
Line 7: | Line 7: | ||
local onChat = function (form, ref, option) | local onChat = function (form, ref, option) | ||
− | + | form:Clear() | |
− | + | form:SetMessage("Hello!") | |
end | end | ||
local onCreateBB = function (station) | local onCreateBB = function (station) | ||
− | + | station:AddAdvert('Click here for a greeting', onChat) | |
end | end | ||
Line 33: | Line 33: | ||
local advert = { | local advert = { | ||
− | + | station = station, | |
− | + | flavour = flavour, -- This will be a table | |
− | + | facedata = facedata, -- This will be a table | |
− | + | target = target, | |
− | + | destination = destination, | |
− | + | reward = reward, | |
} | } | ||
Line 51: | Line 51: | ||
local onChat = function (form, ref, option) | local onChat = function (form, ref, option) | ||
− | + | form:Clear() | |
− | + | form:SetFace(all_adverts[ref].facedata) | |
− | + | form:SetMessage(all_adverts[ref].flavour.title) | |
end | end | ||
local onDelete = function (advert_ref) | local onDelete = function (advert_ref) | ||
− | + | all_adverts[advert_ref] = nil | |
end | end | ||
local onCreateBB = function (station) | local onCreateBB = function (station) | ||
− | + | local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)] | |
− | + | advert_ref = station:AddAdvert(flavour.title,onChat,onDelete) | |
− | + | local female = Engine.rand:Integer(1) == 1 | |
− | + | all_adverts[advert_ref] = { | |
− | + | station = station, | |
− | + | flavour = flavour, | |
− | + | facedata = { | |
− | + | female = female, | |
− | + | name = NameGen.FullName(female), | |
− | + | seed = Engine.rand:Integer() | |
− | |||
} | } | ||
+ | } | ||
end | end | ||
Line 84: | Line 84: | ||
local AddMission = function(mission) | local AddMission = function(mission) | ||
− | + | table.insert(missionrefs,Game.player:AddMission(mission)) | |
− | + | return missionrefs[#missionrefs] | |
end | end | ||
local RemoveMission = function(mref) | local RemoveMission = function(mref) | ||
− | + | for i,m in ipairs(missionrefs) do | |
− | + | if m == mref then | |
− | + | table.remove(missionrefs,i) | |
− | |||
end | end | ||
− | + | end | |
+ | Game.player:RemoveMission(mref) | ||
end | end | ||
Line 117: | Line 117: | ||
local onGameStart | 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 | end | ||
+ | loaded_data = nil | ||
+ | else | ||
+ | -- New game; do other stuff here perhaps | ||
+ | end | ||
end | end | ||
local serialize = function () | local serialize = function () | ||
− | + | return {table_stuff_this_script_uses = table_stuff_this_script_uses} | |
end | end | ||
local unserialize = function (data) | local unserialize = function (data) | ||
− | + | loaded_data = data | |
end | end | ||
Line 145: | Line 145: | ||
local onGameStart = function () | local onGameStart = function () | ||
− | + | all_adverts = {} | |
− | + | missions = {} | |
− | + | if not loaded_data then return end | |
− | + | for k,ad in pairs(loaded_data.all_adverts) do | |
− | + | local ref = ad.station:AddAdvert(ad.flavour.title, onChat, onDelete) | |
− | + | all_adverts[ref] = ad | |
− | + | end | |
− | + | for k,mission in pairs(loaded_data.missions) do | |
− | + | local mref = Game.player:AddMission(mission) | |
− | + | table.insert(missions,mref) | |
− | + | end | |
− | + | loaded_data = nil | |
end | end | ||
local onChat = function (form, ref, option) | local onChat = function (form, ref, option) | ||
− | + | form:Clear() | |
− | + | form:SetFace(all_adverts[ref].facedata) | |
− | + | form:SetMessage(all_adverts[ref].flavour.title) | |
− | + | -- some selective code here that will add missions, etc. | |
end | end | ||
Line 172: | Line 172: | ||
local onDelete = function (advert_ref) | local onDelete = function (advert_ref) | ||
− | + | all_adverts[advert_ref] = nil | |
end | end | ||
local onCreateBB = function (station) | local onCreateBB = function (station) | ||
− | + | local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)] | |
− | + | advert_ref = station:AddAdvert(flavour.title,onChat,onDelete) | |
− | + | local female = Engine.rand:Integer(1) == 1 | |
− | + | all_adverts[advert_ref] = { | |
− | + | station = station, | |
− | + | flavour = flavour, | |
− | + | facedata = { | |
− | + | female = female, | |
− | + | name = NameGen.FullName(female), | |
− | + | seed = Engine.rand:Integer() | |
− | |||
} | } | ||
+ | } | ||
end | end | ||
local AddMission = function(mission) | local AddMission = function(mission) | ||
− | + | table.insert(missionrefs,Game.player:AddMission(mission)) | |
− | + | return missionrefs[#missionrefs] | |
end | end | ||
local RemoveMission = function(mref) | local RemoveMission = function(mref) | ||
− | + | for i,m in ipairs(missionrefs) do | |
− | + | if m == mref then | |
− | + | table.remove(missionrefs,i) | |
− | |||
end | end | ||
− | + | end | |
+ | Game.player:RemoveMission(mref) | ||
end | end | ||
local serialize = function () | local serialize = function () | ||
− | + | local missions_in_full = {} | |
− | + | for k,mref in ipairs(missions) do | |
− | + | table.insert(missions_in_full,Game.player:GetMission(mref)) | |
− | + | end | |
− | + | return { all_adverts = all_adverts, missions = missions_in_full } | |
end | end | ||
local unserialize = function (data) | local unserialize = function (data) | ||
− | + | loaded_data = data | |
end | end | ||
Revision as of 18:25, 3 November 2021
When a game is saved, a script is responsible for informing Pioneer exactly which of its data need 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 = import("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)
If, however, 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:
local advert = { station = station, flavour = flavour, -- This will be a table facedata = facedata, -- This will be a table target = target, destination = destination, reward = reward, }
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. I'll do that using the reference returned by AddAvert()
as the key to a file-scoped local table:
local all_flavours = Translate:GetFlavours('TestModule') local all_adverts = {} local onChat = function (form, ref, option) form:Clear() form:SetFace(all_adverts[ref].facedata) form:SetMessage(all_adverts[ref].flavour.title) end local onDelete = function (advert_ref) all_adverts[advert_ref] = nil end local onCreateBB = function (station) local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)] advert_ref = station:AddAdvert(flavour.title,onChat,onDelete) local female = Engine.rand:Integer(1) == 1 all_adverts[advert_ref] = { station = station, flavour = flavour, facedata = { female = female, name = NameGen.FullName(female), seed = Engine.rand:Integer() } } end Event.Register("onCreateBB", onCreateBB)
Tracking missions
This is less of a challenge. Whenever Player.AddMission()
is called, it returns a reference to the mission. That reference can be stored in a file-local table, and that table can be iterated through, and Player.GetMission()
called on each one. We can wrap these functions up:
local missionrefs = {} local AddMission = function(mission) table.insert(missionrefs,Game.player:AddMission(mission)) return missionrefs[#missionrefs] end local RemoveMission = function(mref) for i,m in ipairs(missionrefs) do if m == mref then table.remove(missionrefs,i) end end Game.player:RemoveMission(mref) end
Now, instead of calling Game.player:AddMission()
and Game.Player:RemoveMission()
, we just directly call our local functions, AddMission()
and RemoveMission()
. They will send their argument on to the instance in Game.player
, and also store or remove the mission ref in the missionrefs table. There is now a record of which missions belong to this script.
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 argument is the name of a function which will return a single table. The third argument is a function that will accept a single table.
The second argument is your serializer function. The third is your unserializer function. By convention, we name these serialize
and unserialize
.
serialize
must return a table. 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)
Now, combining that with the examples above, we get the following, which will save and load both the adverts and the missions that the script created:
local all_flavours = Translate:GetFlavours('TestModule') local all_adverts = {} local missionrefs = {} local onGameStart = function () all_adverts = {} missions = {} if not loaded_data then return end for k,ad in pairs(loaded_data.all_adverts) do local ref = ad.station:AddAdvert(ad.flavour.title, onChat, onDelete) all_adverts[ref] = ad end for k,mission in pairs(loaded_data.missions) do local mref = Game.player:AddMission(mission) table.insert(missions,mref) end loaded_data = nil end local onChat = function (form, ref, option) form:Clear() form:SetFace(all_adverts[ref].facedata) form:SetMessage(all_adverts[ref].flavour.title) -- some selective code here that will add missions, etc. end -- Other event handlers here that might change or remove missions or adverts local onDelete = function (advert_ref) all_adverts[advert_ref] = nil end local onCreateBB = function (station) local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)] advert_ref = station:AddAdvert(flavour.title,onChat,onDelete) local female = Engine.rand:Integer(1) == 1 all_adverts[advert_ref] = { station = station, flavour = flavour, facedata = { female = female, name = NameGen.FullName(female), seed = Engine.rand:Integer() } } end local AddMission = function(mission) table.insert(missionrefs,Game.player:AddMission(mission)) return missionrefs[#missionrefs] end local RemoveMission = function(mref) for i,m in ipairs(missionrefs) do if m == mref then table.remove(missionrefs,i) end end Game.player:RemoveMission(mref) end local serialize = function () local missions_in_full = {} for k,mref in ipairs(missions) do table.insert(missions_in_full,Game.player:GetMission(mref)) end return { all_adverts = all_adverts, missions = missions_in_full } end local unserialize = function (data) loaded_data = data end Event.Register("onGameStart", onGameStart) Event.Register("onCreateBB", onCreateBB) Serializer:Register("TestModule", serialize, unserialize)
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.
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 additonal 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 forum on SpaceSimCentral.com, and many of the dev team can be found on IRC at all hours.