/* 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 3 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, see . * */ #include "common/random.h" #include "vcruise/ad2044_items.h" #include "vcruise/audio_player.h" #include "vcruise/circuitpuzzle.h" #include "vcruise/runtime.h" #include "vcruise/script.h" namespace VCruise { struct AD2044UnusualAnimationRules { enum Type { kTypeLoop, // Loop the animation kTypePlayFirstFrameOnly, kTypeSkip, }; uint roomNumber; uint screenNumber; uint interactionID; uint8 animLookupID; Type ruleType; }; AD2044UnusualAnimationRules g_unusualAnimationRules[] = { // Room, screen, interaction, animation lookup ID, rule {87, 0x24, 0xa4, 0xa7, AD2044UnusualAnimationRules::kTypePlayFirstFrameOnly}, // Taking cigarette, don't play box spin {87, 0x68, 0xa3, 0xa5, AD2044UnusualAnimationRules::kTypeLoop}, // Loop lit cigarette animation {87, 0x69, 0xa3, 0xa5, AD2044UnusualAnimationRules::kTypeSkip}, // Taking lit cigarette, don't play cycling animation }; #ifdef PEEK_STACK #error "PEEK_STACK is already defined" #endif #ifdef TAKE_STACK #error "TAKE_STACK is already defined" #endif #ifdef OPCODE_STUB #error "OPCODE_STUB is already defined" #endif #define PEEK_STACK(n) \ if (!requireAvailableStack(n)) \ return; \ const StackValue *stackArgs = &this->_scriptStack[this->_scriptStack.size() - (n)] #define TAKE_STACK_INT_NAMED(n, arrayName) \ StackInt_t arrayName[n]; \ do { \ if (!requireAvailableStack(n)) \ return; \ const uint stackSize = _scriptStack.size(); \ const StackValue *stackArgsPtr = &this->_scriptStack[stackSize - (n)]; \ for (uint i = 0; i < (n); i++) { \ if (stackArgsPtr[i].type != StackValue::kNumber) \ error("Expected op argument %u to be a number", i); \ arrayName[i] = stackArgsPtr[i].value.i; \ } \ this->_scriptStack.resize(stackSize - (n)); \ } while (false) #define TAKE_STACK_INT(n) TAKE_STACK_INT_NAMED(n, stackArgs) #define TAKE_STACK_STR_NAMED(n, arrayName) \ Common::String arrayName[n]; \ do { \ if (!requireAvailableStack(n)) \ return; \ const uint stackSize = _scriptStack.size(); \ const StackValue *stackArgsPtr = &this->_scriptStack[stackSize - (n)]; \ for (uint i = 0; i < (n); i++) { \ if (stackArgsPtr[i].type != StackValue::kString) \ error("Expected op argument %u to be a string", i); \ arrayName[i] = Common::move(stackArgsPtr[i].value.s); \ } \ this->_scriptStack.resize(stackSize - (n)); \ } while (false) #define TAKE_STACK_STR(n) TAKE_STACK_STR_NAMED(n, stackArgs) #define TAKE_STACK_VAR_NAMED(n, arrayName) \ StackValue arrayName[n]; \ do { \ if (!requireAvailableStack(n)) \ return; \ const uint stackSize = _scriptStack.size(); \ const StackValue *stackArgsPtr = &this->_scriptStack[stackSize - (n)]; \ for (uint i = 0; i < (n); i++) \ arrayName[i] = Common::move(stackArgsPtr[i]); \ this->_scriptStack.resize(stackSize - (n)); \ } while (false) #define TAKE_STACK_VAR(n) TAKE_STACK_VAR_NAMED(n, stackArgs) #define OPCODE_STUB(op) \ void Runtime::scriptOp##op(ScriptArg_t arg) { \ error("Unimplemented opcode '" #op "'"); \ } void Runtime::scriptOpNumber(ScriptArg_t arg) { _scriptStack.push_back(StackValue(arg)); } void Runtime::scriptOpRotate(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs + kAnimDefStackArgs); _panLeftAnimationDef = stackArgsToAnimDef(stackArgs + 0); _panRightAnimationDef = stackArgsToAnimDef(stackArgs + kAnimDefStackArgs); _haveHorizPanAnimations = true; } void Runtime::scriptOpAngle(ScriptArg_t arg) { TAKE_STACK_INT(1); _scriptStack.push_back(StackValue((stackArgs[0] == static_cast(_direction)) ? 1 : 0)); } void Runtime::scriptOpAngleGGet(ScriptArg_t arg) { TAKE_STACK_INT(1); if (stackArgs[0] < 0 || stackArgs[0] >= static_cast(GyroState::kNumGyros)) error("Invalid gyro index in angleGGet op"); _scriptStack.push_back(StackValue(_gyros.gyros[stackArgs[0]].currentState)); } void Runtime::scriptOpSpeed(ScriptArg_t arg) { TAKE_STACK_INT(1); _scriptEnv.fpsOverride = stackArgs[0]; } void Runtime::scriptOpSAnimL(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs + 2); if (stackArgs[kAnimDefStackArgs] != 0) warning("sanimL second operand wasn't zero (what does that do?)"); AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0); uint direction = stackArgs[kAnimDefStackArgs + 1]; if (direction >= kNumDirections) error("sanimL invalid direction"); _haveIdleAnimations[direction] = true; StaticAnimation &outAnim = _idleAnimations[direction]; outAnim = StaticAnimation(); outAnim.animDefs[0] = animDef; outAnim.animDefs[1] = animDef; } void Runtime::scriptOpChangeL(ScriptArg_t arg) { TAKE_STACK_INT(1); // ChangeL changes the screen number. // // If this isn't an entry script, then this must also re-trigger the entry script. // // In Reah, it also forces screen entry scripts to replay, which is needed for things like the fountain. // // In Schizm, it's needed for the preset buttons in the airship navigation coordinates to work correctly // (Room 41 screen 0c2h) // // The check is required because otherwise, this causes an infinite loop in the temple when approaching the // bells puzzle (Room 65 screen 0b2h) due to fnMlynekZerowanie -> 1 fnMlynkiLokacja -> changeL to MLYNKIZLEWEJ1 _screenNumber = stackArgs[0]; _havePendingScreenChange = true; if (!_scriptEnv.isEntryScript) _forceScreenChange = true; } void Runtime::scriptOpAnimR(ScriptArg_t arg) { bool isRight = false; if (_scriptEnv.panInteractionID == kPanLeftInteraction) { debug(1, "Pan-left interaction from direction %u", _direction); uint reverseDirectionSlice = (kNumDirections - _direction); if (reverseDirectionSlice == kNumDirections) reverseDirectionSlice = 0; uint initialFrame = reverseDirectionSlice * (_panLeftAnimationDef.lastFrame - _panLeftAnimationDef.firstFrame) / kNumDirections + _panLeftAnimationDef.firstFrame; AnimationDef trimmedAnimation = _panLeftAnimationDef; trimmedAnimation.lastFrame--; debug(1, "Running frame loop of %u - %u from frame %u", trimmedAnimation.firstFrame, trimmedAnimation.lastFrame, initialFrame); changeAnimation(trimmedAnimation, initialFrame, false, _animSpeedRotation); _gameState = kGameStatePanLeft; } else if (_scriptEnv.panInteractionID == kPanRightInteraction) { debug(1, "Pan-right interaction from direction %u", _direction); uint initialFrame = _direction * (_panRightAnimationDef.lastFrame - _panRightAnimationDef.firstFrame) / kNumDirections + _panRightAnimationDef.firstFrame; AnimationDef trimmedAnimation = _panRightAnimationDef; trimmedAnimation.lastFrame--; debug(1, "Running frame loop of %u - %u from frame %u", trimmedAnimation.firstFrame, trimmedAnimation.lastFrame, initialFrame); changeAnimation(trimmedAnimation, initialFrame, false, _animSpeedRotation); _gameState = kGameStatePanRight; isRight = true; } uint cursorID = 0; if (_haveHorizPanAnimations) { uint panCursor = kPanCursorDraggableHoriz; if (isRight) panCursor |= kPanCursorDirectionRight; else panCursor |= kPanCursorDirectionLeft; cursorID = _panCursors[panCursor]; } changeToCursor(_cursors[cursorID]); drawCompass(); } void Runtime::scriptOpAnimF(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs + 3); AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0); const AnimationDef *faceDirectionAnimDef = nullptr; uint initialFrame = 0; uint stopFrame = 0; if (computeFaceDirectionAnimation(stackArgs[kAnimDefStackArgs + 2], faceDirectionAnimDef, initialFrame, stopFrame)) { _postFacingAnimDef = animDef; _animStopFrame = stopFrame; changeAnimation(*faceDirectionAnimDef, initialFrame, false, _animSpeedRotation); _gameState = kGameStateWaitingForFacingToAnim; } else { consumeAnimChangeAndAdjustAnim(animDef); // Needed for Schizm when entering the statue after finishing the temple. changeAnimation(animDef, animDef.firstFrame, true, _animSpeedDefault); _gameState = kGameStateWaitingForAnimation; } _screenNumber = stackArgs[kAnimDefStackArgs + 0]; _direction = stackArgs[kAnimDefStackArgs + 1]; _havePendingScreenChange = true; clearIdleAnimations(); uint cursorID = kCursorArrow; if (_scriptEnv.panInteractionID == kPanUpInteraction) cursorID = _panCursors[kPanCursorDraggableUp | kPanCursorDirectionUp]; else if (_scriptEnv.panInteractionID == kPanDownInteraction) cursorID = _panCursors[kPanCursorDraggableDown | kPanCursorDirectionDown]; changeToCursor(_cursors[cursorID]); } void Runtime::scriptOpAnimN(ScriptArg_t arg) { TAKE_STACK_INT(1); const AnimationDef *faceDirectionAnimDef = nullptr; uint initialFrame = 0; uint stopFrame = 0; if (computeFaceDirectionAnimation(stackArgs[0], faceDirectionAnimDef, initialFrame, stopFrame)) { _animStopFrame = stopFrame; changeAnimation(*faceDirectionAnimDef, initialFrame, false, _animSpeedRotation); _gameState = kGameStateWaitingForFacing; } _direction = stackArgs[0]; _havePendingScreenChange = true; changeToCursor(_cursors[kCursorArrow]); } void Runtime::scriptOpAnimG(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs * 2 + 1); _gyros.posAnim = stackArgsToAnimDef(stackArgs + 0); _gyros.negAnim = stackArgsToAnimDef(stackArgs + kAnimDefStackArgs); _gyros.isVertical = (stackArgs[kAnimDefStackArgs * 2 + 0] != 0); if (_gyros.isVertical) changeToCursor(_cursors[_panCursors[kPanCursorDraggableUp | kPanCursorDraggableDown]]); else changeToCursor(_cursors[_panCursors[kPanCursorDraggableHoriz]]); _gyros.dragBasePoint = _mousePos; _gyros.dragBaseState = _gyros.gyros[_gyros.activeGyro].currentState; _gyros.dragCurrentState = _gyros.dragBaseState; _gameState = kGameStateGyroIdle; } void Runtime::scriptOpAnimS(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs + 2); AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0); consumeAnimChangeAndAdjustAnim(animDef); // Static animations start on the last frame changeAnimation(animDef, animDef.lastFrame, false); // We have a choice of when to terminate animations: At the start of the final frame, or at the end of the final frame. // Terminating at the start of the final frame means many frames can play in a single gameplay frame. // // In Reah, we terminate at the start because it doesn't really cause problems anywhere and helps some things like // the basket weight puzzle in the bathhouse. // // In Schizm, several things like the mechanical computer and balloon gas puzzle pressure meter don't behave // well when doing this, so we terminate at the end of the frame instead there. _animTerminateAtStartOfFrame = (_gameID == GID_SCHIZM); _gameState = kGameStateWaitingForAnimation; _screenNumber = stackArgs[kAnimDefStackArgs + 0]; _direction = stackArgs[kAnimDefStackArgs + 1]; _havePendingScreenChange = true; changeToCursor(_cursors[kCursorArrow]); } void Runtime::scriptOpAnim(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs + 2); AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0); consumeAnimChangeAndAdjustAnim(animDef); changeAnimation(animDef, animDef.firstFrame, true, _animSpeedDefault); _gameState = kGameStateWaitingForAnimation; _screenNumber = stackArgs[kAnimDefStackArgs + 0]; _direction = stackArgs[kAnimDefStackArgs + 1]; _havePendingScreenChange = true; clearIdleAnimations(); if (_loadedAnimationHasSound) changeToCursor(nullptr); else { uint cursorID = kCursorArrow; if (_scriptEnv.panInteractionID == kPanUpInteraction) cursorID = _panCursors[kPanCursorDraggableUp | kPanCursorDirectionUp]; else if (_scriptEnv.panInteractionID == kPanDownInteraction) cursorID = _panCursors[kPanCursorDraggableDown | kPanCursorDirectionDown]; changeToCursor(_cursors[cursorID]); } } void Runtime::scriptOpStatic(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs); debug(10, "scriptOpStatic() kAnimDefStackArgs: %d", kAnimDefStackArgs); for (uint i = 0; i < kAnimDefStackArgs; i++) { debug(10, "\tstackArgs[%d]: %d", i, stackArgs[i]); } // FIXME: What does this actually do? // It looks like this sets the last frame of an animation as the current scene graphic, but // in some cases that's wrong. For instance, after solving the temple puzzle in Reah, viewing // the rock on the left (screen 0c4 in room 20) runs ":PLANAS_SKALA static" after the rock // symbol displays. However, :PLANAS_SKALA shows the rock with no symbol. // // Another problem occurs when viewing the rotor puzzle in the citadel, described below for now. #if 0 // QUIRK/BUG WORKAROUND: Static animations don't override other static animations! // // In Reah Room05, the script for 0b8 (NGONG) sets the static animation to :NNAWA_NGONG and then // to :NSWIT_SGONG, but NNAWA_NGONG is the correct one, so we must ignore the second static animation if (_haveIdleStaticAnimation) return; AnimationDef animDef = stackArgsToAnimDef(stackArgs); // QUIRK: In the Reah citadel rotor puzzle, all of the "BKOLO" screens execute :DKOLO1_BKOLO1 static but // doing that would replace the transition animation's last frame with the new static animation frame, // blanking out the puzzle, so we must detect if the new static animation is the same as the existing // one and if so, ignore it. if (animDef.animName == _idleCurrentStaticAnimation) return; // FIXME: _idleCurrentStaticAnimation must be cleared sometime! Maybe on loading a save. changeAnimation(animDef, animDef.lastFrame, false, _animSpeedStaticAnim); _havePendingPreIdleActions = true; _haveHorizPanAnimations = false; _haveIdleStaticAnimation = true; _idleCurrentStaticAnimation = animDef.animName; _gameState = kGameStateWaitingForAnimation; #endif } void Runtime::scriptOpVarLoad(ScriptArg_t arg) { TAKE_STACK_INT(1); uint32 varID = (static_cast(_roomNumber) << 16) | static_cast(stackArgs[0]); Common::HashMap::const_iterator it = _variables.find(varID); if (it == _variables.end()) _scriptStack.push_back(StackValue(0)); else _scriptStack.push_back(StackValue(it->_value)); } void Runtime::scriptOpVarStore(ScriptArg_t arg) { TAKE_STACK_INT(2); uint32 varID = (static_cast(_roomNumber) << 16) | static_cast(stackArgs[1]); _variables[varID] = stackArgs[0]; } void Runtime::scriptOpVarAddAndStore(ScriptArg_t arg) { TAKE_STACK_INT(2); uint32 varID = (static_cast(_roomNumber) << 16) | static_cast(stackArgs[0]); Common::HashMap::iterator it = _variables.find(varID); if (it == _variables.end()) _variables[varID] = stackArgs[1]; else it->_value += stackArgs[1]; } void Runtime::scriptOpVarGlobalLoad(ScriptArg_t arg) { TAKE_STACK_INT(1); uint32 varID = static_cast(stackArgs[0]); Common::HashMap::const_iterator it = _variables.find(varID); if (it == _variables.end()) _scriptStack.push_back(StackValue(0)); else _scriptStack.push_back(StackValue(it->_value)); } void Runtime::scriptOpVarGlobalStore(ScriptArg_t arg) { TAKE_STACK_INT(2); uint32 varID = static_cast(stackArgs[1]); _variables[varID] = stackArgs[0]; } void Runtime::scriptOpItemCheck(ScriptArg_t arg) { TAKE_STACK_INT(1); for (const InventoryItem &item : _inventory) { if (item.itemID == static_cast(stackArgs[0])) { _scriptEnv.lastHighlightedItem = item.itemID; _scriptStack.push_back(StackValue(1)); return; } } _scriptStack.push_back(StackValue(0)); } void Runtime::scriptOpItemRemove(ScriptArg_t arg) { TAKE_STACK_INT(1); inventoryRemoveItem(stackArgs[0]); } void Runtime::scriptOpItemHighlightSet(ScriptArg_t arg) { TAKE_STACK_INT(2); bool isHighlighted = (stackArgs[1] != 0); for (uint slot = 0; slot < kNumInventorySlots; slot++) { InventoryItem &item = _inventory[slot]; if (item.itemID == static_cast(stackArgs[0])) { item.highlighted = isHighlighted; drawInventory(slot); break; } } } void Runtime::scriptOpItemHighlightSetTrue(ScriptArg_t arg) { TAKE_STACK_INT(1); for (uint slot = 0; slot < kNumInventorySlots; slot++) { InventoryItem &item = _inventory[slot]; if (item.itemID == static_cast(stackArgs[0])) { item.highlighted = true; drawInventory(slot); break; } } } void Runtime::scriptOpItemAdd(ScriptArg_t arg) { TAKE_STACK_INT(1); if (stackArgs[0] == 0) { // Weird special case, happens in Reah when breaking the glass barrier, this is called with 0 as the parameter. // This can't be an inventory clear because it will not clear the crutch, but it does take away the gong beater, // so the only explanation I can think of is that it clears the previously-checked inventory item. inventoryRemoveItem(_scriptEnv.lastHighlightedItem); } else inventoryAddItem(stackArgs[0]); } void Runtime::scriptOpItemClear(ScriptArg_t arg) { for (uint slot = 0; slot < kNumInventorySlots; slot++) { InventoryItem &item = _inventory[slot]; if (item.itemID != 0) { item.highlighted = false; item.itemID = 0; item.graphic.reset(); item.mask.reset(); drawInventory(slot); } } } void Runtime::scriptOpItemHaveSpace(ScriptArg_t arg) { for (const InventoryItem &item : _inventory) { if (item.itemID == 0) { _scriptStack.push_back(StackValue(1)); return; } } _scriptStack.push_back(StackValue(0)); } void Runtime::scriptOpSetCursor(ScriptArg_t arg) { TAKE_STACK_INT(1); if (stackArgs[0] < 0 || static_cast(stackArgs[0]) >= _cursors.size()) error("Invalid cursor ID"); uint resolvedCursorID = stackArgs[0]; Common::HashMap::const_iterator overrideIt = _scriptCursorIDToResourceIDOverride.find(resolvedCursorID); if (overrideIt != _scriptCursorIDToResourceIDOverride.end()) resolvedCursorID = overrideIt->_value; changeToCursor(_cursors[resolvedCursorID]); } void Runtime::scriptOpSetRoom(ScriptArg_t arg) { TAKE_STACK_INT(1); _roomNumber = stackArgs[0]; } void Runtime::scriptOpLMB(ScriptArg_t arg) { if (!_scriptEnv.lmb) { _idleHaveClickInteraction = true; terminateScript(); } } void Runtime::scriptOpLMB1(ScriptArg_t arg) { if (!_scriptEnv.lmbDrag) { _idleHaveDragInteraction = true; terminateScript(); } } void Runtime::scriptOpSoundS1(ScriptArg_t arg) { TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, false); } void Runtime::scriptOpSoundS2(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(1, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[0], 0, false, false); } void Runtime::scriptOpSoundS3(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false); } void Runtime::scriptOpSoundL1(ScriptArg_t arg) { TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSound(kSoundLoopBehaviorYes, *cachedSound, getDefaultSoundVolume(), 0, false, false); } void Runtime::scriptOpSoundL2(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(1, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], 0, false, false); } void Runtime::scriptOpSoundL3(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], sndParamArgs[1], false, false); } void Runtime::scriptOp3DSoundL2(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(3, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) { setSound3DParameters(*cachedSound, sndParamArgs[1], sndParamArgs[2], _pendingSoundParams3D); triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], 0, true, false); } } void Runtime::scriptOp3DSoundL3(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(4, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) { setSound3DParameters(*cachedSound, sndParamArgs[2], sndParamArgs[3], _pendingSoundParams3D); triggerSound(kSoundLoopBehaviorYes, *cachedSound, sndParamArgs[0], sndParamArgs[1], true, false); } } void Runtime::scriptOp3DSoundS2(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(3, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) { setSound3DParameters(*cachedSound, sndParamArgs[1], sndParamArgs[2], _pendingSoundParams3D); triggerSound(kSoundLoopBehaviorNo, *cachedSound, sndParamArgs[0], 0, true, false); } } void Runtime::scriptOpStopAL(ScriptArg_t arg) { warning("stopaL not implemented yet"); } void Runtime::scriptOpAddXSound(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(3, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); RandomAmbientSound sound; sound.name = sndNameArgs[0]; sound.volume = sndParamArgs[0]; sound.balance = sndParamArgs[1]; sound.frequency = sndParamArgs[2]; _randomAmbientSounds.push_back(sound); } void Runtime::scriptOpClrXSound(ScriptArg_t arg) { _randomAmbientSounds.clear(); } void Runtime::scriptOpStopSndLA(ScriptArg_t arg) { warning("StopSndLA not implemented yet"); } void Runtime::scriptOpStopSndLO(ScriptArg_t arg) { TAKE_STACK_VAR(1); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByNameOrID(stackArgs[0], false, soundID, cachedSound); if (cachedSound) stopSound(*cachedSound); } void Runtime::scriptOpRange(ScriptArg_t arg) { TAKE_STACK_INT(3); _pendingSoundParams3D.minRange = stackArgs[0]; _pendingSoundParams3D.maxRange = stackArgs[1]; _pendingSoundParams3D.unknownRange = stackArgs[2]; } void Runtime::scriptOpMusic(ScriptArg_t arg) { TAKE_STACK_INT(1); changeMusicTrack(stackArgs[0]); } void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) { TAKE_STACK_INT(2); uint32 duration = static_cast(stackArgs[0]) * 100u; int32 newVolume = stackArgs[1]; _musicVolumeRampRatePerMSec = 0; if (duration == 0) { _musicVolume = newVolume; if (_musicWavePlayer) _musicWavePlayer->setVolume(newVolume); } else { if (newVolume != _musicVolume) { uint32 timestamp = g_system->getMillis(); _musicVolumeRampRatePerMSec = (newVolume - _musicVolume) * 65536 / static_cast(duration); _musicVolumeRampStartTime = timestamp; _musicVolumeRampStartVolume = _musicVolume; _musicVolumeRampEnd = newVolume; } } } void Runtime::scriptOpParm0(ScriptArg_t arg) { TAKE_STACK_INT(4); if (stackArgs[0] < 0 || static_cast(stackArgs[0]) >= GyroState::kNumGyros) error("Invalid gyro index for Parm0"); uint gyroIndex = stackArgs[0]; Gyro &gyro = _gyros.gyros[gyroIndex]; gyro.numPreviousStatesRequired = 3; for (uint i = 0; i < 3; i++) gyro.requiredPreviousStates[i] = stackArgs[i + 1]; } void Runtime::scriptOpParm1(ScriptArg_t arg) { TAKE_STACK_INT(3); if (stackArgs[0] < 0 || static_cast(stackArgs[0]) >= GyroState::kNumGyros) error("Invalid gyro index for Parm1"); uint gyroIndex = stackArgs[0]; Gyro &gyro = _gyros.gyros[gyroIndex]; gyro.currentState = stackArgs[1]; gyro.requiredState = stackArgs[2]; gyro.requireState = true; } void Runtime::scriptOpParm2(ScriptArg_t arg) { TAKE_STACK_INT(3); _gyros.completeInteraction = stackArgs[0]; _gyros.failureInteraction = stackArgs[1]; _gyros.frameSeparation = stackArgs[2]; if (_gyros.frameSeparation <= 0) error("Invalid gyro frame separation"); } void Runtime::scriptOpParm3(ScriptArg_t arg) { TAKE_STACK_INT(1); if (stackArgs[0] < 0 || static_cast(stackArgs[0]) >= GyroState::kNumGyros) error("Invalid gyro index for Parm3"); uint gyroIndex = stackArgs[0]; Gyro &gyro = _gyros.gyros[gyroIndex]; gyro.wrapAround = true; } void Runtime::scriptOpParmG(ScriptArg_t arg) { TAKE_STACK_INT(3); int32 gyroSlot = stackArgs[0]; int32 dragMargin = stackArgs[1]; int32 maxValue = stackArgs[2]; if (gyroSlot < 0 || static_cast(gyroSlot) >= GyroState::kNumGyros) error("Invalid gyro slot from ParmG op"); _gyros.activeGyro = gyroSlot; _gyros.dragMargin = dragMargin; _gyros.maxValue = maxValue; } void Runtime::scriptOpSParmX(ScriptArg_t arg) { TAKE_STACK_INT(3); _pendingStaticAnimParams.initialDelay = stackArgs[0]; _pendingStaticAnimParams.repeatDelay = stackArgs[1]; _pendingStaticAnimParams.lockInteractions = (stackArgs[2] != 0); } void Runtime::scriptOpSAnimX(ScriptArg_t arg) { TAKE_STACK_INT(kAnimDefStackArgs * 2 + 1); AnimationDef animDef1 = stackArgsToAnimDef(stackArgs + 0); AnimationDef animDef2 = stackArgsToAnimDef(stackArgs + kAnimDefStackArgs); uint direction = stackArgs[kAnimDefStackArgs * 2 + 0]; if (direction >= kNumDirections) error("sanimX invalid direction"); _haveIdleAnimations[direction] = true; StaticAnimation &outAnim = _idleAnimations[direction]; outAnim = StaticAnimation(); outAnim.animDefs[0] = animDef1; outAnim.animDefs[1] = animDef2; outAnim.params = _pendingStaticAnimParams; } void Runtime::scriptOpVolumeUp3(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_VAR_NAMED(1, sndIDArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByNameOrID(sndIDArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSoundRamp(*cachedSound, sndParamArgs[0] * 100, sndParamArgs[1], false); } void Runtime::scriptOpVolumeDn2(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(1, sndParamArgs); TAKE_STACK_VAR_NAMED(1, sndIDArgs); uint32 durationMSec = static_cast(sndParamArgs[0]) * 100u; if (sndIDArgs[0].type == StackValue::kNumber && sndIDArgs[0].value.i == 0) { // Apply to all sounds for (const Common::SharedPtr &sndPtr : _activeSounds) triggerSoundRamp(*sndPtr, durationMSec, 0, true); } else { StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByNameOrID(sndIDArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSoundRamp(*cachedSound, durationMSec, getSilentSoundVolume(), true); } } void Runtime::scriptOpVolumeDn3(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_VAR_NAMED(1, sndIDArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByNameOrID(sndIDArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSoundRamp(*cachedSound, sndParamArgs[0] * 100, sndParamArgs[1], false); } void Runtime::scriptOpVolumeDn4(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(3, sndParamArgs); TAKE_STACK_VAR_NAMED(1, sndIDArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByNameOrID(sndIDArgs[0], true, soundID, cachedSound); if (cachedSound) triggerSoundRamp(*cachedSound, sndParamArgs[0] * 100, sndParamArgs[1], sndParamArgs[2] != 0); } void Runtime::scriptOpRandom(ScriptArg_t arg) { TAKE_STACK_INT(1); if (stackArgs[0] == 0) _scriptStack.push_back(StackValue(0)); else _scriptStack.push_back(StackValue(_rng->getRandomNumber(stackArgs[0] - 1))); } void Runtime::scriptOpDrop(ScriptArg_t arg) { TAKE_STACK_VAR(1); (void)stackArgs; } void Runtime::scriptOpDup(ScriptArg_t arg) { TAKE_STACK_VAR(1); _scriptStack.push_back(stackArgs[0]); _scriptStack.push_back(stackArgs[0]); } void Runtime::scriptOpSwap(ScriptArg_t arg) { TAKE_STACK_VAR(2); _scriptStack.push_back(Common::move(stackArgs[1])); _scriptStack.push_back(Common::move(stackArgs[0])); } void Runtime::scriptOpSay1(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); // uint unk = sndParamArgs[0]; uint cycleLength = sndParamArgs[1]; debug(5, "Say1 cycle length: %u", cycleLength); Common::String soundIDStr = sndNameArgs[0]; if (soundIDStr.size() < 4) error("Say1 sound name was invalid"); uint32 cycleID = 0; for (uint i = 0; i < 4; i++) { char d = soundIDStr[i]; if (d < '0' || d > '9') error("Invalid sound ID for say1"); cycleID = cycleID * 10 + (d - '0'); } uint &cyclePosRef = _sayCycles[static_cast(cycleID)]; uint32 cycledSoundID = (cyclePosRef + cycleID); cyclePosRef++; if (cyclePosRef == cycleLength) cyclePosRef = 0; soundIDStr = soundIDStr.substr(4); for (uint i = 0; i < 4; i++) { soundIDStr.insertChar(static_cast((cycledSoundID % 10) + '0'), 0); cycledSoundID /= 10; } StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(soundIDStr, true, soundID, cachedSound); if (cachedSound) { triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true); triggerWaveSubtitles(*cachedSound, soundIDStr); } } void Runtime::scriptOpSay2(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) { // The third param seems to control sound interruption, but say3 is a Reah-only op and it's only ever 1. if (sndParamArgs[1] != 1) error("Invalid interrupt arg for say2, only 1 is supported."); triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true); triggerWaveSubtitles(*cachedSound, sndNameArgs[0]); } } void Runtime::scriptOpSay3(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) { TriggeredOneShot oneShot; oneShot.soundID = soundID; oneShot.uniqueSlot = sndParamArgs[0]; // The third param seems to control sound interruption, but say3 is a Reah-only op and it's only ever 1. if (sndParamArgs[1] != 1) error("Invalid interrupt arg for say3, only 1 is supported."); if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) { triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true); _triggeredOneShots.push_back(oneShot); triggerWaveSubtitles(*cachedSound, sndNameArgs[0]); } } } void Runtime::scriptOpSay3Get(ScriptArg_t arg) { TAKE_STACK_INT_NAMED(2, sndParamArgs); TAKE_STACK_STR_NAMED(1, sndNameArgs); StackInt_t soundID = 0; SoundInstance *cachedSound = nullptr; resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound); if (cachedSound) { TriggeredOneShot oneShot; oneShot.soundID = soundID; oneShot.uniqueSlot = sndParamArgs[0]; // The third param seems to control sound interruption, but say3 is a Reah-only op and it's only ever 1. if (sndParamArgs[1] != 1) error("Invalid interrupt arg for say3, only 1 is supported."); if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) { triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true); _triggeredOneShots.push_back(oneShot); _scriptStack.push_back(StackValue(soundID)); } else _scriptStack.push_back(StackValue(0)); } else _scriptStack.push_back(StackValue(0)); } void Runtime::scriptOpSetTimer(ScriptArg_t arg) { TAKE_STACK_INT(2); _timers[static_cast(stackArgs[0])] = g_system->getMillis() + static_cast(stackArgs[1]) * 1000u; } void Runtime::scriptOpGetTimer(ScriptArg_t arg) { TAKE_STACK_INT(1); bool isCompleted = true; Common::HashMap::const_iterator timerIt = _timers.find(stackArgs[0]); if (timerIt != _timers.end()) isCompleted = (g_system->getMillis() >= timerIt->_value); _scriptStack.push_back(StackValue(isCompleted ? 1 : 0)); } void Runtime::scriptOpDelay(ScriptArg_t arg) { TAKE_STACK_INT(1); _gameState = kGameStateDelay; _delayCompletionTime = g_system->getMillis() + stackArgs[0]; } void Runtime::scriptOpLoSet(ScriptArg_t arg) { scriptOpVerticalPanSet(_havePanDownFromDirection); } void Runtime::scriptOpLoGet(ScriptArg_t arg) { scriptOpVerticalPanGet(); } void Runtime::scriptOpHiSet(ScriptArg_t arg) { scriptOpVerticalPanSet(_havePanUpFromDirection); } void Runtime::scriptOpHiGet(ScriptArg_t arg) { scriptOpVerticalPanGet(); } void Runtime::scriptOpVerticalPanSet(bool *flags) { TAKE_STACK_INT(2); uint baseDirection = static_cast(stackArgs[0]) % kNumDirections; uint radius = stackArgs[1]; flags[baseDirection] = true; uint rDir = baseDirection; uint lDir = baseDirection; for (uint i = 1; i <= radius; i++) { rDir++; if (rDir == kNumDirections) rDir = 0; if (lDir == 0) lDir = kNumDirections; lDir--; flags[lDir] = true; flags[rDir] = true; } } void Runtime::scriptOpVerticalPanGet() { TAKE_STACK_INT(2); // In any scenario where this is used, there is a corresponding hi/lo set and this only ever triggers off of interactions, // so don't really even need to check anything other than the facing direction? uint baseDirection = static_cast(stackArgs[0]) % kNumDirections; uint radius = stackArgs[1]; uint rtDirection = (baseDirection + kNumDirections - _direction) % kNumDirections; uint lfDirection = (_direction + kNumDirections - baseDirection) % kNumDirections; bool isInRadius = (rtDirection <= radius || lfDirection <= radius); _scriptStack.push_back(StackValue(isInRadius ? 1 : 0)); } void Runtime::scriptOpCallFunction(ScriptArg_t arg) { Common::SharedPtr