Interaction between @save_undo and bit 4 of Flags 2?

I believe static setting is the case, but someone with more knowledge of Inform than me will have to confirm.

With regard to the table of flags/UNDO results:
I never really understood why the -1 return value was deemed useful, let alone necessary, since it would seem to answer the same question as the flag. Hopefully all interpreters produce consistent results here. I would think if the flag is 0, the game shouldn’t be using the UNDO opcodes and would never see a -1 result.

An interpreter that has 1 for the flag, but returns -1 on the instruction would have to be considered broken, in my mind.

Likewise an interpreter that has 0 for the flag and returns anything other than -1 should also be considered broken, although succeeding when you claim you can’t is perhaps less bothersome than failing when you claim you can. Also, the game should be considered broken too, for not respecting the flag in the first place.

Bear in mind that Graham Nelson’s Z-machine standard is based a lot on reverse-engineering and conjecture; there are conflicts with Infocom’s own standards, and, as you’re discovering, some odd cases.

Infocom’s on YZip documentation has this to say about the flag in the header:

Bit #4 (%FUNDO) should be set at compile time by games which will try to
use ISAVE and IRESTORE. The interpreter should examine this bit in
marginal memory size cases to determine how many swapping pages to
allocate.

It does not say the interpreter clears the bit; it does indicate some other flags (such as the sound bit) are cleared if the interpreter can’t support it, so it seems plain that Infocom meant this bit not to be cleared, but only as a hint to the interpreter that it may need to allocate extra space for undo.

Modern interpreters probably follow Nelson’s standard, and clear the bit, but any reasonable interpreter will properly return a failure value of one kind or another from @save_undo, so I think it’s prudent for a game to simply try the call. I don’t think there’s any practical difference between testing the flag and checking the return value of @save_undo.

I did a cursory check for Infocom’s usage of @save_undo in their games, and they mostly just check whether 2 is returned, ignoring other values. Beyond Zork does look like it compares the result to 0 at one point, considering 0 to mean “no undo available”; but that just indicates that it doesn’t take into account that -1 might be returned.

In short, I’d recommend just calling @save_undo, treating 1/2 as success and any other value as failure. Don’t bother with the header flag.

An interesting question might be whether any original Infocom interpreters ever return -1 for the UNDO opcodes to indicate lack of support. Perhaps if their interpreters didn’t clear the flag, then the -1 return value would make sense from a historical perspective.

Yes, that’s correct. The documentation may be a bit ambiguous, but it’s just noting that if you use a feature which has a corresponding flag, that flag will be set when the story is compiled, i.e. “baked in” to the resulting file. It doesn’t update the flag at runtime.

1 Like

I thought that to be the case. I first learned much of this so long ago that some of the things I believe I have trouble tracing back to actual evidence. :grinning:

To make this more clear, Infocom’s approach appears to be to ignore both success and failure of @save_undo, which is reasonable: if it succeeds, you want to be quiet. If it fails, you also want to be quiet, since it would run every turn; but you’ll learn about the failure when you try to undo.

@cas: I very much appreciate any citations from primary Infocom documentation; they are on point in terms of original intent at Infocom but (regrettably) to some extent moot with respect to my questions because the questions are ultimately about how a game should deal with the widely-used interpreters as they exist today. (Your quote on %FUNDO certainly has implications in that regard, but only if the interpreter maintainers change their code in response.)

Do you have any input on the three remaining questions (especially on whether or not interpreters should ever set bit 4) and the hazy spots on the chart of game-observable bit 4 values/@save_undo results that I laid out above?

@Dannii: Same questions to you, since you chimed in briefly earlier and also are an interpreter author.

@Mike_G: I regret that I don’t know which interpreter is yours. Will you tell me?

The majority of the ones I’ve written have never been publicly released. I write them for fun and education when learning a new programming language.

I had one publicly available many years ago called Grue, written in C#. It died when Codeplex did. I am working on a new z-machine library written in Rust to be used by a separate frontend. But it isn’t at a point I want to make it public. Honestly I’ve been distracted with other things lately and haven’t been working on it like I should.

The citation from Infocom’s documentation was an attempt to understand the “invalid” portions of your table (e.g. a cleared bit but @save_undo succeeds). I agree that modern interpreters shouldn’t be following Infocom’s documentation. It was (apparently) a misunderstanding of the save bit that led to the current standard’s mandate that it be cleared. More an explanation of the status quo than anything that addresses your actual problem (sorry!)

