/* 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. * * 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. * */ #include "common/config-manager.h" #include "common/util.h" #include "common/system.h" #include "scumm/actor.h" #include "scumm/object.h" #include "scumm/resource.h" #include "scumm/util.h" #include "scumm/scumm_v0.h" #include "scumm/scumm_v2.h" #include "scumm/sound.h" #include "scumm/verbs.h" namespace Scumm { /* Start executing script 'script' with the given parameters */ void ScummEngine::runScript(int script, bool freezeResistant, bool recursive, int *lvarptr, int cycle) { ScriptSlot *s; //byte *scriptPtr; uint32 scriptOffs; byte scriptType; int slot; if (!script) return; if (!recursive) stopScript(script); uint16 number = (_currentScript != 0xFF) ? vm.slot[_currentScript].number : 0; if (script < _numGlobalScripts) { // Call getResourceAddress to ensure the resource is loaded & its usage count reset /*scriptPtr =*/ getResourceAddress(rtScript, script); scriptOffs = _resourceHeaderSize; scriptType = WIO_GLOBAL; debugC(DEBUG_SCRIPTS, "runScript(Global-%d) from %d-%d", script, number, _roomResource); } else { scriptOffs = _localScriptOffsets[script - _numGlobalScripts]; if (scriptOffs == 0) error("Local script %d is not in room %d", script, _roomResource); scriptType = WIO_LOCAL; debugC(DEBUG_SCRIPTS, "runScript(%d) from %d-%d", script, number, _roomResource); } if (cycle == 0) cycle = (_game.heversion >= 90) ? VAR(VAR_SCRIPT_CYCLE) : 1; slot = getScriptSlot(); s = &vm.slot[slot]; s->number = script; s->offs = scriptOffs; s->status = ssRunning; s->where = scriptType; s->freezeResistant = freezeResistant; s->recursive = recursive; s->freezeCount = 0; s->delayFrameCount = 0; s->cycle = cycle; initializeLocals(slot, lvarptr); runScriptNested(slot); } void ScummEngine::runObjectScript(int object, int entry, bool freezeResistant, bool recursive, int *vars, int slot, int cycle) { ScriptSlot *s; uint32 obcd; int where, offs; if (!object) return; if (!recursive && (_game.version >= 3)) stopObjectScript(object); where = whereIsObject(object); if (where == WIO_NOT_FOUND) { warning("Code for object %d not in room %d", object, _roomResource); return; } obcd = getOBCDOffs(object); // Find a free object slot, unless one was specified if (slot == -1) slot = getScriptSlot(); offs = getVerbEntrypoint(object, entry); if (offs == 0) return; if (cycle == 0) cycle = (_game.heversion >= 90) ? VAR(VAR_SCRIPT_CYCLE) : 1; s = &vm.slot[slot]; s->number = object; s->offs = obcd + offs; s->status = ssRunning; s->where = where; s->freezeResistant = freezeResistant; s->recursive = recursive; s->freezeCount = 0; s->delayFrameCount = 0; s->cycle = cycle; initializeLocals(slot, vars); runScriptNested(slot); } void ScummEngine::initializeLocals(int slot, int *vars) { int i; if (!vars) { for (i = 0; i < NUM_SCRIPT_LOCAL; i++) vm.localvar[slot][i] = 0; } else { for (i = 0; i < NUM_SCRIPT_LOCAL; i++) vm.localvar[slot][i] = vars[i]; } } int ScummEngine::getVerbEntrypoint(int obj, int entry) { const byte *objptr, *verbptr; int verboffs; // WORKAROUND for bug #1555938: Disallow pulling the rope if it's // already in the player's inventory. if (_game.id == GID_MONKEY2 && obj == 1047 && entry == 6 && whereIsObject(obj) == WIO_INVENTORY) { return 0; } if (whereIsObject(obj) == WIO_NOT_FOUND) return 0; objptr = getOBCDFromObject(obj); assert(objptr); if (_game.version == 0) verbptr = objptr + 14; else if (_game.version <= 2) verbptr = objptr + 15; else if ((_game.id == GID_LOOM && _game.platform == Common::kPlatformPCEngine) || _game.features & GF_OLD_BUNDLE) verbptr = objptr + 17; else if (_game.features & GF_SMALL_HEADER) verbptr = objptr + 19; else verbptr = findResource(MKTAG('V','E','R','B'), objptr); assert(verbptr); verboffs = verbptr - objptr; if (!(_game.features & GF_SMALL_HEADER)) verbptr += _resourceHeaderSize; if (_game.version == 8) { const uint32 *ptr = (const uint32 *)verbptr; uint32 verb; do { verb = READ_LE_UINT32(ptr); if (!verb) return 0; if (verb == (uint32)entry || verb == 0xFFFFFFFF) break; ptr += 2; } while (1); return verboffs + 8 + READ_LE_UINT32(ptr + 1); } else if (_game.version <= 2) { do { const int kFallbackEntry = (_game.version == 0 ? 0x0F : 0xFF); if (!*verbptr) return 0; if (*verbptr == entry || *verbptr == kFallbackEntry) break; verbptr += 2; } while (1); return *(verbptr + 1); } else { do { if (!*verbptr) return 0; if (*verbptr == entry || *verbptr == 0xFF) break; verbptr += 3; } while (1); if (_game.id == GID_LOOM && _game.platform == Common::kPlatformPCEngine) { verbptr += READ_LE_UINT16(verbptr + 1) + 3; return verbptr - objptr; } else if (_game.features & GF_SMALL_HEADER) return READ_LE_UINT16(verbptr + 1); else return verboffs + READ_LE_UINT16(verbptr + 1); } } /* Stop script 'script' */ void ScummEngine::stopScript(int script) { ScriptSlot *ss; int i; if (script == 0) return; ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) { if (script == ss->number && ss->status != ssDead && (ss->where == WIO_GLOBAL || ss->where == WIO_LOCAL)) { if (ss->cutsceneOverride) if (_game.version >= 5) error("Script %d stopped with active cutscene/override", script); ss->number = 0; ss->status = ssDead; nukeArrays(i); if (_currentScript == i) _currentScript = 0xFF; } } for (i = 0; i < vm.numNestedScripts; ++i) { if (vm.nest[i].number == script && (vm.nest[i].where == WIO_GLOBAL || vm.nest[i].where == WIO_LOCAL)) { nukeArrays(vm.nest[i].slot); vm.nest[i].number = 0; vm.nest[i].slot = 0xFF; vm.nest[i].where = 0xFF; } } } /* Stop an object script 'script'*/ void ScummEngine::stopObjectScript(int script) { ScriptSlot *ss; int i; if (script == 0) return; ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) { if (script == ss->number && ss->status != ssDead && (ss->where == WIO_ROOM || ss->where == WIO_INVENTORY || ss->where == WIO_FLOBJECT)) { if (ss->cutsceneOverride) if (_game.version >= 5) error("Object %d stopped with active cutscene/override", script); ss->number = 0; ss->status = ssDead; nukeArrays(i); if (_currentScript == i) _currentScript = 0xFF; } } for (i = 0; i < vm.numNestedScripts; ++i) { if (vm.nest[i].number == script && (vm.nest[i].where == WIO_ROOM || vm.nest[i].where == WIO_INVENTORY || vm.nest[i].where == WIO_FLOBJECT)) { nukeArrays(vm.nest[i].slot); vm.nest[i].number = 0; vm.nest[i].slot = 0xFF; vm.nest[i].where = 0xFF; } } } /* Return a free script slot */ int ScummEngine::getScriptSlot() { ScriptSlot *s; int i; for (i = 1; i < NUM_SCRIPT_SLOT; i++) { s = &vm.slot[i]; if (s->status == ssDead) return i; } error("Too many scripts running, %d max", NUM_SCRIPT_SLOT); return -1; } /* Run script 'script' nested - eg, within the parent script.*/ void ScummEngine::runScriptNested(int script) { NestedScript *nest; ScriptSlot *slot; updateScriptPtr(); if (vm.numNestedScripts >= kMaxScriptNesting) error("Too many nested scripts"); nest = &vm.nest[vm.numNestedScripts]; if (_currentScript == 0xFF) { nest->number = 0; nest->where = 0xFF; } else { // Store information about the currently running script slot = &vm.slot[_currentScript]; nest->number = slot->number; nest->where = slot->where; nest->slot = _currentScript; } vm.numNestedScripts++; _currentScript = script; getScriptBaseAddress(); resetScriptPointer(); executeScript(); if (vm.numNestedScripts != 0) vm.numNestedScripts--; if (nest->number) { // Try to resume the script which called us, if its status has not changed // since it invoked us. In particular, we only resume it if it hasn't been // stopped in the meantime, and if it did not already move on. slot = &vm.slot[nest->slot]; if (slot->number == nest->number && slot->where == nest->where && slot->status != ssDead && slot->freezeCount == 0) { _currentScript = nest->slot; getScriptBaseAddress(); resetScriptPointer(); return; } } _currentScript = 0xFF; } void ScummEngine::updateScriptPtr() { if (_currentScript == 0xFF) return; vm.slot[_currentScript].offs = _scriptPointer - _scriptOrgPointer; } /* Nuke arrays based on script */ void ScummEngine::nukeArrays(byte scriptSlot) { int i; if (_game.heversion == 0 || scriptSlot == 0) return; for (i = 1; i < _numArray; i++) { if (_arraySlot[i] == scriptSlot) { _res->nukeResource(rtString, i); _arraySlot[i] = 0; } } } /* Get the code pointer to a script */ void ScummEngine::getScriptBaseAddress() { ScriptSlot *ss; int idx; if (_currentScript == 0xFF) return; ss = &vm.slot[_currentScript]; switch (ss->where) { case WIO_INVENTORY: /* inventory script * */ for (idx = 0; idx < _numInventory; idx++) if (_inventory[idx] == ss->number) break; _scriptOrgPointer = getResourceAddress(rtInventory, idx); assert(idx < _numInventory); _lastCodePtr = &_res->_types[rtInventory][idx]._address; break; case WIO_LOCAL: case WIO_ROOM: /* room script */ if (_game.version == 8) { _scriptOrgPointer = getResourceAddress(rtRoomScripts, _roomResource); assert(_roomResource < (int)_res->_types[rtRoomScripts].size()); _lastCodePtr = &_res->_types[rtRoomScripts][_roomResource]._address; } else { _scriptOrgPointer = getResourceAddress(rtRoom, _roomResource); assert(_roomResource < _numRooms); _lastCodePtr = &_res->_types[rtRoom][_roomResource]._address; } break; case WIO_GLOBAL: /* global script */ _scriptOrgPointer = getResourceAddress(rtScript, ss->number); assert(ss->number < _numScripts); _lastCodePtr = &_res->_types[rtScript][ss->number]._address; break; case WIO_FLOBJECT: /* flobject script */ idx = getObjectIndex(ss->number); assert(idx != -1); idx = _objs[idx].fl_object_index; _scriptOrgPointer = getResourceAddress(rtFlObject, idx); assert(idx < _numFlObject); _lastCodePtr = &_res->_types[rtFlObject][idx]._address; break; default: error("Bad type while getting base address"); } // The following fixes bug #1202487. Confirmed against disasm. if (_game.version <= 2 && _scriptOrgPointer == NULL) { ss->status = ssDead; _currentScript = 0xFF; } } void ScummEngine::resetScriptPointer() { if (_currentScript == 0xFF) return; _scriptPointer = _scriptOrgPointer + vm.slot[_currentScript].offs; } /** * This method checks whether the resource that contains the active script * moved, and if so, updates the script pointer accordingly. * * The script resource may have moved because it might have been garbage * collected by ResourceManager::expireResources. */ void ScummEngine::refreshScriptPointer() { if (*_lastCodePtr != _scriptOrgPointer) { long oldoffs = _scriptPointer - _scriptOrgPointer; getScriptBaseAddress(); _scriptPointer = _scriptOrgPointer + oldoffs; } } /** Execute a script - Read opcode, and execute it from the table */ void ScummEngine::executeScript() { int c; while (_currentScript != 0xFF) { if (_showStack == 1) { debugN("Stack:"); for (c = 0; c < _scummStackPos; c++) { debugN(" %d", _vmStack[c]); } debugN("\n"); } _opcode = fetchScriptByte(); if (_game.version > 2) // V0-V2 games didn't use the didexec flag vm.slot[_currentScript].didexec = true; debugC(DEBUG_OPCODES, "Script %d, offset 0x%x: [%X] %s()", vm.slot[_currentScript].number, (uint)(_scriptPointer - _scriptOrgPointer), _opcode, getOpcodeDesc(_opcode)); if (_hexdumpScripts == true) { for (c = -1; c < 15; c++) { debugN(" %02x", *(_scriptPointer + c)); } debugN("\n"); } executeOpcode(_opcode); } } void ScummEngine::executeOpcode(byte i) { if (_opcodes[i].proc && _opcodes[i].proc->isValid()) (*_opcodes[i].proc)(); else { error("Invalid opcode '%x' at %lx", i, (long)(_scriptPointer - _scriptOrgPointer)); } } const char *ScummEngine::getOpcodeDesc(byte i) { #ifndef REDUCE_MEMORY_USAGE return _opcodes[i].desc; #else return ""; #endif } byte ScummEngine::fetchScriptByte() { refreshScriptPointer(); return *_scriptPointer++; } uint ScummEngine::fetchScriptWord() { refreshScriptPointer(); uint a = READ_LE_UINT16(_scriptPointer); _scriptPointer += 2; return a; } int ScummEngine::fetchScriptWordSigned() { return (int16)fetchScriptWord(); } uint ScummEngine::fetchScriptDWord() { refreshScriptPointer(); uint a = READ_LE_UINT32(_scriptPointer); _scriptPointer += 4; return a; } int ScummEngine::fetchScriptDWordSigned() { return (int32)fetchScriptDWord(); } int ScummEngine::readVar(uint var) { int a; debugC(DEBUG_VARS, "readvar(%d)", var); if ((var & 0x2000) && (_game.version <= 5)) { a = fetchScriptWord(); if (a & 0x2000) var += readVar(a & ~0x2000); else var += a & 0xFFF; var &= ~0x2000; } if (!(var & 0xF000)) { if (!_copyProtection) { if (var == 490 && _game.id == GID_MONKEY2) { var = 518; } } if (VAR_SUBTITLES != 0xFF && var == VAR_SUBTITLES) { return ConfMan.getBool("subtitles"); } if (VAR_NOSUBTITLES != 0xFF && var == VAR_NOSUBTITLES) { return !ConfMan.getBool("subtitles"); } assertRange(0, var, _numVariables - 1, "variable (reading)"); return _scummVars[var]; } if (var & 0x8000) { if (_game.heversion >= 80) { var &= 0xFFF; assertRange(0, var, _numRoomVariables - 1, "room variable (reading)"); return _roomVars[var]; } else if (_game.version <= 3 && !(_game.id == GID_INDY3 && _game.platform == Common::kPlatformFMTowns) && !(_game.id == GID_LOOM && _game.platform == Common::kPlatformPCEngine)) { int bit = var & 0xF; var = (var >> 4) & 0xFF; if (!_copyProtection) { if (_game.id == GID_LOOM && (_game.platform == Common::kPlatformFMTowns) && var == 214 && bit == 15) { return 0; } else if (_game.id == GID_ZAK && (_game.platform == Common::kPlatformFMTowns) && var == 151 && bit == 8) { return 0; } } assertRange(0, var, _numVariables - 1, "variable (reading)"); return (_scummVars[ var ] & ( 1 << bit ) ) ? 1 : 0; } else { var &= 0x7FFF; if (!_copyProtection) { if (_game.id == GID_INDY3 && (_game.platform == Common::kPlatformFMTowns) && var == 1508) return 0; } assertRange(0, var, _numBitVariables - 1, "variable (reading)"); return (_bitVars[var >> 3] & (1 << (var & 7))) ? 1 : 0; } } if (var & 0x4000) { if (_game.features & GF_FEW_LOCALS) { var &= 0xF; } else { var &= 0xFFF; } if (_game.heversion >= 80) assertRange(0, var, 25, "local variable (reading)"); else assertRange(0, var, 20, "local variable (reading)"); return vm.localvar[_currentScript][var]; } error("Illegal varbits (r)"); return -1; } void ScummEngine::writeVar(uint var, int value) { debugC(DEBUG_VARS, "writeVar(%d, %d)", var, value); if (!(var & 0xF000)) { assertRange(0, var, _numVariables - 1, "variable (writing)"); if (VAR_SUBTITLES != 0xFF && var == VAR_SUBTITLES) { // Ignore default setting in HE72-74 games if (_game.heversion <= 74 && vm.slot[_currentScript].number == 1) return; assert(value == 0 || value == 1); ConfMan.setBool("subtitles", (value != 0)); } if (VAR_NOSUBTITLES != 0xFF && var == VAR_NOSUBTITLES) { // Ignore default setting in HE60-71 games if (_game.heversion >= 60 && vm.slot[_currentScript].number == 1) return; assert(value == 0 || value == 1); ConfMan.setBool("subtitles", !value); } if (var == VAR_CHARINC) { // Did the user override the talkspeed manually? Then use that. // Otherwise, use the value specified by the game script. // Note: To determine whether there was a user override, we only // look at the target specific settings, assuming that any global // value is likely to be bogus. See also bug #2251765. if (ConfMan.hasKey("talkspeed", _targetName)) { value = getTalkSpeed(); } else { // Save the new talkspeed value to ConfMan setTalkSpeed(value); } } _scummVars[var] = value; if ((_varwatch == (int)var) || (_varwatch == 0)) { if (vm.slot[_currentScript].number < 100) debug(1, "vars[%d] = %d (via script-%d)", var, value, vm.slot[_currentScript].number); else debug(1, "vars[%d] = %d (via room-%d-%d)", var, value, _currentRoom, vm.slot[_currentScript].number); } return; } if (var & 0x8000) { if (_game.heversion >= 80) { var &= 0xFFF; assertRange(0, var, _numRoomVariables - 1, "room variable (writing)"); _roomVars[var] = value; } else if (_game.version <= 3 && !(_game.id == GID_INDY3 && _game.platform == Common::kPlatformFMTowns) && !(_game.id == GID_LOOM && _game.platform == Common::kPlatformPCEngine)) { // In the old games, the bit variables were using the same memory // as the normal variables! int bit = var & 0xF; var = (var >> 4) & 0xFF; assertRange(0, var, _numVariables - 1, "variable (writing)"); if (value) _scummVars[var] |= ( 1 << bit ); else _scummVars[var] &= ~( 1 << bit ); } else { var &= 0x7FFF; assertRange(0, var, _numBitVariables - 1, "bit variable (writing)"); if (value) _bitVars[var >> 3] |= (1 << (var & 7)); else _bitVars[var >> 3] &= ~(1 << (var & 7)); } return; } if (var & 0x4000) { if (_game.features & GF_FEW_LOCALS) { var &= 0xF; } else { var &= 0xFFF; } if (_game.heversion >= 80) assertRange(0, var, 25, "local variable (writing)"); else assertRange(0, var, 20, "local variable (writing)"); vm.localvar[_currentScript][var] = value; return; } error("Illegal varbits (w)"); } void ScummEngine::push(int a) { assert(_scummStackPos >= 0 && _scummStackPos < ARRAYSIZE(_vmStack)); _vmStack[_scummStackPos++] = a; } int ScummEngine::pop() { assert(_scummStackPos >= 1 && _scummStackPos <= ARRAYSIZE(_vmStack)); return _vmStack[--_scummStackPos]; } void ScummEngine::stopObjectCode() { ScriptSlot *ss; ss = &vm.slot[_currentScript]; if (_game.version <= 2) { if (ss->where == WIO_GLOBAL || ss->where == WIO_LOCAL) { stopScript(ss->number); } else { ss->number = 0; ss->status = ssDead; } } else if (_game.version <= 5) { if (ss->where != WIO_GLOBAL && ss->where != WIO_LOCAL) { stopObjectScript(ss->number); } else { if (_game.version >= 4 && ss->cutsceneOverride) error("Script %d ending with active cutscene/override (%d)", ss->number, ss->cutsceneOverride); ss->number = 0; ss->status = ssDead; } } else { if (ss->where != WIO_GLOBAL && ss->where != WIO_LOCAL) { if (ss->cutsceneOverride) error("Object %d ending with active cutscene/override (%d)", ss->number, ss->cutsceneOverride); } else { if (ss->cutsceneOverride) error("Script %d ending with active cutscene/override (%d)", ss->number, ss->cutsceneOverride); } ss->number = 0; ss->status = ssDead; } nukeArrays(_currentScript); _currentScript = 0xFF; } void ScummEngine::runInventoryScript(int i) { if (VAR(VAR_INVENTORY_SCRIPT)) { if (_game.id == GID_INDY3 && _game.platform == Common::kPlatformMacintosh) { inventoryScriptIndy3Mac(); } else { int args[NUM_SCRIPT_LOCAL]; memset(args, 0, sizeof(args)); args[0] = i; runScript(VAR(VAR_INVENTORY_SCRIPT), 0, 0, args); } } } void ScummEngine::inventoryScriptIndy3Mac() { int slot; // VAR(67) controls the scroll offset of the inventory in Indy3 for Macintosh. // The inventory consists of two columns with three items visible in each, // so a maximum of six items are visible at once. // The scroll offset must be non-negative and if there are six or less items // in the inventory, the inventory is fixed in the top position. const int invCount = getInventoryCount(VAR(VAR_EGO)); if (VAR(67) < 0 || invCount <= 6) { VAR(67) = 0; } // If there are more than six items in the inventory, clamp the scroll position // to be at most invCount-6, rounded up to the next even integer. bool scrolledToBottom = false; if (invCount > 6 && VAR(67) >= invCount - 6) { VAR(67) = invCount - 6; // Odd number of inventory items? -> increment VAR(67) to make it even if (invCount & 1) { VAR(67)++; } scrolledToBottom = true; } // Now update var 83 till 89 to contain the inventory IDs of the // corresponding inventory slots. // Also setup fake verbs for the inventory byte tmp[6] = { 0xFF, 0x06, 0x52, 0x00, 0x00, 0x00 }; for (int j = 1; j < 7; j++) { int tmpA = (VAR(67) + j); int tmpB = findInventory(VAR(VAR_EGO), tmpA); VAR(82 + j) = tmpB; // Setup fake verb tmp[2] = 0x52 + j; slot = getVerbSlot(100 + j, 0); loadPtrToResource(rtVerb, slot, tmp); VerbSlot *vs = &_verbs[slot]; vs->type = kTextVerbType; vs->imgindex = 0; vs->curmode = 1; drawVerb(slot, 0); } // Enable up arrow if there are more than six items and we are not already // scrolled all the way up. slot = getVerbSlot(107, 0); _verbs[slot].curmode = (invCount > 6 && VAR(67)) ? 1 : 0; drawVerb(slot, 0); // Enable down arrow if there are more than six items and we are not already // scrolled all the way down. slot = getVerbSlot(108, 0); _verbs[slot].curmode = (invCount > 6 && !scrolledToBottom) ? 1 : 0; drawVerb(slot, 0); // Redraw! verbMouseOver(0); } void ScummEngine::freezeScripts(int flag) { int i; if (_game.version <= 2) { for (i = 0; i < NUM_SCRIPT_SLOT; i++) { if (_currentScript != i && vm.slot[i].status != ssDead && !vm.slot[i].freezeResistant) { vm.slot[i].status |= 0x80; } } return; } for (i = 0; i < NUM_SCRIPT_SLOT; i++) { if (_currentScript != i && vm.slot[i].status != ssDead && (!vm.slot[i].freezeResistant || flag >= 0x80)) { vm.slot[i].status |= 0x80; vm.slot[i].freezeCount++; } } for (i = 0; i < NUM_SENTENCE; i++) _sentence[i].freezeCount++; if (vm.cutSceneScriptIndex != 0xFF) { vm.slot[vm.cutSceneScriptIndex].status &= 0x7F; vm.slot[vm.cutSceneScriptIndex].freezeCount = 0; } } void ScummEngine::unfreezeScripts() { int i; if (_game.version <= 2) { for (i = 0; i < NUM_SCRIPT_SLOT; i++) { vm.slot[i].status &= 0x7F; } return; } for (i = 0; i < NUM_SCRIPT_SLOT; i++) { if (vm.slot[i].status & 0x80) { if (!--vm.slot[i].freezeCount) { vm.slot[i].status &= 0x7F; } } } for (i = 0; i < NUM_SENTENCE; i++) { if (_sentence[i].freezeCount > 0) _sentence[i].freezeCount--; } } void ScummEngine::runAllScripts() { int i; for (i = 0; i < NUM_SCRIPT_SLOT; i++) vm.slot[i].didexec = false; _currentScript = 0xFF; int numCycles = (_game.heversion >= 90) ? VAR(VAR_NUM_SCRIPT_CYCLES) : 1; for (int cycle = 1; cycle <= numCycles; cycle++) { for (i = 0; i < NUM_SCRIPT_SLOT; i++) { if (vm.slot[i].cycle == cycle && vm.slot[i].status == ssRunning && !vm.slot[i].didexec) { _currentScript = (byte)i; getScriptBaseAddress(); resetScriptPointer(); executeScript(); } } } } void ScummEngine::runExitScript() { if (VAR_EXIT_SCRIPT != 0xFF && VAR(VAR_EXIT_SCRIPT)) runScript(VAR(VAR_EXIT_SCRIPT), 0, 0, 0); if (_EXCD_offs) { int slot = getScriptSlot(); vm.slot[slot].status = ssRunning; vm.slot[slot].number = 10001; vm.slot[slot].where = WIO_ROOM; vm.slot[slot].offs = _EXCD_offs; vm.slot[slot].freezeResistant = false; vm.slot[slot].recursive = false; vm.slot[slot].freezeCount = 0; vm.slot[slot].delayFrameCount = 0; vm.slot[slot].cycle = 1; // FIXME: the exit script of room 7 in indy3 only seems to have a size // and tag not actual data not even a 00 (stop code). Maybe we should // be limiting ourselves to strictly reading the size from the header? if (_game.id == GID_INDY3 && !(_game.features & GF_OLD_BUNDLE)) { byte *roomptr = getResourceAddress(rtRoom, _roomResource); const byte *excd = findResourceData(MKTAG('E','X','C','D'), roomptr) - _resourceHeaderSize; if (!excd || (getResourceDataSize(excd) < 1)) { debug(2, "Exit-%d is empty", _roomResource); return; } } initializeLocals(slot, 0); runScriptNested(slot); } if (VAR_EXIT_SCRIPT2 != 0xFF && VAR(VAR_EXIT_SCRIPT2)) runScript(VAR(VAR_EXIT_SCRIPT2), 0, 0, 0); #ifdef ENABLE_SCUMM_7_8 // WORKAROUND: The spider lair (room 44) will optionally play the sound // of trickling water (sound 215), but it never stops it. The same sound // effect is also used in room 33, so let's do the same fade out that it // does in that room's exit script. if (_game.id == GID_DIG && _currentRoom == 44) { int scriptCmds[] = { 14, 215, 0x600, 0, 30, 0, 0, 0 }; _sound->soundKludge(scriptCmds, ARRAYSIZE(scriptCmds)); } #endif } void ScummEngine::runEntryScript() { if (VAR_ENTRY_SCRIPT != 0xFF && VAR(VAR_ENTRY_SCRIPT)) runScript(VAR(VAR_ENTRY_SCRIPT), 0, 0, 0); if (_ENCD_offs) { int slot = getScriptSlot(); vm.slot[slot].status = ssRunning; vm.slot[slot].number = 10002; vm.slot[slot].where = WIO_ROOM; vm.slot[slot].offs = _ENCD_offs; vm.slot[slot].freezeResistant = false; vm.slot[slot].recursive = false; vm.slot[slot].freezeCount = 0; vm.slot[slot].delayFrameCount = 0; vm.slot[slot].cycle = 1; initializeLocals(slot, 0); runScriptNested(slot); } if (VAR_ENTRY_SCRIPT2 != 0xFF && VAR(VAR_ENTRY_SCRIPT2)) runScript(VAR(VAR_ENTRY_SCRIPT2), 0, 0, 0); } void ScummEngine::runQuitScript() { if (VAR_QUIT_SCRIPT != 0xFF && VAR(VAR_QUIT_SCRIPT)) { int args[NUM_SCRIPT_LOCAL]; memset(args, 0, sizeof(args)); args[0] = 2; args[1] = 1003; runScript(VAR(VAR_QUIT_SCRIPT), 0, 0, args); } } void ScummEngine::killScriptsAndResources() { ScriptSlot *ss; int i; ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) { if (ss->where == WIO_ROOM || ss->where == WIO_FLOBJECT) { if (ss->cutsceneOverride) { if (_game.version >= 5) warning("Object %d stopped with active cutscene/override in exit", ss->number); ss->cutsceneOverride = 0; } nukeArrays(i); ss->status = ssDead; } else if (ss->where == WIO_LOCAL) { if (ss->cutsceneOverride) { if (_game.version >= 5) warning("Script %d stopped with active cutscene/override in exit", ss->number); ss->cutsceneOverride = 0; } nukeArrays(i); ss->status = ssDead; } } /* Nuke local object names */ if (_newNames) { for (i = 0; i < _numNewNames; i++) { const int obj = _newNames[i]; if (obj) { const int owner = getOwner((_game.version != 0 ? obj : OBJECT_V0_ID(obj))); // We can delete custom name resources if either the object is // no longer in use (i.e. not owned by anyone anymore); or if // it is an object which is owned by a room. if (owner == 0 || (_game.version < 7 && owner == OF_OWNER_ROOM)) { // WORKAROUND for a problem mentioned in bug report #941275: // In FOA in the sentry room, in the chest plate of the statue, // the pegs may be renamed to mouth: this custom name is lost // when leaving the room; this hack prevents this). if (owner == OF_OWNER_ROOM && _game.id == GID_INDY4 && 336 <= obj && obj <= 340) continue; _newNames[i] = 0; _res->nukeResource(rtObjectName, i); } } } } } void ScummEngine::killAllScriptsExceptCurrent() { for (int i = 0; i < NUM_SCRIPT_SLOT; i++) { if (i != _currentScript) { vm.slot[i].status = ssDead; vm.slot[i].cutsceneOverride = 0; } } } void ScummEngine::doSentence(int verb, int objectA, int objectB) { SentenceTab *st; if (_game.version >= 7) { if (objectA == objectB) return; if (_sentenceNum > 0) { st = &_sentence[_sentenceNum - 1]; // Check if this doSentence request is identical to the previous one; // if yes, ignore this invocation. if (_sentenceNum && st->verb == verb && st->objectA == objectA && st->objectB == objectB) return; } } assert(_sentenceNum < NUM_SENTENCE); st = &_sentence[_sentenceNum++]; st->verb = verb; st->objectA = objectA; st->objectB = objectB; st->preposition = (objectB != 0); st->freezeCount = 0; } void ScummEngine::checkAndRunSentenceScript() { int i; int localParamList[NUM_SCRIPT_LOCAL]; const ScriptSlot *ss; int sentenceScript; if (_game.version <= 2) sentenceScript = 2; else sentenceScript = VAR(VAR_SENTENCE_SCRIPT); memset(localParamList, 0, sizeof(localParamList)); if (isScriptInUse(sentenceScript)) { ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) if (ss->number == sentenceScript && ss->status != ssDead && ss->freezeCount == 0) return; } if (!_sentenceNum || _sentence[_sentenceNum - 1].freezeCount) return; _sentenceNum--; SentenceTab &st = _sentence[_sentenceNum]; if (_game.version < 7) if (st.preposition && st.objectB == st.objectA) return; if (_game.version <= 2) { VAR(VAR_ACTIVE_VERB) = st.verb; VAR(VAR_ACTIVE_OBJECT1) = st.objectA; VAR(VAR_ACTIVE_OBJECT2) = st.objectB; VAR(VAR_VERB_ALLOWED) = (0 != getVerbEntrypoint(st.objectA, st.verb)); } else { localParamList[0] = st.verb; localParamList[1] = st.objectA; localParamList[2] = st.objectB; if (_game.id == GID_FT && !isValidActor(localParamList[1]) && !isValidActor(localParamList[2])) { // WORKAROUND for bug #1407789. The buggy script clearly // assumes that one of the two objects is an actor. If that's // not the case, fall back on the default sentence script. // FIXME: We do not yet have all necessary information, but the // following is known: // // * The US PC version uses scripts 28 and 103 and has 456 scripts. // * The French PC version uses scripts 29 and 104 and has 467 scripts. // * The German Mac version uses scripts 29 and 104 and has 469 scripts. // * The German, Italian, Portuguese and Spanish PC versions // use script 29. The other script is not currently known. // * The US Mac demo uses script 28. // // For now we assume that if there are more than 460 scripts, then // the pair 29/104 is used, else the pair 28/103. if (_res->_types[rtScript].size() > 460) { if (sentenceScript == 104) sentenceScript = 29; } else { if (sentenceScript == 103) sentenceScript = 28; } } } _currentScript = 0xFF; if (sentenceScript) runScript(sentenceScript, 0, 0, localParamList); } void ScummEngine_v0::walkToActorOrObject(int object) { int x, y, dir; Actor_v0 *a = (Actor_v0 *)derefActor(VAR(VAR_EGO), "walkToObject"); _walkToObject = object; _walkToObjectState = kWalkToObjectStateWalk; if (OBJECT_V0_TYPE(object) == kObjectV0TypeActor) { walkActorToActor(VAR(VAR_EGO), OBJECT_V0_ID(object), 4); x = a->getRealPos().x; y = a->getRealPos().y; } else { walkActorToObject(VAR(VAR_EGO), object); getObjectXYPos(object, x, y, dir); } VAR(6) = x; VAR(7) = y; // actor must not move if frozen if (a->_miscflags & kActorMiscFlagFreeze) { a->stopActorMoving(); a->_newWalkBoxEntered = false; } } bool ScummEngine_v0::checkPendingWalkAction() { // before a sentence script is executed, it might be necessary to walk to // and pickup objects before. Check if such an action is pending and handle // it if available. if (_walkToObjectState == kWalkToObjectStateDone) return false; int actor = VAR(VAR_EGO); Actor_v0 *a = (Actor_v0 *)derefActor(actor, "checkPendingWalkAction"); // wait until walking or turning action is finished if (a->_moving != 2) return true; // after walking and turning finally execute the script if (_walkToObjectState == kWalkToObjectStateTurn) { runSentenceScript(); // change actor facing } else { int x, y, distX, distY; if (objIsActor(_walkToObject)) { Actor *b = derefActor(objToActor(_walkToObject), "checkPendingWalkAction(2)"); x = b->getRealPos().x; y = b->getRealPos().y; if (x < a->getRealPos().x) x += 4; else x -= 4; } else { getObjectXYPos(_walkToObject, x, y); } AdjustBoxResult abr = a->adjustXYToBeInBox(x, y); distX = ABS(a->getRealPos().x - abr.x); distY = ABS(a->getRealPos().y - abr.y); if (distX <= 4 && distY <= 8) { if (objIsActor(_walkToObject)) { // walk to actor finished // make actors turn to each other a->faceToObject(_walkToObject); int otherActor = objToActor(_walkToObject); // ignore the plant if (otherActor != 19) { Actor *b = derefActor(otherActor, "checkPendingWalkAction(3)"); b->faceToObject(actorToObj(actor)); } } else { // walk to object finished int tmpX, tmpY, dir; getObjectXYPos(_walkToObject, tmpX, tmpY, dir); a->turnToDirection(dir); } _walkToObjectState = kWalkToObjectStateTurn; return true; } } _walkToObjectState = kWalkToObjectStateDone; return false; } void ScummEngine_v0::checkAndRunSentenceScript() { if (checkPendingWalkAction()) return; if (!_sentenceNum || _sentence[_sentenceNum - 1].freezeCount) return; SentenceTab &st = _sentence[_sentenceNum - 1]; if (st.preposition && st.objectB == st.objectA) { _sentenceNum--; return; } _currentScript = 0xFF; assert(st.objectA); // If two objects are involved, at least one must be in the actors inventory if (st.objectB && (OBJECT_V0_TYPE(st.objectA) != kObjectV0TypeFG || _objectOwnerTable[st.objectA] != VAR(VAR_EGO)) && (OBJECT_V0_TYPE(st.objectB) != kObjectV0TypeFG || _objectOwnerTable[st.objectB] != VAR(VAR_EGO))) { if (getVerbEntrypoint(st.objectA, kVerbPickUp)) doSentence(kVerbPickUp, st.objectA, 0); else if (getVerbEntrypoint(st.objectB, kVerbPickUp)) doSentence(kVerbPickUp, st.objectB, 0); else _sentenceNum--; return; } _cmdVerb = st.verb; _cmdObject = st.objectA; _cmdObject2 = st.objectB; _sentenceNum--; // abort sentence execution if the number of nested scripts is too high. // This might happen for instance if the sentence command depends on an // object that the actor has to pick-up in a nested doSentence() call. // If the actor is not able to pick-up the object (e.g. because it is not // reachable or pickupable) a nested pick-up command is triggered again // and again, so the actual sentence command will never be executed. // In this case the sentence command has to be aborted. _sentenceNestedCount++; if (_sentenceNestedCount > 6) { _sentenceNestedCount = 0; _sentenceNum = 0; return; } if (whereIsObject(st.objectA) != WIO_INVENTORY) { if (_currentMode != kModeKeypad) { walkToActorOrObject(st.objectA); return; } } else if (st.objectB && whereIsObject(st.objectB) != WIO_INVENTORY) { walkToActorOrObject(st.objectB); return; } runSentenceScript(); if (_currentMode == kModeKeypad) { _walkToObjectState = kWalkToObjectStateDone; } } void ScummEngine_v0::runSentenceScript() { _redrawSentenceLine = true; if (getVerbEntrypoint(_cmdObject, _cmdVerb) != 0) { // do not read in the dark if (!(_cmdVerb == kVerbRead && _currentLights == 0)) { VAR(VAR_ACTIVE_OBJECT2) = OBJECT_V0_ID(_cmdObject2); runObjectScript(_cmdObject, _cmdVerb, false, false, NULL); return; } } else { if (_cmdVerb == kVerbGive) { // no "give to"-script: give to other kid or ignore int actor = OBJECT_V0_ID(_cmdObject2); if (actor < 8) setOwnerOf(_cmdObject, actor); return; } } if (_cmdVerb != kVerbWalkTo) { // perform verb's fallback action VAR(VAR_ACTIVE_VERB) = _cmdVerb; runScript(3, 0, 0, 0); } } void ScummEngine_v2::runInputScript(int clickArea, int val, int mode) { int args[NUM_SCRIPT_LOCAL]; int verbScript; verbScript = 4; VAR(VAR_CLICK_AREA) = clickArea; switch (clickArea) { case kVerbClickArea: // Verb clicked VAR(VAR_CLICK_VERB) = val; break; case kInventoryClickArea: // Inventory clicked VAR(VAR_CLICK_OBJECT) = val; break; } memset(args, 0, sizeof(args)); args[0] = clickArea; args[1] = val; args[2] = mode; if (verbScript) runScript(verbScript, 0, 0, args); } void ScummEngine::runInputScript(int clickArea, int val, int mode) { int args[NUM_SCRIPT_LOCAL]; int verbScript; verbScript = VAR(VAR_VERB_SCRIPT); memset(args, 0, sizeof(args)); args[0] = clickArea; args[1] = val; args[2] = mode; // All HE 72+ games but only some HE 71 games. if (_game.heversion >= 71) { args[3] = VAR(VAR_VIRT_MOUSE_X); args[4] = VAR(VAR_VIRT_MOUSE_Y); } // Macintosh version of indy3ega used different interface, so adjust values. if (_game.id == GID_INDY3 && _game.platform == Common::kPlatformMacintosh) { if (clickArea == kVerbClickArea && (val >= 101 && val <= 108)) { if (val == 107) { VAR(67) -= 2; inventoryScriptIndy3Mac(); return; } else if (val == 108) { VAR(67) += 2; inventoryScriptIndy3Mac(); return; } else { args[0] = kInventoryClickArea; args[1] = VAR(82 + (val - 100)); } } // Clicks are handled differently in Indy3 mac: param 2 of the // input script is set to 0 for normal clicks, and to 1 for double clicks. // The EGA DOS version of Loom also checks that the second click happens // close enough to the first one, but that seems like overkill. uint32 time = _system->getMillis(); args[2] = (time < _lastInputScriptTime + 500); // 500 ms double click delay _lastInputScriptTime = time; } else if (_game.id == GID_LOOM && _game.platform == Common::kPlatformMacintosh) { uint32 time = _system->getMillis(); VAR(52) = (time < _lastInputScriptTime + 500); // 500 ms double click delay _lastInputScriptTime = time; } if (verbScript) runScript(verbScript, 0, 0, args); } void ScummEngine::decreaseScriptDelay(int amount) { ScriptSlot *ss = vm.slot; int i; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) { if (ss->status == ssPaused) { ss->delay -= amount; if (ss->delay < 0) { ss->status = ssRunning; ss->delay = 0; } } } } bool ScummEngine::isScriptInUse(int script) const { int i; const ScriptSlot *ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) if (ss->number == script) return true; return false; } bool ScummEngine::isScriptRunning(int script) const { int i; const ScriptSlot *ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) if (ss->number == script && (ss->where == WIO_GLOBAL || ss->where == WIO_LOCAL) && ss->status != ssDead) return true; return false; } bool ScummEngine::isRoomScriptRunning(int script) const { int i; const ScriptSlot *ss = vm.slot; for (i = 0; i < NUM_SCRIPT_SLOT; i++, ss++) if (ss->number == script && ss->where == WIO_ROOM && ss->status != ssDead) return true; return false; } void ScummEngine::copyScriptString(byte *dst) { int len = resStrLen(_scriptPointer) + 1; memcpy(dst, _scriptPointer, len); _scriptPointer += len; dst += len; *dst = 0; } /** * Given a pointer to a Scumm string, this function returns the total * byte length of the string data in that resource. To do so it has to * understand certain special characters embedded into the string. The * reason for this function is that sometimes this embedded data * contains zero bytes, thus we can't just use strlen. */ int ScummEngine::resStrLen(const byte *src) { int num = 0; byte chr; if (src == NULL) { refreshScriptPointer(); src = _scriptPointer; } while ((chr = *src++) != 0) { num++; if (_game.heversion <= 71 && chr == 0xFF) { chr = *src++; num++; // WORKAROUND for bug #985948, a script bug in Indy3. See also // the corresponding code in ScummEngine::convertMessageToString(). if (_game.id == GID_INDY3 && chr == 0x2E) { continue; } if (chr != 1 && chr != 2 && chr != 3 && chr != 8) { if (_game.version == 8) { src += 4; num += 4; } else { src += 2; num += 2; } } } } return num; } void ScummEngine::beginCutscene(int *args) { int scr = _currentScript; vm.slot[scr].cutsceneOverride++; ++vm.cutSceneStackPointer; if (vm.cutSceneStackPointer >= kMaxCutsceneNum) error("Cutscene stack overflow"); vm.cutSceneData[vm.cutSceneStackPointer] = args[0]; vm.cutSceneScript[vm.cutSceneStackPointer] = 0; vm.cutScenePtr[vm.cutSceneStackPointer] = 0; vm.cutSceneScriptIndex = scr; if (VAR(VAR_CUTSCENE_START_SCRIPT)) runScript(VAR(VAR_CUTSCENE_START_SCRIPT), 0, 0, args); vm.cutSceneScriptIndex = 0xFF; } void ScummEngine::endCutscene() { ScriptSlot *ss = &vm.slot[_currentScript]; int args[NUM_SCRIPT_LOCAL]; if (ss->cutsceneOverride > 0) // Only terminate if active ss->cutsceneOverride--; memset(args, 0, sizeof(args)); args[0] = vm.cutSceneData[vm.cutSceneStackPointer]; VAR(VAR_OVERRIDE) = 0; if (vm.cutScenePtr[vm.cutSceneStackPointer] && (ss->cutsceneOverride > 0)) // Only terminate if active ss->cutsceneOverride--; vm.cutSceneScript[vm.cutSceneStackPointer] = 0; vm.cutScenePtr[vm.cutSceneStackPointer] = 0; if (0 == vm.cutSceneStackPointer) error("Cutscene stack underflow"); vm.cutSceneStackPointer--; if (VAR(VAR_CUTSCENE_END_SCRIPT)) runScript(VAR(VAR_CUTSCENE_END_SCRIPT), 0, 0, args); } void ScummEngine::abortCutscene() { const int idx = vm.cutSceneStackPointer; assert(0 <= idx && idx < kMaxCutsceneNum); uint32 offs = vm.cutScenePtr[idx]; if (offs) { ScriptSlot *ss = &vm.slot[vm.cutSceneScript[idx]]; ss->offs = offs; ss->status = ssRunning; ss->freezeCount = 0; if (ss->cutsceneOverride > 0) ss->cutsceneOverride--; VAR(VAR_OVERRIDE) = 1; vm.cutScenePtr[idx] = 0; } } void ScummEngine::beginOverride() { const int idx = vm.cutSceneStackPointer; assert(0 <= idx && idx < kMaxCutsceneNum); vm.cutScenePtr[idx] = _scriptPointer - _scriptOrgPointer; vm.cutSceneScript[idx] = _currentScript; // Skip the jump instruction following the override instruction // (the jump is responsible for "skipping" cutscenes, and the reason // why we record the current script position in vm.cutScenePtr). fetchScriptByte(); fetchScriptWord(); if (_game.version >= 5) VAR(VAR_OVERRIDE) = 0; } void ScummEngine::endOverride() { const int idx = vm.cutSceneStackPointer; assert(0 <= idx && idx < kMaxCutsceneNum); vm.cutScenePtr[idx] = 0; vm.cutSceneScript[idx] = 0; if (_game.version >= 4) VAR(VAR_OVERRIDE) = 0; } } // End of namespace Scumm