Time travel: Wrapping Undo or Implementing with Properties

Hi,

I’m looking for some high-level feedback regarding implementation of a mechanic I’m interested in: time travel.

Specifically, I’d be looking to make something a la the rewind ability in the platformer Braid.

My plan has been to expose the ability through a pseudo-action “unwait”, which shares features of both “undo” and “wait”- namely, when the player “unwaits”, they remain in the same place and state, their inventory remains the same and with the same state, but everything else in the environment reverts to its state as of the player’s most recent non-unwait action.

Such a mechanic would make “in-the-weeds” time travel puzzles, a la Braid, possible. (For people very familiar with Braid, this style of time travel is not an exact rendering of the default mode of rewind in Braid, but more like an “everything in your inventory is green” or “you’re always on a green platform” mode.)

My original thought was that I could hook the default “undo” implementation to implement “unwait” fairly easily. However, I’m beginning to suspect there might not be such an easy approach- hooking “undo” requires dropping into Inform6, which seems quite poorly documented compared to Inform7 proper. There’s an extension for hooking “undo”: Erik Temple’s Undo Output Control but it is out of date and fails to compile with the latest Inform7.

As such I’m beginning to consider whether giving all objects a history of states/values over timesteps, and using that to implement time travel, might be the way to go.

Looking for thoughts from experienced Inform writers (I’m quite inexperienced but have a very strong software background). Any advice on how I might proceed with this idea?

So the reason “undo” is kept mostly at the I6 level is that it’s a very finnicky action which is difficult to work with. It’s actually not even an action, per se; it’s run at a different stage of parsing. And its effect is to restore (effectively) the entire RAM of the virtual machine to the state it was in one turn ago. Doing that and then restoring the player’s location is definitely possible, but would require some I6 hacking. (You mention a software background, so this might be the route for you: it’d certainly be more memory-efficient, and require less new code for each new object, but would also require assembly-level coding.)

For a case like this, the question is, how far do you want to rewind? In Braid you can go all the way back to the start of the level, which could be implemented using a list.

A thing has a list of objects called the previous holders. Every turn: repeat with the item running through things: add the holder of the item to the previous holders of the item. The alterable timestamp is a number that varies. Every turn: increment the alterable timestamp.

This list is effectively a stack. When you rewind, you’d pop the most recent entry from it.

To go back one minute: repeat with the item running through things not enclosed by the player: let the home be entry (alterable timestamp) in the former holders of the item; remove entry (alterable timestamp) from the former holders of the item; move the item to the home; decrement the alterable timestamp.

States would unfortunately have to be handled separately. You could create a kind of value representing possible states:

Prior state is a kind of value. The prior states are formerly locked, formerly unlocked, formerly closed, formerly open [and so on...]

Then you’d add a second list to each thing, this one being a list of prior states.

If you wanted to limit the amount of time the player could rewind, you could “truncate the former holders of the item to the last 24 entries” (for instance) in the first Every Turn rule.

You wrote:

You are familiar with the Inform Designer’s Manual?

(I admit it’s not going to help much with the sort of hacking you have in mind.)

The best documentation of the I6 parser level is inform7.com/extensions/Ron%20New … ource.html .

Thanks all for the feedback and advice! I was unaware of the I6 resources, thanks.

The extension I linked above, Undo Output Control, has thus far served as my guide for how to implement “Unwait”- overriding the routines Reading The Command, and Perform Undo. With the additional resources supplied I’ll be making a go of updating the extension’s outdated I6 (Presently it complains: “No such constant as L__M”, which seems like how one used to signal errors maybe?)

If this effort goes poorly, I’ll try implementing things in pure I7. As an aside- the process of writing I7 as an experienced software developer with considerable programming language theory background is… hard! It’s simply not designed with that skill set in mind, and I find it refreshing. Merely writing I7 is a puzzle in its own right.

Thanks again!