I’ll try to do quick answers to your “competing concepts” area:

1b. This has to be correct because the flag is not marked as Dyn in §11.1.
2b. This is marked as Rst so it needs to be reset per §6.1.2.2.
3a. Agree with your reasoning.
4b. Same reasoning as 1b.

I think what it comes down to is that the standard doesn’t say you should be updating this flag in real-time, so you can’t expect it to be updated in real-time. The important parts are:

  1. The flag will be cleared on startup if undo isn’t supported.
  2. The flag will persist across an undo (which is pretty meaningless since the flag won’t change: as mentioned by Dannii, this is more important with save, when the save file can come from another interpreter).

As a game developer, you can only rely on these since it’s what the standard says must happen. Even if an interpreter decided to update a flag each turn, nothing requires any other interpreter to do so.

My feelings as an interpreter author:

Interpreters should never set bit 4. If it’s not set initially, that’s a signal from the game that it doesn’t want to use the corresponding opcodes. If it does wind up using them, I think the interpreter should oblige if it can, but if not, so be it.

Games should only rely on @save_undo, not checking the bit. There may be some corner case I’m not thinking of, but if you want to make use of undo, just try calling it: you’d have to try it anyway, if the bit is set. If the bit’s not set and you try, you’ll likely just fail, but you have to handle failure anyway.

As a corollary, I don’t see any reason to care about reconciling bit 4 and @save_undo results. While I think interpreters should clear the bit, that’s only because the standard says they should. Games ought to treat it like Infocom did: purely as a signal that they’ll probably use those opcodes. Any interpreter that’s worth using will gracefully fail (i.e. return -1 or 0) even if it cleared the bit.

Is there a specific case you’re looking to handle, such as telling the user up front that undo definitely won’t be supported? That would be reasonable, but could be hacked around by calling @save_undo once, up front, and if it fails, just assume for the rest of the session it’s not available. You’d have to arrange for @save_undo to be called again in a suitable location, though, before the user can try to undo, or they’d wind up undoing back to the starting message. This is a case where the interpreter saying “I don’t support undo” would be useful, now that I’ve thought about it, but it’s just not something that you can rely on. I can say with certainty Bocfel (Gargoyle’s interpreter) doesn’t behave that way, and I expect some non-trivial number of people will play the game that way. @Dannii can probably offer input as to whether ZVM sets the bit, though I’m willing to bet it doesn’t (and this would cover Parchment/Lectrote which is likely the majority of users).

I’m willing to take some time looking through various interpreters to get a definitive picture on the real-world implementation of this.

Some quick notes about various interpreters:

  • Bocfel
    • Returns 0 if the user has disabled undo or if undo fails due to memory issues
    • Clears bit 4 if the user has disabled undo, never sets bit 4
  • Fizmo
    • Returns 0 if the user has disabled undo
    • Does not set or clear bit 4 as far as I can see
  • Frotz
    • Returns -1 if either the user disabled undo or if memory is exhausted; doesn’t appear to ever return 0
    • Clears bit 4 if the user disabled undo, never sets bit 4
  • Nitfol
    • Returns 0 on failure, never -1
    • Does set bit 4
  • ZVM
    • Always returns 1 (@save_undo cannot fail)
    • Does not set bit 4 from what I can see (since it can’t fail, it won’t need to clear it either)
  • XZip
    • Returns -1 if there’s not enough memory for undo, otherwise returns 1 (won’t return 0)
    • Does not set or clear bit 4 from what I can see

Of the interpreters I checked, Nitfol does set bit 4, but it’s the only interpreter to do so. You can check any interpreter with the following program:

[Main;
    if ($11->0 & $10 == $10) {
        print "This interpreter sets bit 4.^";
    } else {
        print "This interpreter does not set bit 4.^";
    }
];
1 Like

@cas: Thank you very much for the thoughtful and comprehensive replies. To answer your question about why I’m after this information: It’s important for the effect that I was trying to achieve that it be possible to determine in advance whether or not a @save_undo is expected to work, as part of what’s communicated to the player. (So basically “telling the user up front”, as you put it.)

