/*
* $Id: PortableGameState.java 536 2008-02-19 06:03:27Z weiju $
*
* Created on 10/03/2005
* Copyright 2005-2008 by Wei-ju Wu
* This file is part of The Z-machine Preservation Project (ZMPP).
*
* ZMPP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ZMPP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ZMPP. If not, see <http://www.gnu.org/licenses/>.
*/
package org.zmpp.vm;
import java.util.ArrayList;
import java.util.List;
import org.zmpp.base.Memory;
import org.zmpp.iff.Chunk;
import org.zmpp.iff.DefaultChunk;
import org.zmpp.iff.FormChunk;
import org.zmpp.iff.WritableFormChunk;
/**
* This class represents the state of the Z machine in an external format, so it
* can be exchanged using the Quetzal IFF format.
*
* @author Wei-ju Wu
* @version 1.0
*/
public class PortableGameState {
/**
* The return variable value for discard result.
*/
public static final int DISCARD_RESULT = -1;
/**
* This class represents a stack frame in the portable game state model.
*/
public static class StackFrame {
/**
* The return program counter.
*/
int pc;
/**
* The return variable.
*/
int returnVariable;
/**
* The local variables.
*/
short[] locals;
/**
* The evaluation stack.
*/
short[] evalStack;
/**
* The arguments.
*/
int[] args;
public int getProgramCounter() {
return pc;
}
public int getReturnVariable() {
return returnVariable;
}
public short[] getEvalStack() {
return evalStack;
}
public short[] getLocals() {
return locals;
}
public int[] getArgs() {
return args;
}
public void setProgramCounter(final int pc) {
this.pc = pc;
}
public void setReturnVariable(final int varnum) {
this.returnVariable = varnum;
}
public void setEvalStack(final short[] stack) {
this.evalStack = stack;
}
public void setLocals(final short[] locals) {
this.locals = locals;
}
public void setArgs(final int[] args) {
this.args = args;
}
}
/**
* The release number.
*/
private int release;
/**
* The story file checksum.
*/
private int checksum;
/**
* The serial number.
*/
private byte[] serialBytes;
/**
* The program counter.
*/
private int pc;
/**
* The uncompressed dynamic memory.
*/
private byte[] dynamicMem;
/**
* The delta.
*/
private byte[] delta;
/**
* The list of stack frames in this game state, from oldest to latest.
*/
private List<StackFrame> stackFrames;
/**
* Constructor.
*/
public PortableGameState() {
super();
serialBytes = new byte[6];
stackFrames = new ArrayList<StackFrame>();
}
// **********************************************************************
// ***** Accessing the state
// *******************************************
/**
* Returns the game release number.
*
* @return the release number
*/
public int getRelease() {
return release;
}
/**
* Returns the game checksum.
*
* @return the checksum
*/
public int getChecksum() {
return checksum;
}
/**
* Returns the game serial number.
*
* @return the serial number
*/
public String getSerialNumber() {
return new String(serialBytes);
}
/**
* Returns the program counter.
*
* @return the program counter
*/
public int getProgramCounter() {
return pc;
}
/**
* Returns the list of stack frames.
*
* @return the stack frames
*/
public List<StackFrame> getStackFrames() {
return stackFrames;
}
/**
* Returns the delta bytes. This is the changes in dynamic memory, where 0
* represents no change.
*
* @return the delta bytes
*/
public byte[] getDeltaBytes() {
return delta;
}
/**
* Returns the current dump of dynamic memory captured from a Machine
* object.
*
* @return the dynamic memory dump
*/
public byte[] getDynamicMemoryDump() {
return dynamicMem;
}
public void setRelease(final int release) {
this.release = release;
}
public void setChecksum(final int checksum) {
this.checksum = checksum;
}
public void setSerialNumber(final String serial) {
this.serialBytes = serial.getBytes();
}
public void setProgramCounter(final int pc) {
this.pc = pc;
}
public void setDynamicMem(final byte[] memdata) {
this.dynamicMem = memdata;
}
// **********************************************************************
// ***** Reading the state from a file
// *******************************************
/**
* Initialize the state from an IFF form.
*
* @param formChunk the IFF form
* @return false if there was a consistency problem during the read
*/
public boolean readSaveGame(final FormChunk formChunk) {
stackFrames.clear();
if (formChunk != null
&& (new String(formChunk.getSubId())).equals("IFZS")) {
readIfhdChunk(formChunk);
readStacksChunk(formChunk);
readMemoryChunk(formChunk);
return true;
}
return false;
}
/**
* Evaluate the contents of the IFhd chunk.
*
* @param formChunk the FORM chunk
*/
private void readIfhdChunk(final FormChunk formChunk) {
final Chunk ifhdChunk = formChunk.getSubChunk("IFhd".getBytes());
final Memory chunkMem = ifhdChunk.getMemory();
int offset = Chunk.CHUNK_HEADER_LENGTH;
// read release number
release = chunkMem.readUnsignedShort(offset);
offset += 2;
// read serial number
for (int i = 0; i < 6; i++) {
serialBytes[i] = chunkMem.readByte(offset + i);
}
offset += 6;
// read check sum
checksum = chunkMem.readUnsignedShort(offset);
offset += 2;
// read pc
pc = decodePcBytes(chunkMem.readByte(offset), chunkMem.readByte(offset + 1),
chunkMem.readByte(offset + 2));
}
/**
* Evaluate the contents of the Stks chunk.
*
* @param formChunk the FORM chunk
*/
private void readStacksChunk(final FormChunk formChunk) {
final Chunk stksChunk = formChunk.getSubChunk("Stks".getBytes());
final Memory chunkMem = stksChunk.getMemory();
int offset = Chunk.CHUNK_HEADER_LENGTH;
final int chunksize = stksChunk.getSize() + Chunk.CHUNK_HEADER_LENGTH;
while (offset < chunksize) {
final StackFrame stackFrame = new StackFrame();
offset = readStackFrame(stackFrame, chunkMem, offset);
stackFrames.add(stackFrame);
}
}
/**
* Reads a stack frame from the specified chunk at the specified offset.
*
* @param stackFrame the stack frame to set the data into
* @param chunkMem the Stks chunk to read from
* @param offset the offset to read the stack
* @return the offset after reading the stack frame
*/
public int readStackFrame(final StackFrame stackFrame,
final Memory chunkMem,
final int offset) {
int tmpoff = offset;
stackFrame.pc = decodePcBytes(chunkMem.readByte(tmpoff),
chunkMem.readByte(tmpoff + 1), chunkMem.readByte(tmpoff + 2));
tmpoff += 3;
final byte pvFlags = chunkMem.readByte(tmpoff++);
final int numLocals = pvFlags & 0x0f;
final boolean discardResult = (pvFlags & 0x10) > 0;
stackFrame.locals = new short[numLocals];
// Read the return variable, ignore the result if DISCARD_RESULT
final int returnVar = chunkMem.readByte(tmpoff++);
stackFrame.returnVariable = discardResult ? DISCARD_RESULT
: returnVar;
final byte argSpec = chunkMem.readByte(tmpoff++);
stackFrame.args = getArgs(argSpec);
final int evalStackSize = chunkMem.readUnsignedShort(tmpoff);
stackFrame.evalStack = new short[evalStackSize];
tmpoff += 2;
// Read local variables
for (int i = 0; i < numLocals; i++) {
stackFrame.locals[i] = chunkMem.readShort(tmpoff);
tmpoff += 2;
}
// Read evaluation stack values
for (int i = 0; i < evalStackSize; i++) {
stackFrame.evalStack[i] = chunkMem.readShort(tmpoff);
tmpoff += 2;
}
return tmpoff;
}
/**
* Evaluate the contents of the Cmem and the UMem chunks.
*
* @param formChunk the FORM chunk
*/
private void readMemoryChunk(final FormChunk formChunk) {
final Chunk cmemChunk = formChunk.getSubChunk("CMem".getBytes());
final Chunk umemChunk = formChunk.getSubChunk("UMem".getBytes());
if (cmemChunk != null) {
readCMemChunk(cmemChunk);
}
if (umemChunk != null) {
readUMemChunk(umemChunk);
}
}
/**
* Decompresses and reads the dynamic memory state.
*
* @param cmemChunk the CMem chunk
*/
private void readCMemChunk(final Chunk cmemChunk) {
final Memory chunkMem = cmemChunk.getMemory();
int offset = Chunk.CHUNK_HEADER_LENGTH;
final int chunksize = cmemChunk.getSize() + Chunk.CHUNK_HEADER_LENGTH;
final List<Byte> byteBuffer = new ArrayList<Byte>();
byte b;
while (offset < chunksize) {
b = chunkMem.readByte(offset++);
if (b == 0) {
final short runlength = chunkMem.readUnsignedByte(offset++);
for (int r = 0; r <= runlength; r++) { // (runlength + 1) iterations
byteBuffer.add((byte) 0);
}
} else {
byteBuffer.add(b);
}
}
// Copy the results to the delta array
delta = new byte[byteBuffer.size()];
for (int i = 0; i < delta.length; i++) {
delta[i] = byteBuffer.get(i);
}
}
/**
* Reads the uncompressed dynamic memory state.
*
* @param umemChunk the UMem chunk
*/
private void readUMemChunk(final Chunk umemChunk) {
final Memory chunkMem = umemChunk.getMemory();
final int datasize = umemChunk.getSize();
dynamicMem = new byte[datasize];
for (int i = 0; i < datasize; i++) {
dynamicMem[i] = chunkMem.readByte(i + Chunk.CHUNK_HEADER_LENGTH);
}
}
// **********************************************************************
// ***** Reading the state from a Machine
// *******************************************
/**
* Makes a snapshot of the current machine state. The savePc argument is
* taken as the restore program counter.
*
* @param machine a Machine
* @param savePc the program counter restore value
*/
public void captureMachineState(final Machine machine, final int savePc) {
final StoryFileHeader fileheader
= machine.getGameData().getStoryFileHeader();
release = fileheader.getRelease();
checksum = fileheader.getChecksum();
serialBytes = fileheader.getSerialNumber().getBytes();
pc = savePc;
// capture dynamic memory which ends at address(staticsMem) - 1
// uncompressed
final Memory memory = machine.getGameData().getMemory();
final int staticMemStart = fileheader.getStaticsAddress();
dynamicMem = new byte[staticMemStart];
for (int i = 0; i < staticMemStart; i++) {
dynamicMem[i] = memory.readByte(i);
}
captureStackFrames(machine);
}
/**
* Read the list of RoutineContexts in Machine, convert them to StackFrames,
* prepending a dummy stack frame.
*
* @param machine the machine object
*/
private void captureStackFrames(final Machine machine) {
final Cpu cpu = machine.getCpu();
final List<RoutineContext> contexts = cpu.getRoutineContexts();
// Put in initial dummy stack frame
final StackFrame dummyFrame = new StackFrame();
dummyFrame.args = new int[0];
dummyFrame.locals = new short[0];
int numElements = calculateNumStackElements(machine, contexts, 0, 0);
dummyFrame.evalStack = new short[numElements];
for (int i = 0; i < numElements; i++) {
dummyFrame.evalStack[i] = cpu.getStackElement(i);
}
stackFrames.add(dummyFrame);
// Write out stack frames
for (int c = 0; c < contexts.size(); c++) {
final RoutineContext context = contexts.get(c);
final StackFrame stackFrame = new StackFrame();
stackFrame.pc = context.getReturnAddress();
stackFrame.returnVariable = context.getReturnVariable();
// Copy local variables
stackFrame.locals = new short[context.getNumLocalVariables()];
for (int i = 0; i < stackFrame.locals.length; i++) {
stackFrame.locals[i] = context.getLocalVariable(i);
}
// Create argument array
stackFrame.args = new int[context.getNumArguments()];
for (int i = 0; i < stackFrame.args.length; i++) {
stackFrame.args[i] = i;
}
// Transfer evaluation stack
final int localStackStart = context.getInvocationStackPointer();
numElements = calculateNumStackElements(machine, contexts, c + 1,
localStackStart);
stackFrame.evalStack = new short[numElements];
for (int i = 0; i < numElements; i++) {
stackFrame.evalStack[i] = cpu.getStackElement(localStackStart + i);
}
stackFrames.add(stackFrame);
}
}
/**
* Determines the number of stack elements between localStackStart and the
* invocation stack pointer of the specified routine context. If
* contextIndex is greater than the size of the List contexts, the functions
* assumes this is the top routine context and therefore calculates the
* difference between the current stack pointer and localStackStart.
*
* @param machine the Machine object
* @param contexts a list of RoutineContext
* @param contextIndex the index of the context to calculate the difference
* @param localStackStart the local stack start pointer
* @return the number of stack elements in the specified stack frame
*/
private int calculateNumStackElements(final Machine machine,
final List<RoutineContext> contexts, final int contextIndex,
final int localStackStart) {
if (contextIndex < contexts.size()) {
final RoutineContext context = contexts.get(contextIndex);
return context.getInvocationStackPointer() - localStackStart;
} else {
return machine.getCpu().getStackPointer() - localStackStart;
}
}
// ***********************************************************************
// ******* Export to an IFF FORM chunk
// *****************************************
/**
* Exports the current object state to a FormChunk.
*
* @return the state as a FormChunk
*/
public WritableFormChunk exportToFormChunk() {
final byte[] id = "IFZS".getBytes();
final WritableFormChunk formChunk = new WritableFormChunk(id);
formChunk.addChunk(createIfhdChunk());
formChunk.addChunk(createUMemChunk());
formChunk.addChunk(createStksChunk());
return formChunk;
}
private Chunk createIfhdChunk() {
final byte[] id = "IFhd".getBytes();
final byte[] data = new byte[13];
final Chunk chunk = new DefaultChunk(id, data);
final Memory chunkmem = chunk.getMemory();
// Write release number
chunkmem.writeUnsignedShort(8, (short) release);
for (int i = 0; i < serialBytes.length; i++) {
chunkmem.writeByte(10 + i, serialBytes[i]);
}
chunkmem.writeUnsignedShort(16, checksum);
chunkmem.writeByte(18, (byte) ((pc >>> 16) & 0xff));
chunkmem.writeByte(19, (byte) ((pc >>> 8) & 0xff));
chunkmem.writeByte(20, (byte) (pc & 0xff));
return chunk;
}
private Chunk createUMemChunk() {
final byte[] id = "UMem".getBytes();
return new DefaultChunk(id, dynamicMem);
}
private Chunk createStksChunk() {
final byte[] id = "Stks".getBytes();
final List<Byte> byteBuffer = new ArrayList<Byte>();
for (StackFrame stackFrame : stackFrames) {
writeStackFrameToByteBuffer(byteBuffer, stackFrame);
}
final byte[] data = new byte[byteBuffer.size()];
for (int i = 0; i < data.length; i++) {
data[i] = byteBuffer.get(i);
}
return new DefaultChunk(id, data);
}
/**
* Writes the specified stackframe to the given byte buffer.
*
* @param byteBuffer a byte buffer
* @param stackFrame the stack frame
*/
public void writeStackFrameToByteBuffer(final List<Byte> byteBuffer,
final StackFrame stackFrame) {
// returnpc
final int pc = stackFrame.pc;
byteBuffer.add((byte) ((pc >>> 16) & 0xff));
byteBuffer.add((byte) ((pc >>> 8) & 0xff));
byteBuffer.add((byte) (pc & 0xff));
// locals flag, is simply the number of local variables
final boolean discardResult = stackFrame.returnVariable == DISCARD_RESULT;
byte pvFlag = (byte) (stackFrame.locals.length & 0x0f);
if (discardResult) {
pvFlag |= 0x10;
}
byteBuffer.add(pvFlag);
// returnvar
byteBuffer.add((byte) (discardResult ? 0 : stackFrame.returnVariable));
// argspec
byteBuffer.add(createArgSpecByte(stackFrame.args));
// eval stack size
final int stacksize = stackFrame.evalStack.length;
addUnsignedShortToByteBuffer(byteBuffer, stacksize);
// local variables
for (short local : stackFrame.locals) {
addShortToByteBuffer(byteBuffer, local);
}
// stack values
for (short stackValue : stackFrame.evalStack) {
addShortToByteBuffer(byteBuffer, stackValue);
}
}
private void addUnsignedShortToByteBuffer(final List<Byte> buffer,
final int value) {
buffer.add((byte) ((value & 0xff00) >> 8));
buffer.add((byte) (value & 0xff));
}
private void addShortToByteBuffer(final List<Byte> buffer,
final short value) {
buffer.add((byte) ((value & 0xff00) >>> 8));
buffer.add((byte) (value & 0xff));
}
private byte createArgSpecByte(final int[] args) {
byte result = 0;
for (int arg : args) {
result |= (1 << arg);
}
return result;
}
// ***********************************************************************
// ******* Transfer to Machine object
// *****************************************
/**
* Transfers the current object state to the specified Machine object. The
* machine needs to be in a reset state in order to function correctly.
*
* @param machine a Machine object
*/
public void transferStateToMachine(final Machine machine) {
final Memory memory = machine.getGameData().getMemory();
// Dynamic memory
for (int i = 0; i < dynamicMem.length; i++) {
memory.writeByte(i, dynamicMem[i]);
}
// Stack frames
final List<RoutineContext> contexts = new ArrayList<RoutineContext>();
// Dummy frame, only the stack is interesting
if (stackFrames.size() > 0) {
final StackFrame dummyFrame = stackFrames.get(0);
// Stack
for (int s = 0; s < dummyFrame.getEvalStack().length; s++) {
machine.getCpu().setVariable(0, dummyFrame.getEvalStack()[s]);
}
}
// Now iterate through all real stack frames
for (int i = 1; i < stackFrames.size(); i++) {
final StackFrame stackFrame = stackFrames.get(i);
// ignore the start address
final RoutineContext context
= new RoutineContext(0, stackFrame.locals.length);
context.setReturnVariable(stackFrame.returnVariable);
context.setReturnAddress(stackFrame.pc);
context.setNumArguments(stackFrame.args.length);
// local variables
for (int l = 0; l < stackFrame.locals.length; l++) {
context.setLocalVariable(l, stackFrame.locals[l]);
}
// Stack
for (int s = 0; s < stackFrame.evalStack.length; s++) {
machine.getCpu().setVariable(0, stackFrame.evalStack[s]);
}
contexts.add(context);
}
machine.getCpu().setRoutineContexts(contexts);
// Prepare the machine continue
int pc = getProgramCounter();
if (machine.getGameData().getStoryFileHeader().getVersion() <= 3) {
// In version 3 this is a branch target that needs to be read
// Execution is continued at the first instruction after the branch offset
pc += getBranchOffsetLength(machine.getGameData().getMemory(), pc);
} else if (machine.getGameData().getStoryFileHeader().getVersion() >= 4) {
// in version 4 and later, this is always 1
pc++;
}
machine.getCpu().setProgramCounter(pc);
}
/**
* For versions >= 4. Returns the store variable
*
* @param machine the machine
* @return the store variable
*/
public int getStoreVariable(final Machine machine) {
final int storeVarAddress = getProgramCounter();
return machine.getGameData().getMemory().readUnsignedByte(
storeVarAddress);
}
/**
* Determine if the branch offset is one or two bytes long.
*
* @param memory the Memory object of the current story
* @param offsetAddress the branch offset address
* @return 1 or 2, depending on the value of the branch offset
*/
private static int getBranchOffsetLength(final Memory memory,
final int offsetAddress) {
final short offsetByte1 = memory.readUnsignedByte(offsetAddress);
// Bit 6 set -> only one byte needs to be read
return ((offsetByte1 & 0x40) > 0) ? 1 : 2;
}
// ***********************************************************************
// ******* Helpers
// *****************************************
/**
* There is no apparent reason at the moment to implement getArgs().
*
* @param argspec the argspec byte
* @return the specified arguments
*/
private int[] getArgs(final byte argspec) {
int andBit;
final List<Integer> result = new ArrayList<Integer>();
for (int i = 0; i < 7; i++) {
andBit = 1 << i;
if ((andBit & argspec) > 0) {
result.add(i);
}
}
final int[] intArray = new int[result.size()];
for (int i = 0; i < result.size(); i++) {
intArray[i] = result.get(i);
}
return intArray;
}
/**
* Joins three bytes to a program counter value.
*
* @param b0 byte 0
* @param b1 byte 1
* @param b2 byte 2
* @return the resulting program counter
*/
private int decodePcBytes(final byte b0, final byte b1, final byte b2) {
return ((b0 & 0xff) << 16) | ((b1 & 0xff) << 8) | (b2 & 0xff);
}
}