scummvm/engines/sword2/interpreter.cpp
2007-12-16 00:41:30 +00:00

773 lines
20 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* Additional copyright for this file:
* Copyright (C) 1994-1998 Revolution Software Ltd.
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*/
#include "common/util.h"
#include "sword2/sword2.h"
#include "sword2/header.h"
#include "sword2/defs.h"
#include "sword2/interpreter.h"
#include "sword2/logic.h"
#include "sword2/memory.h"
#include "sword2/resman.h"
namespace Sword2 {
#define STACK_SIZE 10
// The machine code table
#ifndef REDUCE_MEMORY_USAGE
# define OPCODE(x) { &Logic::x, #x }
#else
# define OPCODE(x) { &Logic::x, "" }
#endif
void Logic::setupOpcodes() {
static const OpcodeEntry opcodes[] = {
/* 00 */
OPCODE(fnTestFunction),
OPCODE(fnTestFlags),
OPCODE(fnRegisterStartPoint),
OPCODE(fnInitBackground),
/* 04 */
OPCODE(fnSetSession),
OPCODE(fnBackSprite),
OPCODE(fnSortSprite),
OPCODE(fnForeSprite),
/* 08 */
OPCODE(fnRegisterMouse),
OPCODE(fnAnim),
OPCODE(fnRandom),
OPCODE(fnPreLoad),
/* 0C */
OPCODE(fnAddSubject),
OPCODE(fnInteract),
OPCODE(fnChoose),
OPCODE(fnWalk),
/* 10 */
OPCODE(fnWalkToAnim),
OPCODE(fnTurn),
OPCODE(fnStandAt),
OPCODE(fnStand),
/* 14 */
OPCODE(fnStandAfterAnim),
OPCODE(fnPause),
OPCODE(fnMegaTableAnim),
OPCODE(fnAddMenuObject),
/* 18 */
OPCODE(fnStartConversation),
OPCODE(fnEndConversation),
OPCODE(fnSetFrame),
OPCODE(fnRandomPause),
/* 1C */
OPCODE(fnRegisterFrame),
OPCODE(fnNoSprite),
OPCODE(fnSendSync),
OPCODE(fnUpdatePlayerStats),
/* 20 */
OPCODE(fnPassGraph),
OPCODE(fnInitFloorMouse),
OPCODE(fnPassMega),
OPCODE(fnFaceXY),
/* 24 */
OPCODE(fnEndSession),
OPCODE(fnNoHuman),
OPCODE(fnAddHuman),
OPCODE(fnWeWait),
/* 28 */
OPCODE(fnTheyDoWeWait),
OPCODE(fnTheyDo),
OPCODE(fnWalkToTalkToMega),
OPCODE(fnFadeDown),
/* 2C */
OPCODE(fnISpeak),
OPCODE(fnTotalRestart),
OPCODE(fnSetWalkGrid),
OPCODE(fnSpeechProcess),
/* 30 */
OPCODE(fnSetScaling),
OPCODE(fnStartEvent),
OPCODE(fnCheckEventWaiting),
OPCODE(fnRequestSpeech),
/* 34 */
OPCODE(fnGosub),
OPCODE(fnTimedWait),
OPCODE(fnPlayFx),
OPCODE(fnStopFx),
/* 38 */
OPCODE(fnPlayMusic),
OPCODE(fnStopMusic),
OPCODE(fnSetValue),
OPCODE(fnNewScript),
/* 3C */
OPCODE(fnGetSync),
OPCODE(fnWaitSync),
OPCODE(fnRegisterWalkGrid),
OPCODE(fnReverseMegaTableAnim),
/* 40 */
OPCODE(fnReverseAnim),
OPCODE(fnAddToKillList),
OPCODE(fnSetStandbyCoords),
OPCODE(fnBackPar0Sprite),
/* 44 */
OPCODE(fnBackPar1Sprite),
OPCODE(fnForePar0Sprite),
OPCODE(fnForePar1Sprite),
OPCODE(fnSetPlayerActionEvent),
/* 48 */
OPCODE(fnSetScrollCoordinate),
OPCODE(fnStandAtAnim),
OPCODE(fnSetScrollLeftMouse),
OPCODE(fnSetScrollRightMouse),
/* 4C */
OPCODE(fnColour),
OPCODE(fnFlash),
OPCODE(fnPreFetch),
OPCODE(fnGetPlayerSaveData),
/* 50 */
OPCODE(fnPassPlayerSaveData),
OPCODE(fnSendEvent),
OPCODE(fnAddWalkGrid),
OPCODE(fnRemoveWalkGrid),
/* 54 */
OPCODE(fnCheckForEvent),
OPCODE(fnPauseForEvent),
OPCODE(fnClearEvent),
OPCODE(fnFaceMega),
/* 58 */
OPCODE(fnPlaySequence),
OPCODE(fnShadedSprite),
OPCODE(fnUnshadedSprite),
OPCODE(fnFadeUp),
/* 5C */
OPCODE(fnDisplayMsg),
OPCODE(fnSetObjectHeld),
OPCODE(fnAddSequenceText),
OPCODE(fnResetGlobals),
/* 60 */
OPCODE(fnSetPalette),
OPCODE(fnRegisterPointerText),
OPCODE(fnFetchWait),
OPCODE(fnRelease),
/* 64 */
OPCODE(fnPrepareMusic),
OPCODE(fnSoundFetch),
OPCODE(fnPrepareMusic), // Again, apparently
OPCODE(fnSmackerLeadIn),
/* 68 */
OPCODE(fnSmackerLeadOut),
OPCODE(fnStopAllFx),
OPCODE(fnCheckPlayerActivity),
OPCODE(fnResetPlayerActivityDelay),
/* 6C */
OPCODE(fnCheckMusicPlaying),
OPCODE(fnPlayCredits),
OPCODE(fnSetScrollSpeedNormal),
OPCODE(fnSetScrollSpeedSlow),
/* 70 */
OPCODE(fnRemoveChooser),
OPCODE(fnSetFxVolAndPan),
OPCODE(fnSetFxVol),
OPCODE(fnRestoreGame),
/* 74 */
OPCODE(fnRefreshInventory),
OPCODE(fnChangeShadows)
};
_numOpcodes = ARRAYSIZE(opcodes);
_opcodes = opcodes;
}
#define push(value) \
do { \
assert(stackPtr < ARRAYSIZE(stack)); \
stack[stackPtr++] = (value); \
} while (false)
#define push_ptr(ptr) push(_vm->_memory->encodePtr(ptr))
#define pop() (assert(stackPtr < ARRAYSIZE(stack)), stack[--stackPtr])
int Logic::runResScript(uint32 scriptRes, uint32 offset) {
byte *scriptAddr;
int result;
scriptAddr = _vm->_resman->openResource(scriptRes);
result = runScript(scriptAddr, scriptAddr, offset);
_vm->_resman->closeResource(scriptRes);
return result;
}
int Logic::runResObjScript(uint32 scriptRes, uint32 objRes, uint32 offset) {
byte *scriptAddr, *objAddr;
int result;
scriptAddr = _vm->_resman->openResource(scriptRes);
objAddr = _vm->_resman->openResource(objRes);
result = runScript(scriptAddr, objAddr, offset);
_vm->_resman->closeResource(objRes);
_vm->_resman->closeResource(scriptRes);
return result;
}
int Logic::runScript(byte *scriptData, byte *objectData, uint32 offset) {
byte pc[4];
WRITE_LE_UINT32(pc, offset);
return runScript2(scriptData, objectData, pc);
}
// This form of the runScript function is only called directly from
// the processSession() function, which uses it to update the script PC in the
// current object hub. For reasons which I do not understand, I couldn't get it
// to work if I called the function first with a dummy offset variable, and
// and then updated the object hub myself.
int Logic::runScript2(byte *scriptData, byte *objectData, byte *offsetPtr) {
// Interestingly, unlike our BASS engine the stack is a local variable.
// I don't know whether or not this is relevant to the working of the
// BS2 engine.
int32 stack[STACK_SIZE];
int32 stackPtr = 0;
uint32 offset = READ_LE_UINT32(offsetPtr);
ResHeader header;
header.read(scriptData);
scriptData += ResHeader::size() + ObjectHub::size();
// The script data format:
// int32_TYPE 1 Size of variable space in bytes
// ... The variable space
// int32_TYPE 1 numberOfScripts
// int32_TYPE numberOfScripts The offsets for each script
// Initialise some stuff
uint32 ip = 0; // Code pointer
int scriptNumber;
// Get the start of variables and start of code
byte *localVars = scriptData + 4;
byte *code = scriptData + READ_LE_UINT32(scriptData) + 4;
uint32 noScripts = READ_LE_UINT32(code);
code += 4;
byte *offsetTable = code;
if (offset < noScripts) {
ip = READ_LE_UINT32(offsetTable + offset * 4);
scriptNumber = offset;
debug(8, "Starting script %d from %d", scriptNumber, ip);
} else {
uint i;
ip = offset;
for (i = 1; i < noScripts; i++) {
if (READ_LE_UINT32(offsetTable + 4 * i) >= ip)
break;
}
scriptNumber = i - 1;
debug(8, "Resuming script %d from %d", scriptNumber, ip);
}
// There are a couple of known script bugs related to interacting with
// certain objects. We try to work around a few of them.
bool checkMopBug = false;
bool checkPyramidBug = false;
bool checkElevatorBug = false;
bool checkPearlBug = false;
if (scriptNumber == 2) {
if (strcmp((char *)header.name, "mop_73") == 0)
checkMopBug = true;
else if (strcmp((char *)header.name, "titipoco_81") == 0)
checkPyramidBug = true;
else if (strcmp((char *)header.name, "lift_82") == 0)
checkElevatorBug = true;
else if (strcmp((char *)header.name, "pearl_31") == 0)
checkPearlBug = true;
}
code += noScripts * 4;
// Code should now be pointing at an identifier and a checksum
byte *checksumBlock = code;
code += 4 * 3;
if (READ_LE_UINT32(checksumBlock) != 12345678) {
error("Invalid script in object %s", header.name);
return 0;
}
int32 codeLen = READ_LE_UINT32(checksumBlock + 4);
int32 checksum = 0;
for (int i = 0; i < codeLen; i++)
checksum += (unsigned char) code[i];
if (checksum != (int32)READ_LE_UINT32(checksumBlock + 8)) {
debug(1, "Checksum error in object %s", header.name);
// This could be bad, but there has been a report about someone
// who had problems running the German version because of
// checksum errors. Could there be a version where checksums
// weren't properly calculated?
}
bool runningScript = true;
int parameterReturnedFromMcodeFunction = 0; // Allow scripts to return things
int savedStartOfMcode = 0; // For saving start of mcode commands
while (runningScript) {
int i;
int32 a, b;
int curCommand, parameter, value; // Command and parameter variables
int retVal;
int caseCount;
bool foundCase;
byte *ptr;
curCommand = code[ip++];
switch (curCommand) {
// Script-related opcodes
case CP_END_SCRIPT:
// End the script
runningScript = false;
// WORKAROUND: The dreaded pyramid bug makes the torch
// untakeable when you speak to Titipoco. This is
// because one of the conditions for the torch to be
// takeable is that Titipoco isn't doing anything out
// of the ordinary. Global variable 913 has to be 0 to
// signify that he is in his "idle" state.
//
// Unfortunately, simply the act of speaking to him
// sets variable 913 to 1 (probably to stop him from
// turning around every now and then). The script may
// then go on to set the variable to different values
// to trigger various behaviours in him, but if you
// have run out of these cases the script won't ever
// set it back to 0 again.
//
// So if his click hander finishes, and variable 913 is
// 1, we set it back to 0 manually.
if (checkPyramidBug && readVar(913) == 1) {
warning("Working around pyramid bug: Resetting Titipoco's state");
writeVar(913, 0);
}
// WORKAROUND: The not-so-known-but-should-be-dreaded
// elevator bug.
//
// The click handler for the top of the elevator only
// handles using the elevator, not examining it. When
// examining it, the mouse cursor is removed but never
// restored.
if (checkElevatorBug && readVar(RIGHT_BUTTON)) {
warning("Working around elevator bug: Restoring mouse pointer");
fnAddHuman(NULL);
}
debug(9, "CP_END_SCRIPT");
break;
case CP_QUIT:
// Quit out for a cycle
WRITE_LE_UINT32(offsetPtr, ip);
debug(9, "CP_QUIT");
return 0;
case CP_TERMINATE:
// Quit out immediately without affecting the offset
// pointer
debug(9, "CP_TERMINATE");
return 3;
case CP_RESTART_SCRIPT:
// Start the script again
ip = FROM_LE_32(offsetTable[scriptNumber]);
debug(9, "CP_RESTART_SCRIPT");
break;
// Stack-related opcodes
case CP_PUSH_INT32:
// Push a long word value on to the stack
Read32ip(parameter);
push(parameter);
debug(9, "CP_PUSH_INT32: %d", parameter);
break;
case CP_PUSH_LOCAL_VAR32:
// Push the contents of a local variable
Read16ip(parameter);
push(READ_LE_UINT32(localVars + parameter));
debug(9, "CP_PUSH_LOCAL_VAR32: localVars[%d] => %d", parameter / 4, READ_LE_UINT32(localVars + parameter));
break;
case CP_PUSH_GLOBAL_VAR32:
// Push a global variable
Read16ip(parameter);
push(readVar(parameter));
debug(9, "CP_PUSH_GLOBAL_VAR32: scriptVars[%d] => %d", parameter, readVar(parameter));
break;
case CP_PUSH_LOCAL_ADDR:
// Push the address of a local variable
// From what I understand, some scripts store data
// (e.g. mouse pointers) in their local variable space
// from the very beginning, and use this mechanism to
// pass that data to the opcode function. I don't yet
// know the conceptual difference between this and the
// CP_PUSH_DEREFERENCED_STRUCTURE opcode.
Read16ip(parameter);
push_ptr(localVars + parameter);
debug(9, "CP_PUSH_LOCAL_ADDR: &localVars[%d] => %p", parameter / 4, localVars + parameter);
break;
case CP_PUSH_STRING:
// Push the address of a string on to the stack
// Get the string size
Read8ip(parameter);
// ip now points to the string
ptr = code + ip;
push_ptr(ptr);
debug(9, "CP_PUSH_STRING: \"%s\"", ptr);
ip += (parameter + 1);
break;
case CP_PUSH_DEREFERENCED_STRUCTURE:
// Push the address of a dereferenced structure
Read32ip(parameter);
ptr = objectData + 4 + ResHeader::size() + ObjectHub::size() + parameter;
push_ptr(ptr);
debug(9, "CP_PUSH_DEREFERENCED_STRUCTURE: %d => %p", parameter, ptr);
break;
case CP_POP_LOCAL_VAR32:
// Pop a value into a local word variable
Read16ip(parameter);
value = pop();
WRITE_LE_UINT32(localVars + parameter, value);
debug(9, "CP_POP_LOCAL_VAR32: localVars[%d] = %d", parameter / 4, value);
break;
case CP_POP_GLOBAL_VAR32:
// Pop a global variable
Read16ip(parameter);
value = pop();
// WORKAROUND for bug #1214168: The not-at-all dreaded
// mop bug.
//
// At the London Docks, global variable 1003 keeps
// track of Nico:
//
// 0: Hiding behind the first crate.
// 1: Hiding behind the second crate.
// 2: Standing in plain view on the deck.
// 3: Hiding on the roof.
//
// The bug happens when trying to pick up the mop while
// hiding on the roof. Nico climbs down, the mop is
// picked up, but the variable remains set to 3.
// Visually, everything looks ok. But as far as the
// scripts are concerned, she's still hiding up on the
// roof. This is not fatal, but leads to a number of
// glitches until the state is corrected. E.g. trying
// to climb back up the ladder will cause Nico to climb
// down again.
//
// Global variable 1017 keeps track of the mop. Setting
// it to 2 means that the mop has been picked up. We
// use that as the signal that Nico's state needs to be
// updated as well.
if (checkMopBug && parameter == 1017 && readVar(1003) != 2) {
warning("Working around mop bug: Setting Nico's state");
writeVar(1003, 2);
}
writeVar(parameter, value);
debug(9, "CP_POP_GLOBAL_VAR32: scriptsVars[%d] = %d", parameter, value);
break;
case CP_ADDNPOP_LOCAL_VAR32:
Read16ip(parameter);
value = READ_LE_UINT32(localVars + parameter) + pop();
WRITE_LE_UINT32(localVars + parameter, value);
debug(9, "CP_ADDNPOP_LOCAL_VAR32: localVars[%d] => %d", parameter / 4, value);
break;
case CP_SUBNPOP_LOCAL_VAR32:
Read16ip(parameter);
value = READ_LE_UINT32(localVars + parameter) - pop();
WRITE_LE_UINT32(localVars + parameter, value);
debug(9, "CP_SUBNPOP_LOCAL_VAR32: localVars[%d] => %d", parameter / 4, value);
break;
case CP_ADDNPOP_GLOBAL_VAR32:
// Add and pop a global variable
Read16ip(parameter);
value = readVar(parameter) + pop();
writeVar(parameter, value);
debug(9, "CP_ADDNPOP_GLOBAL_VAR32: scriptVars[%d] => %d", parameter, value);
break;
case CP_SUBNPOP_GLOBAL_VAR32:
// Sub and pop a global variable
Read16ip(parameter);
value = readVar(parameter) - pop();
writeVar(parameter, value);
debug(9, "CP_SUBNPOP_GLOBAL_VAR32: scriptVars[%d] => %d", parameter, value);
break;
// Jump opcodes
case CP_SKIPONTRUE:
// Skip if the value on the stack is true
Read32ipLeaveip(parameter);
value = pop();
if (!value) {
ip += 4;
debug(9, "CP_SKIPONTRUE: %d (IS FALSE (NOT SKIPPED))", parameter);
} else {
ip += parameter;
debug(9, "CP_SKIPONTRUE: %d (IS TRUE (SKIPPED))", parameter);
}
break;
case CP_SKIPONFALSE:
// Skip if the value on the stack is false
Read32ipLeaveip(parameter);
value = pop();
if (value) {
ip += 4;
debug(9, "CP_SKIPONFALSE: %d (IS TRUE (NOT SKIPPED))", parameter);
} else {
ip += parameter;
debug(9, "CP_SKIPONFALSE: %d (IS FALSE (SKIPPED))", parameter);
}
break;
case CP_SKIPALWAYS:
// skip a block
Read32ipLeaveip(parameter);
ip += parameter;
debug(9, "CP_SKIPALWAYS: %d", parameter);
break;
case CP_SWITCH:
// switch
value = pop();
Read32ip(caseCount);
// Search the cases
foundCase = false;
for (i = 0; i < caseCount && !foundCase; i++) {
if (value == (int32)READ_LE_UINT32(code + ip)) {
// We have found the case, so lets
// jump to it
foundCase = true;
ip += READ_LE_UINT32(code + ip + 4);
} else
ip += 4 * 2;
}
// If we found no matching case then use the default
if (!foundCase)
ip += READ_LE_UINT32(code + ip);
debug(9, "CP_SWITCH: [SORRY, NO DEBUG INFO]");
break;
case CP_SAVE_MCODE_START:
// Save the start position on an mcode instruction in
// case we need to restart it again
savedStartOfMcode = ip - 1;
debug(9, "CP_SAVE_MCODE_START");
break;
case CP_CALL_MCODE:
// Call an mcode routine
Read16ip(parameter);
assert(parameter < _numOpcodes);
// amount to adjust stack by (no of parameters)
Read8ip(value);
debug(9, "CP_CALL_MCODE: '%s', %d", _opcodes[parameter].desc, value);
stackPtr -= value;
assert(stackPtr >= 0);
retVal = (this->*_opcodes[parameter].proc)(&stack[stackPtr]);
switch (retVal & 7) {
case IR_STOP:
// Quit out for a cycle
WRITE_LE_UINT32(offsetPtr, ip);
return 0;
case IR_CONT:
// Continue as normal
break;
case IR_TERMINATE:
if (checkPearlBug && readVar(1290) == 0) {
// Pearl's interaction script will wait
// until global(1290) is no longer 0
// before doing anything. But if the
// script was terminated prematurely,
// that never happens.
warning("Working around Pearl bug: Resetting Pearl's state");
writeVar(1290, 1);
}
// Return without updating the offset
return 2;
case IR_REPEAT:
// Return setting offset to start of this
// function call
WRITE_LE_UINT32(offsetPtr, savedStartOfMcode);
return 0;
case IR_GOSUB:
// that's really neat
WRITE_LE_UINT32(offsetPtr, ip);
return 2;
default:
error("Bad return code (%d) from '%s'", retVal & 7, _opcodes[parameter].desc);
}
parameterReturnedFromMcodeFunction = retVal >> 3;
break;
case CP_JUMP_ON_RETURNED:
// Jump to a part of the script depending on
// the return value from an mcode routine
// Get the maximum value
Read8ip(parameter);
debug(9, "CP_JUMP_ON_RETURNED: %d => %d",
parameterReturnedFromMcodeFunction,
READ_LE_UINT32(code + ip + parameterReturnedFromMcodeFunction * 4));
ip += READ_LE_UINT32(code + ip + parameterReturnedFromMcodeFunction * 4);
break;
// Operators
case OP_ISEQUAL:
b = pop();
a = pop();
push(a == b);
debug(9, "OP_ISEQUAL: RESULT = %d", a == b);
break;
case OP_NOTEQUAL:
b = pop();
a = pop();
push(a != b);
debug(9, "OP_NOTEQUAL: RESULT = %d", a != b);
break;
case OP_GTTHAN:
b = pop();
a = pop();
push(a > b);
debug(9, "OP_GTTHAN: RESULT = %d", a > b);
break;
case OP_LSTHAN:
b = pop();
a = pop();
push(a < b);
debug(9, "OP_LSTHAN: RESULT = %d", a < b);
break;
case OP_GTTHANE:
b = pop();
a = pop();
push(a >= b);
debug(9, "OP_GTTHANE: RESULT = %d", a >= b);
break;
case OP_LSTHANE:
b = pop();
a = pop();
push(a <= b);
debug(9, "OP_LSTHANE: RESULT = %d", a <= b);
break;
case OP_PLUS:
b = pop();
a = pop();
push(a + b);
debug(9, "OP_PLUS: RESULT = %d", a + b);
break;
case OP_MINUS:
b = pop();
a = pop();
push(a - b);
debug(9, "OP_MINUS: RESULT = %d", a - b);
break;
case OP_TIMES:
b = pop();
a = pop();
push(a * b);
debug(9, "OP_TIMES: RESULT = %d", a * b);
break;
case OP_DIVIDE:
b = pop();
a = pop();
push(a / b);
debug(9, "OP_DIVIDE: RESULT = %d", a / b);
break;
case OP_ANDAND:
b = pop();
a = pop();
push(a && b);
debug(9, "OP_ANDAND: RESULT = %d", a && b);
break;
case OP_OROR:
b = pop();
a = pop();
push(a || b);
debug(9, "OP_OROR: RESULT = %d", a || b);
break;
// Debugging opcodes, I think
case CP_DEBUGON:
debug(9, "CP_DEBUGON");
break;
case CP_DEBUGOFF:
debug(9, "CP_DEBUGOFF");
break;
case CP_TEMP_TEXT_PROCESS:
Read32ip(parameter);
debug(9, "CP_TEMP_TEXT_PROCESS: %d", parameter);
break;
default:
error("Invalid script command %d", curCommand);
return 3;
}
}
return 1;
}
} // End of namespace Sword2