Saving and Loading TADS VM State

Okay, so… Let’s say there’s a clear point where a puzzle begins in a game. There’s no turning back, and you have all the necessary resources to solve it.

Let’s say that after 16 turns, the player dies, and realizes that their death was caused by something on turn 3.

I would really not like to ask the player to use the UNDO command 13 times, and I can’t guarantee they made a game save at the start of the puzzle.

Let’s ignore how harsh this puzzle might seem in abstract, for the sake of the question. It’s for a combat puzzle, and combat could take quite a few turns.

Is there a way I could create an internal game save automatically, at the time that the puzzle begins, and offer to load it upon death? So in addition to the other options, also offer “retry this combat encounter”?

The tads-gen functions provide savepoint() and undo() to do something like this, but I’ve only seen them used in try/catch blocks to handle things that might cause the interpreter to chuck a wobbly.

Reading the tads-gen documentation it looks like it should be possible to do the sort of thing you’re wanting to do (with a couple limitations) but I’ve never actually implemented anything like it.

1 Like

Actually, looking at the way it’s used in the parser I’m not sure you could reliably use savepoint() and undo() this way. What you’d have to do is keep track internally of how many moves have happened since the savepoint you want to roll back to.

That’s easy enough, but as far as I know there’s no way to know exactly how many savepoints the parser itself has created doing its business and (as far as I know) there’s no explicit guarantee that it’s going to be one per turn.

There’s also a limit on the number of savepoints the interpreter will keep track of, and it automagically removes the ones at the bottom of the stack (the oldest ones) when it needs space. So I don’t think there’s any way you can prevent the case where you make a savepoint at the start of the puzzle, the player types >X ME a dozen times, and then poof, your savepoint has been lost.

1 Like

The saveGame and restoreGame functions take a file name, so you should be able to manually create a savefile at the required place without prompting the user.

See frobtads/save.t at master · realnc/frobtads · GitHub

If TADS is like Glulx then you’d want to also add a bunch of safety checking code, to ensure the file exists etc. Or just wrap it in a try-catch.

1 Like

He’s a kinda/sorta kludgy hack version of what I’m talking about.

This is a simple “game” with a pebble, two rooms, one new verb, and an Oxford comma. When you pick up the pebble, it sets a savepoint. You can then do what you will, within the somewhat limited options afforded by the game, and then type >FOOZLE to (try to) roll back to the savepoint.

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

modify libGlobal
        // A place to keep track of our turn number
        _rollbackTurn = nil
;

// A new verb for our nefarious undo-ery
DefineIAction(Foozle)
        execAction() {
                local i, r;

                // Sanity check
                if(libGlobal._rollbackTurn == nil) {
                        "Nothing to roll back. ";
                        return;
                }

                // Roll things back, avoiding a fenceposting error.
                i = libGlobal.totalTurns - libGlobal._rollbackTurn + 1;
                r = 0;
                while(i > 0) {
                        // Should never happen
                        if(!undo()) {
                                "What?  Ran out of stuff to undo. ";
                                return;
                        }
                        i = i - 1;
                        r = r + 1;
                }
                
                // Report
                "Undid <<toString(r)>> turns. ";

                // Stop ourselves from doing it again.
                libGlobal._rollbackTurn = nil;
        }
;
VerbRule(Foozle) 'foozle' : FoozleAction verbPhrase = 'foozle/foozling';

startRoom:      Room 'Void'
        "This is a featureless void, with more to the north. "
        north = northRoom
;
+pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble."
        dobjFor(Take) {
                action() {
                        // Save our current turn
                        libGlobal._rollbackTurn = libGlobal.totalTurns;
                        savepoint();
                        inherited();
                }
        }
;
+me:    Person;
northRoom:      Room 'Other Void'
        "This is a different, but still featureless, void, with a bit to the south."
        south = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;
1 Like

This seems doable, but you bring up a very good point about potentially-limited undo states to step through. Had not considered that.

Wow my eyes had completely blanked those out when I was searching through the documentation. Excellent catch! I might see if it’s possible to create a directory for autosaves as well, so they don’t fall into the same directory as the game file itself, and keep the player’s filesystem neat and organized.

I don’t know if you can make directories in TADS, but even if you can I wouldn’t rely on that. It wouldn’t be possible in the Glk terps: Gargoyle/Lectrote/Parchment.

1 Like

Noted; thank you!

You may want to consider using this extension. It just might do what you’re trying to do.

1 Like

This may not be what you’re looking for, but I have done this in my game:
Modify the library code to only fire savepoint() after the action if a global property is true ( or nil, if the property is suspendSavepoint). If the player commits an an irremediable action, your code sets useSavepoint to nil. Using preSave objects and postUndo objects as necessary (manually calling the undo() function) the game will always revert (with both save and undo) to a point before the player is stuck. That might not be clear, but if you want details I can elaborate…

1 Like

Oooh! Is there documentation with this extension? That seems to just be download link.

I think I follow what you’re saying… So, essentially, if the player has not yet performed an “oops” action, then the global variable for “player_messed_up” is nil. Once an “oops” action is done, then an autosave is made before the action is executed, and the global variable is set to true, so that future “oops” actions do not cause additional autosaves? Basically so when a player reverts to an autosave, it’s always the first one that was made?

The library savepoint() is called after every successful action. So if a player does something irretrievable in an actionDobjForXx, all you have to do is add a line that says e.g. { libGlobal.suspendSavepoint = true; }
From that point on, unless you write other code to change the property, the game’s last saved state remains at the point before the player gaffed. No game states are saved anywhere unless you turn them back on. And I don’t mean that you are auto-saving their game to disk… simply that if the undo() function is called, it will unfailingly revert back to the turn before things were messed up.

1 Like

This brings up PreSave objects, where if the player tries to save their game after things are lost, you can notify them of what’s happening, and then manually call undo() there. The undo call will take the game state back to the last savepoint, which is before the problems, because no save points have been called since then. The rest of SaveAction will proceed as normal, but their game won’t be stuck.

1 Like

Additionally, if they try to ‘undo’, they will be whisked back to the pre-flubbed state. You may want to use a PostUndo object there to let them know that they’re not being taken back just one turn. Note that with the suspendSavepoint method, they won’t have the option to only undo one turn in a messed up situation (though it’s hard to see why that would be desirable), because undo by definition goes back to the state at the last savepoint() call.

1 Like

So to answer specifically, you should not have to make a manual savepoint() call… just turn on suspendSavepoint in an actionDobjFor.
To use the system, you need to add a clause in the executeAction function, in the block that starts with if (action.includeInUndo… keep the existing conditions and add && !libGlobal.suspendSavepoint

1 Like

Don’t forget if you use any manual undo() call, you will almost certainly want to be turning savepoint back on. You may also want to modify performUndo method of UndoAction to turn savepoint back on, unless you’re covering it in PostUndoObjects.

1 Like

Keep in mind that the user can restrict your filesystem access via the interpreter’s file safety settings. In its most restrictive setting (level 4) you can only read and write TemporaryFiles, embedded resources, and files the user chose from a prompt.

1 Like

concur with Adrian; I always set every terp to the most restrictive level, writing only savefiles in the current dir (the one with the story file; every story file has his own directory for this reason.

Best regards from Italy,
dott. Piergiorgio.

1 Like

saveGame and restoreGame can use TemporaryFiles, so if Joey was happy for players only being able to retry the combat within a play session then that should be fine with the most restrictive settings.

1 Like