Persistence - Senscape/Dagon GitHub Wiki

Dagon has a persistence system under development. It is stable enough that we need Dagon developers to test the system with their games. However, it is recommended that you do not ship games with it just yet. That's because the file format is not finalized and it has so far only been tested with one game.

How do I get started?

These are the things you need to do before you can use the system.

1. Update to the latest version of Dagon

Before doing anything else, you will have to update to the latest version of Dagon. I recommended building from source as we do not have a new release on GitHub for it yet. If worse comes to worst I can share a Dropbox folder with you to get very recent Windows executables from. Consider this as a last resort as it really is far better for you to do your own builds - until a proper release build arrives.

2. Create a saves folder

In the same directory as where your Dagon executable resides, you will have to make a folder named saves. Unsurprisingly this is where the .sav files Dagon outputs can be found (and are expected to be found, when loading).

3. Make sure your scripts are compatible

The system tries to be non intrusive, nonetheless these are the rules thou shall abide by.

  • All nodes, slides, spots and their attachments must be created unconditionally

Consider a spot that becomes unusable after some action has been completed but not before. One way to do this, would be to attach a function that is called when said action is completed. That function would then create the spot. This won't go well with the current save system. Instead, you should create the spot outside the scope of any function and then immediately disable it after its creation. Then, a function that enables the spot can be called when aforementioned action has been completed. More generally, a Room must create all of its spots (etc. see above) simply by loading the room's script file. Loading as in: the same way you load a Room with the room 'MyRoom' command. This basically amounts to creating the room "unconditionally".

  • Make sure Dagon knows all of your rooms before saving and loading

Consider a game where you start in room A. Room A has a door to room B. As you know, before you can switch to room B you have to use the room 'RoomB' command, and then you can switch(RoomB). You see, Dagon doesn't know room B exists until it has seen the room 'RoomB' command. Therefore you may experience incorrect behaviour if you save in room A, go to room B, do stuff in room B and then reload into room A. You would expect your changes in room B to be reverted, but they aren't if Dagon didn't know about room B when you saved in room A. The solution would simply be to place room 'RoomB' at the top of RoomA.lua

  • Make sure none of your Spot, Node, Slide or Audio objects are anonymous

This amounts to always assigning your Spot, Node, Slide and Audio objects to Lua variables. Additionally all variable names must uniquely identify an object (very specific names are good then and if you assign different objects to the same variable, only the first object assigned will be checked). NOTE: Two tables t1 and t2both with a variable mySpot also violate the rule of assigning to the same variable. That case will be logged but same variable same table will unfortunately not be logged. Currently the best indication that you have duplicates in the variable names you assign Spots to, is to check the log for warnings of the form

No object mapping for Spot{COORDINATES}. State not saved.

If you have any of these you can use an editor like Notepad++ to search for some of these coordinates across your script files and see if any Spots are assigned to equivalent variable names more than once.

4. Take your pick! (about what you want to save)

The system tries to be simple, but strives to give control to the script developer. As such, Dagon will automatically create a table named dgPersistence. It is in this table that you put the data you want saved. Consider a flag you want saved. Before your code might have looked like

myFlag = false

... -- do stuff here to toggle myFlag
myFlag = true
...
if myFlag == true then
...
end

If you decide that it is important that myFlag will be saved, you must add it to dgPersistence which would mean your code will look like the following

dgPersistence.myFlag = false

... -- do stuff here to toggle myFlag
dgPersistence.myFlag = true
...
if dgPersistence.myFlag == true then
...
end

Conceptually anything can be stored in dgPersistence but only a select few datatypes will make it into the save file. These are

  • Numbers
  • Strings
  • Booleans
  • Tables (only data of the three types above will be saved)

What this means is you can store nodes and functions and other types of complex data in the table, but they won't have an effect on the save file. NOTICE: Such data won't be destroyed if it exists in dgPersistence when you load.

5. Don't rename Lua variables after game release!

Dagon uses the names of Lua variables to identify objects assigned to them across sessions, so if you change them you effectively invalidate players' save files!

Whew, that was quite a mouthful - but I know you can handle it ;)

What will be saved?

  • Player position (current room, current node)
  • Enable/disable status of all spots (not just current room)
  • Cursors assigned to individual spots
  • Previous Nodes for Slides
  • Status of audio objects for current room
  • Camera direction
  • Field of view
  • Timers that were running
  • Control mode

API

There are three functions that are particularly important.

  • persist
  • unpersist
  • getSaves

Here is a script example of how you could add quicksave and quickload functionality to your game.

hotkey(F5, "quicksave()")

function quicksave()
    success = persist("qs", { overwrite = true })
    -- first argument is the save name. This will create qs.sav in saves/ folder
    -- only the 1st argument is required. The second one is optional and
    -- default is overwrite = false
    if success == true then
        print("quicksaved!")
    else
        print("Save failed. Check log or console.")
    end
end

hotkey(F9, "quickload()")

function quickload()
    success = unpersist("qs")

    if success == true then
        print("load: check")
    else
        print("Load failed. Check log or console.")
    end
end

The getSaves function will return a table whose keys are save names (ready to be fed into unpersist) and the values of those keys is the preview text stored in the save file. The preview text is the description of the Node the game was saved in. It's functionality can be sampled like this.

hotkey(F7, "printSaves()")

function printSaves()
    for game, preview in pairs(getSaves()) do
        print(game)
        print(preview)
    end
end

Callbacks

Because the system has some restrictions, we offer onPersist and onUnpersist callbacks as a means for developers to extend the system. Consider saving in a room called MyRoom. MyRoom has multiple nodes but one you hold particularly dear to your heart. Let's call it MyNode. You can then define specific save / load behaviour for MyRoom and MyNode. Let's look at some examples.

MyRoom:onPersist(function()
    print("The game was saved in MyRoom!")
end)

MyRoom:onUnpersist(function()
    print("The game was loaded in MyRoom!")
end)

The onPersist callback will be called just after writing the save file header. Before dumping Lua data that is. This means you can make changes to dgPersistence in onPersist. onUnpersist is called just after switching the player to MyRoom. Node specific behaviour could also have been defined by replacing MyRoom with MyNode in the example. onUnpersist for nodes will be called just after switching the player to the relevant node. Notice that these callbacks are also provided for Slides.