/* 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