Save Load - ThePix/QuestJS GitHub Wiki
There are two big issues with saving and loading...
Where to save: For security reasons browsers will not allow JavaScript to have full access to the file system on the player's computer. We can save to a special area called "LocalStorage" or in effect download or upload a file. The alternative is to save to the server, but that then ties it to textadventures.co.uk, and would I guess require some effort on the server.
What to save: Quest 5 saves everything. When the player loads a save-game, she is really loading a modified version of the original game - just as if she had editing the original game. If the author has changed or even deleted his game, it does not matter; the player has everything in the save-game. That works great most of the time. Where it fails is if the author updates his game - perhaps correcting bugs or extending it. When the player loads up her save-game, the bugs are still there, and the new areas inaccessible.
To reduce the security risks as perceived by the browser, Quest 6 web pages do no not allow eval
. This means you cannot convert a string to a script. As all game data is ultimately saved as a string, this means scripts cannot be saved (or more accurately cannot be recovered if they are). This means the Quest 5 strategy is just not an option.
Quest 6 therefore uses a different approach; just saving certain attributes as required, and then loading them on top of the existing game. If an author modifies his game, that modified game will be used as the basis and the attributes of objects in the new game modified. This is, please note, something of a compromise and there are disadvantages, discussed below.
Where To Save
Quest reduces the game state to a string, as described in the next section. This string can either be saved to LocalStorage, or converted to a file and downloaded (as the browser see it anyway).
With LocalStorage, we have full control of the save files, do the player can do DIR or LS to see a list of save-games, including date and time, and can delete save-games from within the game. However, there is a size limitation, and big games may hit this. Note that the UNDO system uses LocalStorage, so will be eating into the space too.
If the browser deletes its history - or is set to do so when the browser closes - all save-games in LocalStorage will disappear.
How big is big? The House on Highfield Lane had a play time of two hours or so, and did not run into this problem (save files are aroind 215 kB), but other authors have.
Saving as a file, you lose control. The file gets saved wherever the browser wants, you cannot get a listing of save-games or delete games from within the game. But the save-games are not going to disappear and the user can have as many as she wants.
By default, the user can choose either destination. SAVE HOUSE will save a file called "house" to LocalStorage, while FSAVE HOUSE will save as a file. LOAD HOUSE will load from LocalStorage, FLOAD will open a dialog to allow the file to be used.
You can disable LocalStorage saving:
settings.localStorageDisabled = true
SAVE HOUSE and FSAVE HOUSE will save as a file. LOAD and FLOAD will open a dialog to allow the file to be used.
What To Save
The game is saved as a string, with each segment separated by an exclamation mark. The first four segments are the header, the rest are the body. The header consists of the title, version, comment and timestamp. Each segment of the body is an object in the game.
An object is saved as its name followed by an equals sign followed either by "Object" or by "Clone:" and the name of the clone's prototype. This is followed by an equals sign, and then the data. Each datum is separated by a semi-colon. Each datum consists of the name, the type and the value, separated by colons.
If a datum is an object, and has a name attribute, the name is saved as type "qobject".
If the datum is an array and the first element is a string, it is assumed that all the elements are strings, and it is saved as an "array". If the first element is a number, it is assumed that all the elements are numbers, and it is saved as a "numberarray". If it is an array with no elements, it is saved as an "emptyarray". Other arrays are not saved.
If the datum is a number, a string or Boolean it is saved as such (an empty string is saved specifically as "emptystring").
Any other objects or values (such as undefined
or null
, or functions, or arrays of object) will not be saved.
The body has the save game, and could be encrypted, but currently is not. Frankly, any player wanting to cheat can do so via the console, so encrypting is a waste of time.
Saving data
Note that symbols used for formatting are escaped using multiple @ symbols, for example a colon is escaped as @@@cln @@@
.
Loading data
When loading data, the system over-writes the values on existing objects. This means that if you have deleted an object in your game during play, and the player then loads a game, that object will still be deleted (and in fact you will get an error). If you have created an object during play, the object will still be there after loading (in fact an error message will be displayed when the object is created if done via the create functions).
Therefore objects should be neither created nor destroyed during play.
You can, however, create and destroy clones, as discussed later.
NOTE: When an object is retrieved, its existing attributes are removed, except those that are functions, Exits or are listed in settings.saveLoadExcludedAtts
.
Customising for other attributes
If an object has a "beforeSave" function, this will be called before hand, and you could use that to create a new string attribute encoding another data type. The "afterLoad" function, called after loading, can be used to go the other way.
Here is an example that shows when this might be useful. The scenario here is the player has planted one or more seeds, and waiting for something to grow. The alias and pronouns can therefore changing, depending on the number and progress in the plant's lifecycle - how that is done is, we will assume, handled elsewhere. The pronouns will not get saved and the alias has some special considerations, so after the item is loaded, the custom "afterLoad" function sets them up correctly, based on attributes that are saved.
createItem("growing_plant", {
simpleIsAtLoc:function(loc) { return loc === 'greenhouse' && this.seedsPlanted > 0 && this.growthTime > 0 },
seedsPlanted:0,
growthTime:0,
afterLoad:function() {
this.pronouns = this.seedsPlanted === 1 ? lang.pronouns.thirdperson : lang.pronouns.plural
this.setAlias(this.growthTime < 4 ? 'shoot' : (this.growthTime < 9 ? 'seedling' : 'plant'))
},
})
For templates, use "beforeSaveForTemplate" and "afterLoadForTemplate". This should call "beforeSave" and "afterLoad" respectively. This means that a template can be customised but still allow authors to add their own customisation. Note that we need to be careful that two templates with "beforeSaveForTemplate" or with "afterLoadForTemplate" are not combined in one item - if they are, only one will apply (the solution is to create a new function that combines both). Currently ROOM, COUNTABLE and NPC are the only templates that do this.
New items
This means you cannot create new items during play, as they will not have any functions. However, you can clone objects. When the player does LOAD, all existing clones are deleted before hand. Once the original objects are re-loaded, the clones can be re-created (clones have a "clonePrototype" attribute that points to the original, and gets saved as the name).
What is saved?
As mentioned, by default only strings, integers, Booleans, string arrays and number arrays get saved. It should also be noted that only objects directly in w
will be saved. That does include the game
object, and obviously items, NPCs, rooms and events are all in w
. It does not include, for example, exits.
However, rooms are set up to save the "locked" and "hidden" attribute of each exit.
Note that other attributes will get deleted when a saved game is loaded, to "wipe the slate clean" as it were. The only attributes that will not be deleted are those that are functions or exits, and those listed in settings.saveLoadExcludedAtts
and the item's own "saveLoadExcludedAtts" attribute. That said, if you update your game, objects that are only in the new version will not be altered when a saved game is loaded.
You can add your own custom attributes to the list in settings, as the ZONE template does. This is necessary if you have complex data structures, as a ZONE location does for both exits and description, both of which are arrays of dictionaries.
settings.saveLoadExcludedAtts.push("zoneExits")
settings.saveLoadExcludedAtts.push("zoneDescs")
You might find it useful to have an exclusion on an object when you update you game to ensure the data is not deleted when an old save game is loaded, though I must admit I have not done this, so you are kind of on your own here!
An exclusion can be a regular expression or a string. You can also give an object a custom saveLoadExclude
function for full control (the base version is in _defaults.js, and you are probably best copying that and then editing as required).
Type | Comment |
---|---|
Standard | Standard attribute types are saved |
Functions | Function attributes are not changed |
Exits | Exit attributes are not changed (this is determined by their type, not their name) |
Exclusions | Attributes that are listed in settings.saveLoadExcludedAtts or the item's saveLoadExcludedAtts attribute are not changed |
Other | Any other attribute will get deleted |
Save errors
As discussed, Quest expects an array that starts with a string to only contain strings, and that starts with a number to only include numbers. If it encounters an unexpected type you will get an error. This example is because the "onGoActionList" has an object in it:
Error encountered with attribute "onGoActionList": Found type "object" in array - should be only strings.
You need to either ensure the array only has strings, or, if it never changes, you can set it to be ignored by save/load by using "saveLoadExcludedAtts" as described earlier.
Undo
The UNDO system works by saving to an array, world.saveGameState
, but the state is saved in exactly the same way as described above.
Transcripts
Transcripts are also saved to localStorage, so the transcript system is also in _saveLoad.js.