int Logic::runScript(byte *scriptData, byte *objectData, uint32 offset) { byte pc[4]; WRITE_LE_UINT32(pc, offset); return runScript2(scriptData, objectData, pc); }
int Logic::runScript2(byte *scriptData, byte *objectData, byte *offsetPtr) { int i; // 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. Common::FixedStack<int32, STACK_SIZE> stack; int32 opcodeParams[STACK_SIZE]; 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 { ip = offset; for (i = 1; i < (int)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 (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) { 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); stack.push(parameter); debug(9, "CP_PUSH_INT32: %d", parameter); break; case CP_PUSH_LOCAL_VAR32: // Push the contents of a local variable Read16ip(parameter); stack.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); stack.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); stack.push(_vm->_memory->encodePtr(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; stack.push(_vm->_memory->encodePtr(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; stack.push(_vm->_memory->encodePtr(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 = stack.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 = stack.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) + stack.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) - stack.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) + stack.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) - stack.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 = stack.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 = stack.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 = stack.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); // The scripts do not always call the mcode command // with as many parameters as it can accept. To keep // things predictable, initialise the remaining // parameters to 0. for (i = STACK_SIZE - 1; i >= value; i--) { opcodeParams[i] = 0; } while (--value >= 0) { opcodeParams[value] = stack.pop(); } retVal = (this->*_opcodes[parameter].proc)(opcodeParams); 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 = stack.pop(); a = stack.pop(); stack.push(a == b); debug(9, "OP_ISEQUAL: RESULT = %d", a == b); break; case OP_NOTEQUAL: b = stack.pop(); a = stack.pop(); stack.push(a != b); debug(9, "OP_NOTEQUAL: RESULT = %d", a != b); break; case OP_GTTHAN: b = stack.pop(); a = stack.pop(); stack.push(a > b); debug(9, "OP_GTTHAN: RESULT = %d", a > b); break; case OP_LSTHAN: b = stack.pop(); a = stack.pop(); stack.push(a < b); debug(9, "OP_LSTHAN: RESULT = %d", a < b); break; case OP_GTTHANE: b = stack.pop(); a = stack.pop(); stack.push(a >= b); debug(9, "OP_GTTHANE: RESULT = %d", a >= b); break; case OP_LSTHANE: b = stack.pop(); a = stack.pop(); stack.push(a <= b); debug(9, "OP_LSTHANE: RESULT = %d", a <= b); break; case OP_PLUS: b = stack.pop(); a = stack.pop(); stack.push(a + b); debug(9, "OP_PLUS: RESULT = %d", a + b); break; case OP_MINUS: b = stack.pop(); a = stack.pop(); stack.push(a - b); debug(9, "OP_MINUS: RESULT = %d", a - b); break; case OP_TIMES: b = stack.pop(); a = stack.pop(); stack.push(a * b); debug(9, "OP_TIMES: RESULT = %d", a * b); break; case OP_DIVIDE: b = stack.pop(); a = stack.pop(); stack.push(a / b); debug(9, "OP_DIVIDE: RESULT = %d", a / b); break; case OP_ANDAND: b = stack.pop(); a = stack.pop(); stack.push(a && b); debug(9, "OP_ANDAND: RESULT = %d", a && b); break; case OP_OROR: b = stack.pop(); a = stack.pop(); stack.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; // for compilers that don't support NORETURN } } return 1; }