It would be fine for the interpreter to change its mind over the course of a playthrough, but the goal would be to be able to detect that happening from the game side. I was also hoping that it would also be possible to control the availability of undo from the game side (and to determine whether the interpreter was being cooperative about that), but it seems like this is definitively not to be expected according to the ZMS 1.1 standard. (Thank you again to @Mike_G for your patience in pointing out my misconceptions here.)

Just to double check, did you mean 1a (game sets at compile time only) instead of 1b (game can set at run time) in your second-most recent response? (It seems from context that the answer is yes, but I want to be sure.) Also, which interpreter is yours?

I have done a little research on compliance myself, but you’ve already posted most of it. Some additional data:

  1. Story files compiled with Inform 6.31 have only bits 4 (requesting undo) and 6 (requesting colors) set in Flags 2. (This is apparently a change in the policy on bit 4 from that of Inform 5.5, as described in the standard.)

  2. Frotz (2.53) will allow bit 4 to be set or cleared at run time, but this has no apparent effect on @save_undo behavior. If a game saved with undo disabled is restored with undo enabled, bit 4 is set – presumably it’s just deciding not to clear the story file’s original setting. Interestingly, use of @restore_undo causes Frotz to set bit 4 again at run time, even if it has been cleared by the game, but the same is not true of @save_undo, which leaves it as set at the time of the call.

  3. WinFrotz (1.21, out of date) follows the same pattern as Frotz by default (i.e. with undo available). I haven’t found a way to disable undo on the interpreter side for WinFrotz, so I don’t know how it behaves then. I didn’t try restoring a saved game that had been saved when undo was disabled for that reason.

  4. Bocfel (3.5.2) will allow bit 4 to be set at run time, but it halts with a fatal error on an attempt by the game to clear it. (Arguably, this is stricter compliance. Arguably, it would also halt if the game tries to set the flag at run time.) If a game is saved with undo disabled but restore with undo enabled, then bit 4 is set (same as Frotz).

That’s pretty strict. I wouldn’t recommend an interpreter be this strict about enforcing restrictions on games writing to header values marked read-only by the standard. Bit 3 of flags 1 (in Appendix B) is marked as not game writable, but Infocom games such as The Witness do alter it at runtime as mentioned in note 2 in the same appendix.

Yes, sorry for the confusion: I did mean 1a. Bocfel is my interpreter.

This is correct in a sense, but what’s really happening is that Bocfel traps any modification to bits 3-7 at address 0x11. Bits 0-2 can be set by the game, but since the Z-machine isn’t bit-addressable, the game will necessarily write a byte at 0x11 (or possibly a word at 0x10), which must include the “forbidden” bits 3-7. So, if bits 3-7 aren’t being changed, it’s not considered an error, even though they’re technically being written to.

A completely reasonable stance. Bocfel is probably the most strict interpreter in general, in this case disallowing writes to any memory which is not explicitly writeable. I’d prefer keeping it as such, loosening the restrictions only when it has real-world effects. A number of such cases have been explicit bugs in the game software, which Bocfel patches around to avoid special-casing non-standard behavior in the interpreter itself. So far, most games have proven to be bug free (in the sense of not performing standards-violating behavior that is trapped by Bocfel), so this approach has worked well, but I won’t argue it’s objectively superior.

The Tandy case is interesting. Allowing just the Tandy bit to be written has a measurable (around 5%) performance impact, because a new condition is needed which is checked each time memory is written. Bocfel allows the Tandy bit to be set, but only if you use a particular (non-default) compile option. As a workaround, Bocfel has the -X flag, which sets the Tandy bit on startup. Since the Tandy bit is more of a novelty than anything else, and is non-standard behavior, I tried to find a middle ground that allows it to be explored without affecting every other game in existence. I’ll consider just unconditionally supporting writing of the Tandy bit in future releases.

1 Like

Unfortunately the documentation and behavior around UNDO is very inconsistent.

All modern games (Inform 6 and Zilf) set the bit if they support undo (I6 if you use @save_undo or @restore_undo, Zilf if you set the <ZIP-OPTION UNDO>) I assume Inform 7 does also.

The Version 5 Infocom games set the bit but never check it. They use the result of @save_undo (<ISAVE>) to determine if undo is available for certain things, like the death message – 0 means not available, not-zero means available. They use the result of @restore_undo (<IRESTORE>) to say whether the undo failed (0) or simply is not available (not 0). This is not Standard behavior and is also the opposite of what Infocom’s own documentation says.

