Exactly what happens in a Quetzal file?

As I tinker around more with the idea of Frotz running one command, saving, and then exiting, I keep coming across problems caused by the Z-machine being interrupted in ways not intended and then restored to an inconsistent state. To get around this, I’m trying to get a clearer picture of what’s actually in a Quetzal file and how to manipulate one to avoid these problems.

After I stepping away from the idea of calling save_quetzal() at the end of a turn, I went back to injecting a SAVE command into the input stream. This works for V3 games, but V5 and higher get corrupted. That shouldn’t be happening because as far as the Z-machine is concerned, it doesn’t know that the SAVE command didn’t come from the player.

In a Quetzal file, there is the IFhd chunk which contains the stored program counter, but I don’t understand where it’s supposed to point. I think it has something to do with the return value from the @save instruction. Following that is the CMem or UMem chunk (compressed or uncompressed). After making Frotz make a UMem chunk, I figured out that this contains the dynamic memory of the Z-machine. Then comes the stack chunk: Stks. I don’t understand this one at all. Why does it need to exist if stack data lives in dynamic memory?

The stack is outside the dynamic memory. Recall the picture from the standard:

Image from the Z-Machine standard showing the architecture of the Z-Machine

5.8 describes what address the PC should point to. Note that the address is actually in the middle of an instruction.

Using Quetzal for an autosave is technically invalid, and you couldn’t restore it using the @restore instruction, but it works well enough if you’re careful. If you’re going to make an autosave I’d say you should probably just store the PC as it currently is (probably the address of the next instruction after @read.)

1 Like

At least how I’ve been going about it, where the PC points is a major stumbling block for doing an autosave. Calling save_quetzal() directly results in a Quetzal file that passes as valid, but results in a illegal opcode error when trying to restore when bot mode is not active. I guess that’s what you mean with not being able to restore using @restore. For V5 and higher and maybe V4 too, I frequently get an illegal opcode error when trying to restore even in bot mode. If that doesn’t happen, then the screen and possible other stuff is corrupted.

When I called save_quetzal() after a turn is finished, I believe that this happens during a @read instruction. What would moving the PC to the next instruction accomplish? I thought that moving it back would allow for the game to pick up where it left off.

I’m not sure if the Z-machine has the same issues, but for Glulx there are some extra things it has to do after restoring a save to actually continue playing successfully (notably recovering Glk I/O objects and windows). It detects this by checking the result of the @save opcode, and I doubt it could recover if it were saved anywhere except the middle of that opcode.

(Checking, it doesn’t look like this applies to the Z-Machine – although it does use different code for V5+ vs. older versions – and the V5 version does have a similar “return a different value after restoring” behaviour, so there might still be some relationship there.)

There is a kind of save that does happen on every turn though – @save_undo. Perhaps you could hook into that somehow?

Quetzal requires the PC to be within a @save instruction because it doesn’t save the store variable/branch address. Saving the PC of that address allows the VM to read the store variable/branch address when restoring. Arguably this isn’t the best design for the save format, but it works well enough.

You could do the same for an autosave, and set the PC to the address of the @read store variable. But for versions <5 there is no store variable, so what would you do? And unlike @save, you also need the other operands, not just the store variable. I think it’s just simpler to set the PC to the next instruction (as it naturally would be after decoding the instruction) and to save the @read instruction’s data in the autosave’s supplementary data (along with the UI state, random state, etc.) But you have lots of freedom - you don’t need interoperability for an autosave. You could even save the PC of the @read instruction itself, and reparse it.

I don’t know why the Frotz save code is proving so difficult to adapt for autosaving. If you didn’t originally write save_quetzal and the other functions, then maybe you should ignore them and try implementing Quetzal yourself from scratch, so that you’ll both be completely on top of how the code works, as well as able to implement it in a manner that would allow it to be used for autosaving too.

1 Like

(Or a @save_undo instruction.)

For Glulx, I just figured out what extra information was necessary to relaunch the read operation, and shoved that onto the stack in the autosave. Then at autorestore time I pull that info back off.

Maybe this info is different for V3/4 vs V5+.

As Dannii says, the autosave file doesn’t have to be compatible with a regular @restore – in fact it can’t be. So you just put in whatever you need to make it possible.

1 Like

The z_read() function in Frotz assumes that the PC is on the next instruction (V3/4) or on the byte representing the store argument (V5+). This makes restarting the read a little messy, but you just need to test those two cases.

I’d save the PC there (exactly where it is inside the z_read()) and then push the two or four read arguments (text/parse/time/routine) onto the stack.

Then, at restore time, you can pull that info to set up zargs[], get back into the execution loop, and make sure z_read() is called first. This may require some hackery to skip the instruction decoding, since you don’t need to do that again.

EDIT-ADD: The alternative is to save the PC on the @read instruction, and make sure the stack is set back up the way it was before that instruction was decoded. This also requires some hackery! You’d have to push back only those @read arguments that came off the stack. (Which could be any, all, or none of them.)

Kind of a coin toss which route is less painful.