L__M was the way I6 used to refer to any old library message, which were basically standard messages produced by Inform (including not only error messages but standard responses like “It is pitch dark, and you can’t see such a thing.” One of the big changes in the most recent versions of Inform 7 was to replace the old library message system with a system of responses, documented in §14.10 of Writing with Inform (the manual included with I7). This involved a total change in how those messages were handled in I6. So any update of an old extension that involved I6 replacement is going to have to change those library messages.

Some hopefully useful rambling follows:

Most of these extensions that have huge I6 extensions IME don’t actually make that many changes to the I6 template–they tend to work by copying out a big chunk of the I6 template and changing it in a few places. One time before I successfully updated an outdated extension basically by going through the compilation errors, finding the corresponding place in the new I6 template, and copying them back in. In some ways the hardest part was finding the new I6 template (the one posted online is the old one, unfortunately). On the Mac they seem to be found in Inform/Contents/Resources/Internal; I had to “Show Package Contents” on the Inform app (and have the Finder set to “View as List” rather than “View as Columns”) to get there. (Sorry if the stuff I’m saying there is obvious to someone with a much stronger software background than I have! For me, it took a while to figure out.)

Looking at the places where Undo Output Control does its replacements, it looks like we have to open up Parser.i6t–oh, that’s always fun to mess with–OutOfWorld.i6t, zmachine.i6t, and Glulx.i6t. (Well, technically for your own project you only have to do zmachine.i6t or Glulx.i6t if that’s the virtual machine you’re targeting, but it probably won’t be that much extra work.) Then we have to scout out the parts we need to change. We’ll want to copy out the parts of the new templates that need changing for the extension, make the appropriate changes, and include them back in.

Having the 6F95 templates that zarf linked handy is actually very helpful here, because we can compare them to the existing Undo Output Control code to see where Erik changed them, and then find the corresponding place in the new code to try to reproduce Erik’s change there.

So, for instance, here’s what Perform_Undo looked like in the old OutOfWorld.i6t template:

[ Perform_Undo; #ifdef PREVENT_UNDO; L__M(##Miscellany, 70); return; #endif; if (turns == 1) { L__M(##Miscellany, 11); return; } if (undo_flag == 0) { L__M(##Miscellany, 6); return; } if (undo_flag == 1) { L__M(##Miscellany, 7); return; } if (VM_Undo() == 0) L__M(##Miscellany, 7); ];

Here’s Erik’s modification of it:

Include (- [ Perform_Undo; #ifdef PREVENT_UNDO; if ( FollowRulebook( (+ report prevented undo rules +) ) && RulebookFailed()) { L__M(##Miscellany, 70); } return; #endif; if (turns == 1) { FollowRulebook ( (+ before nothing to be undone failure rules +) ); if ( FollowRulebook( (+ report nothing to be undone failure rules +) ) && RulebookFailed()) { L__M(##Miscellany, 11); } FollowRulebook ( (+ after nothing to be undone failure rules +) ); return; } if (undo_flag == 0) { FollowRulebook ( (+ before interpreter-undo-incapacity rules +) ); if ( FollowRulebook( (+ report interpreter-undo-incapacity rules +) ) && RulebookFailed()) { L__M(##Miscellany, 6); } FollowRulebook ( (+ after interpreter-undo-incapacity rules +) ); return; } if (undo_flag == 1) { FollowRulebook ( (+ before interpreter undo failure rules +) ); if ( FollowRulebook( (+ report interpreter undo failure rules +) ) && RulebookFailed()) { L__M(##Miscellany, 7); } FollowRulebook ( (+ after interpreter undo failure rules +) ); return; } if ( (+ temporary undo suspension +) ) {FollowRulebook ( (+ report attempt to undo-while-disabled rules +) ); return;} if (VM_Undo() == 0) { FollowRulebook ( (+ before interpreter undo failure rules +) ); if ( FollowRulebook( (+ report interpreter undo failure rules +) ) && RulebookFailed()) { L__M(##Miscellany, 7); } FollowRulebook ( (+ after interpreter undo failure rules +) ); } ]; -) instead of "Perform Undo" in "OutOfWorld.i6t".

Here’s the Perform_Undo from the currrent OutOfWorld.i6t:

[ Perform_Undo; #ifdef PREVENT_UNDO; IMMEDIATELY_UNDO_RM('A'); new_line; return; #endif; if (IterationsOfTurnSequence == 0) { IMMEDIATELY_UNDO_RM('B'); new_line; return; } if (undo_flag == 0) { IMMEDIATELY_UNDO_RM('C'); new_line; return; } if (undo_flag == 1) { IMMEDIATELY_UNDO_RM('D'); new_line; return; } if (VM_Undo() == 0) { IMMEDIATELY_UNDO_RM('F'); new_line; } ];

Notice that the change from the old OutOfWorld.i6t is that L__M(##Miscellany, 70) is now IMMEDIATELY_UNDO_RM(‘A’), and so on; oh, and it looks like the variable “turns” got changed to “IterationsOfTurnSequence.” So my guess is that to update this we need to take Erik’s code and change those L__M calls and the “turns” variable:

Include (- [ Perform_Undo; #ifdef PREVENT_UNDO; if ( FollowRulebook( (+ report prevented undo rules +) ) && RulebookFailed()) { IMMEDIATELY_UNDO_RM('A'); } return; #endif; if (IterationsOfTurnSequence == 1) { FollowRulebook ( (+ before nothing to be undone failure rules +) ); if ( FollowRulebook( (+ report nothing to be undone failure rules +) ) && RulebookFailed()) { IMMEDIATELY_UNDO_RM('B');; } FollowRulebook ( (+ after nothing to be undone failure rules +) ); return; } if (undo_flag == 0) { FollowRulebook ( (+ before interpreter-undo-incapacity rules +) ); if ( FollowRulebook( (+ report interpreter-undo-incapacity rules +) ) && RulebookFailed()) { IMMEDIATELY_UNDO_RM('C');; } FollowRulebook ( (+ after interpreter-undo-incapacity rules +) ); return; } if (undo_flag == 1) { FollowRulebook ( (+ before interpreter undo failure rules +) ); if ( FollowRulebook( (+ report interpreter undo failure rules +) ) && RulebookFailed()) { IMMEDIATELY_UNDO_RM('D');; } FollowRulebook ( (+ after interpreter undo failure rules +) ); return; } if ( (+ temporary undo suspension +) ) {FollowRulebook ( (+ report attempt to undo-while-disabled rules +) ); return;} if (VM_Undo() == 0) { FollowRulebook ( (+ before interpreter undo failure rules +) ); if ( FollowRulebook( (+ report interpreter undo failure rules +) ) && RulebookFailed()) { IMMEDIATELY_UNDO_RM('F');; } FollowRulebook ( (+ after interpreter undo failure rules +) ); } ]; -) instead of "Perform Undo" in "OutOfWorld.i6t".

Now, I don’t know whether that will be enough. There may be other I6 changes that weren’t as simple as that to catch, and there may be other subtleties. Also, I make lots of copy-paste errors, so you should check my work above if something’s going wrong, and I don’t know why the “instead of” line says “Perform Undo” rather than “Perform_Undo” (though if it works, it works, obviously). And I really don’t know any I6 and definitely don’t know what’s going on with this extension.

But hopefully this will help you get started with the update!

…or you could grab what looks like an updated version of Undo Output Control from here and save yourself a ton of work. That might be a better idea.

So, it’s worth noting (for me as well as you) that a lot of extensions have updated versions somewhere on Github, and if you have an outdated version from the I7 extensions site, it’s worth doing a quick search to see if someone updated it. :blush:

Haha! Excellent, thanks for pointing out the github, I had no idea! I’ll definitely be moving forward with the existing extension, I can confirm this version compiles.

You might also want to check out the Hypothetical Questions extension, which is similar to what you’re looking for. It hacks the undo system to preserve a single value (a rulebook result), using the Glulx @protect instruction to shield a block of memory from being affected by restore/undo, then runs an action and immediately undoes it.

You could do something similar, but fill that block with object state instead. For example, if the objects only have simple properties (i.e. they don’t hold pointers to heap-allocated strings, lists, etc.), you could just copy the object structure and property table for every carried object into protected memory; after undoing, rearrange the inventory using the saved object structures as a guide, and copy the saved property tables back in place.

Those structures are documented in the Glulx Inform Technical Reference.