The Version 6 infocom games use the flag to determine if undo is available. The note about interpreters clearing the bit if undo is not available is a handwritten addendum to one version of the available Infocom docs, so it may have been added in later xzip (V5) revisions (Beyond Zork uses the equivalent for graphics, so, it’s probably not V6-only. And at least one version of Zork Zero uses the wrong bit!)

So if you’re writing a game, my suggestion is to set the bit in the flags word. If you want to know if undo is available, @save_undo early on. If you don’t get 1 (or 2, but you should try to avoid allowing a @restore_undo before another @save_undo is done), undo is not available. You can repeat the test after a regular @restore if you like. Ignore the bit.

I stand corrected! I was looking at the wrong docs when diving into this.

I’m sorry, I didn’t mean to criticize. Bocfel’s approach is certainly a legitmate one.

To me, this suggests an incomplete, imprecise and/or insufficiently explicit standard.

I’ve seen Mike_G’s post about currently undefined behavior (Z-Machine undefined behavior). Perhaps some more comprehensive definitions of expected behavior in this area can go on the list. Is there some sort of working committee for a ZMS 1.2 standard?

It’s quite fine. The criticism is valid, and there’s no question that some of the decisions I’ve made are a bit on the dogmatic side. Ultimately if they’re an impediment to playing the games, I make changes. Feedback on which parts might need to be changed is always welcome.

OK. I’ve expanded the first draft of the interpretation table that I put together. Please don’t hesitate to point out any errors; my hope is that this will be useful to others later on.

[The following table assumes that the game's story file has Flags 2 bit 4 statically set at compile time.
ZMS 1.1 defines attempts by the game to set Flags 2 bit 4 at run time as illegal; see section 11.1.]


bit 4 value  @save_undo result  valid?     game's most likely to be correct interpretation
-----------  -----------------  ------     ---------------------------------------------------------
0            -1                 YES        interpreter which does not support undo (or is currently
                                           configured to not support undo) has correctly cleared bit
                                           4 and is correctly returning -1 to indicate "permanent"
                                           failure in response to an attempted @save_undo; undo is
                                           not and will not be supported in this play session

1            -1                 NO         either interpreter which does not support undo has failed
                                           to clear bit 4 on start/restore, or the interpreter is
                                           incorrectly returning -1 instead of 0 in response to a
                                           "temporary" error situation while trying to save state;
                                           game should assume that undo will not be available in
                                           this play session, and author may wish to notify
                                           interpreter maintainer(s) of the discrepancy

0             0                 NO         interpreter which does not support undo has correctly
                                           cleared bit 4 on startup or restore but is incorrectly
                                           returning 0 instead of -1 in response to a game request
                                           to save state, and game probably shouldn't have tried a
                                           @save_undo in the first place; game should assume that
                                           undo will not be available in this play session, and
                                           author may wish to notify interpreter maintainer(s) of
                                           the discrepancy

1             0                 YES        interpreter which does support undo is correctly return-
                                           ing zero in response to a temporary error while trying to
                                           save state; game may hope that undo will become available
                                           again at some point in the play session but there is no
                                           guarantee that it will

0             1                 NO         interpreter which does support undo has incorrectly
                                           cleared bit 4 on start or restore; game should assume
                                           that undo will be available in this play session, and
                                           author may wish to notify interpreter maintainer(s) of
                                           the discrepancy
                             

1             1                 YES        all is well: undo is supported in this play session and
                                           the last attempt to save state went fine; game can
                                           probably rely on undo remaining available for the rest
                                           of the play session

0             2                 NO         interpreter has cleared bit 4 on start or restore to
                                           signal that undo is not supported in this play session,
                                           but nonetheless a successful @restore_undo was just
                                           executed; game should assume that undo will be available
                                           for the rest of the play session, and author may wish to
                                           notify interpreter maintainer(s) of the discrepancy

1             2                 YES        all is well: undo is supported in this play session and
                                           a successful execution of @restore_undo has just
                                           occurred; game should assume that undo will be available
                                           for the rest of the play session
1 Like

This looks entirely reasonable.

And it’s already been useful to me: I’ve fixed the bug in Bocfel where @save_undo was returning 0 despite the user disabling undo. In fact, TerpEtude tests for this specifically: if the header bit is cleared, but @save_undo doesn’t return -1, TerpEtude considers that non-standard behavior. Apparently I never thought to run it with undo disabled!

1 Like