Language::Zcode::Runtime::State - Handle saving, restoring, etc. the game state
Getter/setter: currently in the process of restoring or not?
Start executing the Z-machine.
In the normal case (starting a new game, or restarting), this is as simple as calling the Z-machine subroutine whose address is stored in the header.
If we're restoring from a save file, it's more complicated. See "resume_execution".
Wrapper around Z-code subroutine calls. The main reason we need it is for save/restore.
In the normal case, z_call just calls the Z-code subroutine at address arg0 with the given args (arg5-argn), if any. Args 1-4 aren't used by z_call, but (hack alert!) they go into the Perl call stack, which is needed for saving Z-machine state.
Input: subroutine address to call, local variables & eval stack (arrayrefs), next PC, store variable, args to the Z-sub.
See "The call stack" for far more detail on this sub and save/restore.
Implement the @save opcode, saving the current Z-machine state (as opposed to writing a table to a file, the other use of the @save opcode)
Note that this sub also gets called at the very end of the restoring process.
Returns 0 for failed save, 1 for successful save, 2 for "just finished restoring".
Create a Z-machine call stack by peeking at the Perl call stack.
When calling Z_machine subroutines, we call z_call with all the information contained in a Z stack frame. We retrieve that information from the Perl call stack and build a Z-machine call stack with it.
Implement the @restore opcode, restoring the current Z-machine state (as opposed to reading a table from a file, the other use of the @restore opcode)
The Z-code call stack is much different than the Perl call stack, so it takes a bit of work to convert one to the other. Almost all of the work is done by the z_call routine which is a wrapper around Z-code subs.
Building the call stack
z_call is called with extra args (arg1-arg4), that are not technically used by z_call or the subs it calls. However, the Perl call stack stores these args, and we can later build a Z-machine (Quetzal) call stack using @DB::args (see "caller" in perlfunc). Sneaky!
How do we restore from a save file? We need to start execution with the Z-machine in the same state it was in when we did the @save. So we need to restore local variables and the eval stack. We also need to restore the call stack, so that when we finish a sub, we jump back into the middle of the sub that called that sub (at the address right after the call).
restore_state is quite simple: it just sets the restoring flag and then die()s (i.e., reboots the Z-machine). Most of the work is done by &z_call, when it sees the restoring flag.
When we're restoring, z_call is more complicated. We need to start executing subroutines in the middle (i.e., wherever the program counter was when @save was called), with the correct local variable and eval stack values.
z_call does this by reading data from the call stack and calling the Z-code subs with special args that tell them to start executing at a given address.
Where the usual calling tree looks like this: The main Perl program calls z_code(A) z_call calls Z-code sub A ('main' sub) Z-code sub A calls z_call(B) z_call calls B Z-code sub B calls z_call(C), etc.
We instead do this: The main Perl program calls z_code(A) z_call sees that A will call B, so it calls z_call(B) z_call(B) sees that B will call C, so it calls z_call(C) ... z_call(E) calls z_call(F), exhausting the restored call stack z_call(F) stops recursing and calls Z-code sub F with special args saying to start executing at the @save command F says "stop restoring", and returns z_call(E) calls E, saying to start executing after the call to F ... z_call(B) calls B, saying to start executing after the call to C
This is complicated, but has several benefits:
The very first opcode we execute when z_call gets to the top of the stack is guaranteed to be @save. This turns the restoring flag off. By the time we return from the called Z-code sub back to z_call, even though we're within several levels of recursion, we're in normal program flow.
(Note that z_call and the Z-code subs were calling each other recursively anyway. We just change the order around when restoring, so z_call calls itself and then calls the Z-code sub.)
Another piece of complexity is a mismatch between Quetzal and Perl.
Assume sub B calls sub C, with a call like $result = z_call(C, Blocals, Bstack, Bnext_PC, Bstore_var, args);
(where Blocals means the local variables in subroutine B).
Quetzal frames actually mix values from different (Perl) subroutines, so we have frames like (Clocals, Cstack, Bnext_PC, Bstore_var).
In addition, the only way we can figure out C while restoring is to get Cnext_PC (which will be in the frame with Dlocals et al.!) and to find the sub that contains the Cnext_PC address. So we actually need to look at three different frames in order to recreate the z_call that happened in the saved program.
Starting subs in the middle
z_call is called with @args, the list of arguments to be passed to the Z-code sub. Usually, we just pass the @args to the subroutine we call. However, when we're restoring, we replace @args with the values that should be in @locv for the called sub (which we got from the frame). We ignore @args in that case. Since Z-code subs automatically set @locv to be equal to the input args, this sets @locv to have the same value as it had when machine state was saved.
Also note that Quetzal saves only the number of args in a subroutine call, not their values. So when we're restoring and recursively calling z_call as we climb the stack, we don't know which @args to pass to those z_call calls. So we just pass in dummy values. (We need to get the right number of args so that the call stack is built correctly.)